729 lines
26 KiB
C#
729 lines
26 KiB
C#
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Local.Models;
|
|
using Omika.App.Local.Services;
|
|
using Omika.App.Navigation;
|
|
using Omika.App.ViewModels;
|
|
using Omika.Core.Contracts.Configuration;
|
|
using Omika.Core.Contracts.Mobile.License;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests du ViewModel Agenda d'équipe : deux filtres indépendants (équipe +
|
|
/// collaborateur), exclusion mutuelle, chargement à la demande (API), groupement
|
|
/// par date et navigation vers les fiches rendez-vous.
|
|
/// </summary>
|
|
public class AgendaEquipeViewModelTests
|
|
{
|
|
private readonly NavigationService _navigation = new();
|
|
|
|
public AgendaEquipeViewModelTests()
|
|
{
|
|
// L'état de session est statique : on le réinitialise avant chaque test
|
|
// pour éviter les fuites entre scénarios.
|
|
AgendaEquipeViewModel.ResetSessionState();
|
|
}
|
|
|
|
private AgendaEquipeViewModel CreateViewModel(
|
|
IReadOnlyList<EquipeListItem> equipes,
|
|
IReadOnlyList<CollaborateurListItem> collaborateurs,
|
|
IReadOnlyList<AgendaItem> agendaItems,
|
|
DateTime? date = null,
|
|
IReadOnlyList<ParametreMetierDto>? parametresMetier = null,
|
|
StubParametreMetierService? parametreMetierService = null)
|
|
{
|
|
var viewModel = new AgendaEquipeViewModel(
|
|
new FakeAgendaService(agendaItems),
|
|
new FakeEquipeService(equipes),
|
|
new FakeCollaborateurService(collaborateurs),
|
|
_navigation,
|
|
new FakeSessionService(parametresMetier ?? [MobileTypeRdvParam()]),
|
|
parametreMetierService ?? new StubParametreMetierService(),
|
|
new RendezVousStateNotifier());
|
|
|
|
if (date is not null)
|
|
{
|
|
viewModel.SelectedDateOffset = date;
|
|
}
|
|
|
|
return viewModel;
|
|
}
|
|
|
|
private static CollaborateurListItem Membre(Guid id, string nom)
|
|
=> new() { CollaborateurGuid = id, DisplayName = nom };
|
|
|
|
private static AgendaItem Item(Guid id, DateTime debut, params Guid[] collaborateurs)
|
|
=> new()
|
|
{
|
|
RendezVousGuid = id,
|
|
Titre = "RDV",
|
|
Debut = debut,
|
|
Fin = debut.AddHours(1),
|
|
TypeActivite = "Intervention",
|
|
TypeActiviteKey = "T.RDV.INTER",
|
|
CollaborateurGuids = collaborateurs
|
|
};
|
|
|
|
private static ParametreMetierDto MobileTypeRdvParam()
|
|
=> new()
|
|
{
|
|
Id = "T.RDV.INTER",
|
|
Code = "14",
|
|
Categorie = "T.RDV",
|
|
Libelle = "Intervention",
|
|
Parametre = """{"Envoi_mobile":1}"""
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Initialise_ConstruitLesDeuxFiltres()
|
|
{
|
|
// Arrange
|
|
Guid alice = Guid.NewGuid();
|
|
Guid bob = Guid.NewGuid();
|
|
Guid solo = Guid.NewGuid();
|
|
|
|
var equipe = new EquipeListItem
|
|
{
|
|
EquipeGuid = Guid.NewGuid(),
|
|
Nom = "Équipe A",
|
|
Membres = [Membre(alice, "Alice"), Membre(bob, "Bob")]
|
|
};
|
|
|
|
// Act
|
|
AgendaEquipeViewModel vm = CreateViewModel([equipe], [Membre(solo, "Solo")], []);
|
|
await vm.InitializeAsync();
|
|
|
|
// Assert : deux listes distinctes, aucune sélection par défaut.
|
|
Assert.Single(vm.Equipes);
|
|
Assert.Equal("👥 Équipe A", vm.Equipes[0].Label);
|
|
Assert.Equal(
|
|
new[] { alice, bob }.OrderBy(g => g).ToArray(),
|
|
vm.Equipes[0].CollaborateurGuids.OrderBy(g => g).ToArray());
|
|
Assert.Single(vm.Collaborateurs);
|
|
Assert.Equal("👤 Solo", vm.Collaborateurs[0].Label);
|
|
Assert.Null(vm.SelectedEquipe);
|
|
Assert.Null(vm.SelectedCollaborateur);
|
|
Assert.False(vm.HasSelection);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FiltreParEquipe_ChargeLesRendezVousDeLEquipe()
|
|
{
|
|
// Arrange
|
|
Guid alice = Guid.NewGuid();
|
|
Guid autre = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12);
|
|
|
|
var equipe = new EquipeListItem
|
|
{
|
|
EquipeGuid = Guid.NewGuid(),
|
|
Nom = "Équipe A",
|
|
Membres = [Membre(alice, "Alice")]
|
|
};
|
|
|
|
var items = new[]
|
|
{
|
|
Item(Guid.NewGuid(), jour.AddHours(9), alice),
|
|
Item(Guid.NewGuid(), jour.AddHours(11), autre)
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([equipe], [], items, jour);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act : sélection de l'équipe.
|
|
vm.SelectedEquipe = vm.Equipes[0];
|
|
|
|
// Assert : seul le rendez-vous de la membre de l'équipe est chargé.
|
|
Assert.True(vm.HasAppointments);
|
|
Assert.Single(vm.DayGroups);
|
|
AgendaItem item = Assert.Single(vm.DayGroups[0].Items);
|
|
Assert.Equal(alice, item.CollaborateurGuids.Single());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FiltreParCollaborateur_ChargeSesRendezVous()
|
|
{
|
|
// Arrange
|
|
Guid solo = Guid.NewGuid();
|
|
Guid autre = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12);
|
|
|
|
var items = new[]
|
|
{
|
|
Item(Guid.NewGuid(), jour.AddHours(9), solo),
|
|
Item(Guid.NewGuid(), jour.AddHours(11), autre)
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [Membre(solo, "Solo")], items, jour);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act : sélection du collaborateur.
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Assert
|
|
Assert.True(vm.HasAppointments);
|
|
Assert.Single(vm.DayGroups);
|
|
AgendaItem item = Assert.Single(vm.DayGroups[0].Items);
|
|
Assert.Equal(solo, item.CollaborateurGuids.Single());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectionEquipe_EffaceLeCollaborateur()
|
|
{
|
|
// Arrange
|
|
Guid alice = Guid.NewGuid();
|
|
Guid solo = Guid.NewGuid();
|
|
var equipe = new EquipeListItem
|
|
{
|
|
EquipeGuid = Guid.NewGuid(),
|
|
Nom = "Équipe A",
|
|
Membres = [Membre(alice, "Alice")]
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([equipe], [Membre(solo, "Solo")], []);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act : collaborateur puis équipe.
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
vm.SelectedEquipe = vm.Equipes[0];
|
|
|
|
// Assert : l'équipe l'emporte, le collaborateur est effacé.
|
|
Assert.Equal(vm.Equipes[0], vm.SelectedEquipe);
|
|
Assert.Null(vm.SelectedCollaborateur);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectionCollaborateur_EffaceLEquipe()
|
|
{
|
|
// Arrange
|
|
Guid alice = Guid.NewGuid();
|
|
Guid solo = Guid.NewGuid();
|
|
var equipe = new EquipeListItem
|
|
{
|
|
EquipeGuid = Guid.NewGuid(),
|
|
Nom = "Équipe A",
|
|
Membres = [Membre(alice, "Alice")]
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([equipe], [Membre(solo, "Solo")], []);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act : équipe puis collaborateur.
|
|
vm.SelectedEquipe = vm.Equipes[0];
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Assert : le collaborateur l'emporte, l'équipe est effacée.
|
|
Assert.Equal(vm.Collaborateurs[0], vm.SelectedCollaborateur);
|
|
Assert.Null(vm.SelectedEquipe);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EquipeSelectionnee_ExposeSesMembres()
|
|
{
|
|
// Arrange
|
|
Guid alice = Guid.NewGuid();
|
|
Guid bob = Guid.NewGuid();
|
|
var equipe = new EquipeListItem
|
|
{
|
|
EquipeGuid = Guid.NewGuid(),
|
|
Nom = "Équipe A",
|
|
Membres = [Membre(alice, "Alice"), Membre(bob, "Bob")]
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([equipe], [], []);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act
|
|
vm.SelectedEquipe = vm.Equipes[0];
|
|
|
|
// Assert : les noms des membres sont exposés pour l'affichage.
|
|
Assert.True(vm.HasSelectedEquipeMembres);
|
|
Assert.Equal(["Alice", "Bob"], vm.SelectedEquipeMembres);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EquipeAvecSociete_LibelleInclutLaSociete()
|
|
{
|
|
// Arrange
|
|
var equipe = new EquipeListItem
|
|
{
|
|
EquipeGuid = Guid.NewGuid(),
|
|
Nom = "Pose",
|
|
SocieteNom = "Groupe GAP",
|
|
Membres = [Membre(Guid.NewGuid(), "Alice")]
|
|
};
|
|
|
|
// Act
|
|
AgendaEquipeViewModel vm = CreateViewModel([equipe], [], []);
|
|
await vm.InitializeAsync();
|
|
|
|
// Assert
|
|
AgendaScopeOption option = Assert.Single(vm.Equipes);
|
|
Assert.Equal("👥 Pose (Groupe GAP)", option.Label);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Groupement_ParDate()
|
|
{
|
|
// Arrange : deux jours distincts de la même semaine, même collaborateur.
|
|
Guid solo = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12); // mercredi
|
|
|
|
var items = new[]
|
|
{
|
|
Item(Guid.NewGuid(), jour.AddHours(9), solo),
|
|
Item(Guid.NewGuid(), jour.AddDays(1).AddHours(9), solo)
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [Membre(solo, "Solo")], items, jour);
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Assert : deux groupes (un par jour), ordonnés chronologiquement.
|
|
Assert.Equal(2, vm.DayGroups.Count);
|
|
Assert.Equal(jour, vm.DayGroups[0].Date);
|
|
Assert.Equal(jour.AddDays(1), vm.DayGroups[1].Date);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SansSelection_EtatVide()
|
|
{
|
|
// Act
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [], [], new DateTime(2026, 8, 12));
|
|
await vm.InitializeAsync();
|
|
|
|
// Assert
|
|
Assert.False(vm.HasAppointments);
|
|
Assert.True(vm.IsEmpty);
|
|
Assert.False(vm.HasSelection);
|
|
Assert.Equal("Aucun périmètre sélectionné", vm.EmptyStateTitle);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AvecSelectionSansRdv_EtatVide()
|
|
{
|
|
// Arrange
|
|
Guid solo = Guid.NewGuid();
|
|
|
|
// Act
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [Membre(solo, "Solo")], [], new DateTime(2026, 8, 12));
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Assert
|
|
Assert.True(vm.IsEmpty);
|
|
Assert.True(vm.HasSelection);
|
|
Assert.Equal("Aucun rendez-vous", vm.EmptyStateTitle);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PreviousNextWeek_ChangeLaSemaine()
|
|
{
|
|
// Arrange
|
|
DateTime jour = new(2026, 8, 12);
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [Membre(Guid.NewGuid(), "Solo")], [], jour);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act
|
|
await vm.NextWeekCommand.ExecuteAsync(null);
|
|
|
|
// Assert : la date a avancé de 7 jours.
|
|
Assert.Equal(jour.AddDays(7), vm.SelectedDate);
|
|
|
|
// Act
|
|
await vm.PreviousWeekCommand.ExecuteAsync(null);
|
|
|
|
// Assert : retour à la semaine initiale.
|
|
Assert.Equal(jour, vm.SelectedDate);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenRendezVous_NavigueVersDetail()
|
|
{
|
|
// Arrange
|
|
Guid rdvId = Guid.NewGuid();
|
|
Guid solo = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12);
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel(
|
|
[],
|
|
[Membre(solo, "Solo")],
|
|
[Item(rdvId, jour.AddHours(9), solo)],
|
|
jour);
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Act
|
|
AgendaItem item = vm.DayGroups[0].Items[0];
|
|
vm.OpenRendezVousCommand.Execute(item);
|
|
|
|
// Assert : la fiche reçoit l'élément complet (déjà enrichi) pour ne pas
|
|
// recharger par GUID (hors snapshot local).
|
|
Assert.Equal(AppPage.RendezVousDetail, _navigation.CurrentPage);
|
|
Assert.Same(item, _navigation.CurrentParameter);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenRendezVous_NonEnvoyeMobile_BloqueNavigation()
|
|
{
|
|
// Arrange : un rendez-vous de type « CP » (non « Envoi_mobile ») de l'équipe.
|
|
Guid rdvId = Guid.NewGuid();
|
|
Guid solo = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12);
|
|
|
|
var item = new AgendaItem
|
|
{
|
|
RendezVousGuid = rdvId,
|
|
Titre = "CP",
|
|
Debut = jour.AddHours(9),
|
|
Fin = jour.AddHours(10),
|
|
TypeActivite = "CP",
|
|
TypeActiviteKey = "CP",
|
|
CollaborateurGuids = [solo]
|
|
};
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel(
|
|
[],
|
|
[Membre(solo, "Solo")],
|
|
[item],
|
|
jour);
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Act
|
|
AgendaItem loaded = vm.DayGroups[0].Items[0];
|
|
Assert.False(loaded.EstEnvoyeMobile);
|
|
vm.OpenRendezVousCommand.Execute(loaded);
|
|
|
|
// Assert : aucune navigation vers le détail (le RDV reste consultable uniquement).
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Back_SansHistorique_RetourneAccueil()
|
|
{
|
|
// Act
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [], []);
|
|
await vm.InitializeAsync();
|
|
vm.BackCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.Equal(AppPage.Home, _navigation.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ShowCalendar_PasseEnModeCalendrier()
|
|
{
|
|
// Arrange
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [Membre(Guid.NewGuid(), "Solo")], []);
|
|
await vm.InitializeAsync();
|
|
|
|
// Act
|
|
vm.ShowCalendarCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.True(vm.IsCalendarMode);
|
|
Assert.False(vm.IsListMode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MonthDays_Contient42Jours_EnModeCalendrier()
|
|
{
|
|
// Arrange
|
|
Guid solo = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12);
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel(
|
|
[],
|
|
[Membre(solo, "Solo")],
|
|
[Item(Guid.NewGuid(), jour.AddHours(9), solo)],
|
|
jour);
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Act
|
|
vm.ShowCalendarCommand.Execute(null);
|
|
|
|
// Assert
|
|
Assert.True(vm.IsCalendarMode);
|
|
Assert.Equal(42, vm.MonthDays.Count);
|
|
Assert.Equal(new DateTime(2026, 7, 27), vm.MonthDays[0].Date);
|
|
AgendaMonthDayItem cell = vm.MonthDays.Single(d => d.Date == jour);
|
|
Assert.True(cell.HasAppointments);
|
|
Assert.Single(cell.Appointments);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelectCalendarDay_OuvreLaVueJour()
|
|
{
|
|
// Arrange
|
|
Guid solo = Guid.NewGuid();
|
|
DateTime jour = new(2026, 8, 12);
|
|
|
|
AgendaEquipeViewModel vm = CreateViewModel([], [Membre(solo, "Solo")], [], jour);
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
vm.ShowCalendarCommand.Execute(null);
|
|
|
|
AgendaMonthDayItem target = vm.MonthDays.Single(d => d.Date == new DateTime(2026, 8, 13));
|
|
|
|
// Act
|
|
vm.SelectCalendarDayCommand.Execute(target);
|
|
|
|
// Assert
|
|
Assert.False(vm.IsCalendarMode);
|
|
Assert.True(vm.IsDayMode);
|
|
Assert.Equal(new DateTime(2026, 8, 13), vm.SelectedDate);
|
|
}
|
|
|
|
private sealed class FakeAgendaService : IAgendaService
|
|
{
|
|
private readonly IReadOnlyList<AgendaItem> _items;
|
|
|
|
public FakeAgendaService(IReadOnlyList<AgendaItem> items)
|
|
{
|
|
_items = items;
|
|
}
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForCollaborateursAsync(
|
|
IReadOnlyList<Guid> collaborateurGuids,
|
|
DateTime startInclusive,
|
|
DateTime endExclusive,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var set = collaborateurGuids.ToHashSet();
|
|
return Task.FromResult<IReadOnlyList<AgendaItem>>(
|
|
_items.Where(i => i.CollaborateurGuids.Any(g => set.Contains(g))).ToList());
|
|
}
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForCollaborateurAsync(Guid collaborateurGuid, DateTime startInclusive, DateTime endExclusive, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<AgendaItem>>([]);
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForDayAsync(DateTime date, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForRangeAsync(DateTime startInclusive, DateTime endExclusive, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<AgendaItem?> GetRendezVousByIdAsync(Guid rendezVousGuid, CancellationToken cancellationToken = default, string? affectationId = null)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task EnrichSiteAccessAsync(AgendaItem item, CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
private sealed class FakeEquipeService : IEquipeLocalService
|
|
{
|
|
private readonly IReadOnlyList<EquipeListItem> _equipes;
|
|
|
|
public FakeEquipeService(IReadOnlyList<EquipeListItem> equipes)
|
|
{
|
|
_equipes = equipes;
|
|
}
|
|
|
|
public Task<IReadOnlyList<EquipeListItem>> GetEquipesAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(_equipes);
|
|
|
|
public Task<IReadOnlyList<EquipeListItem>> GetAllEquipesAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(_equipes);
|
|
}
|
|
|
|
private sealed class FakeCollaborateurService : ICollaborateurLocalService
|
|
{
|
|
private readonly IReadOnlyList<CollaborateurListItem> _collaborateurs;
|
|
|
|
public FakeCollaborateurService(IReadOnlyList<CollaborateurListItem> collaborateurs)
|
|
{
|
|
_collaborateurs = collaborateurs;
|
|
}
|
|
|
|
public Task<IReadOnlyList<CollaborateurListItem>> GetCollaborateursAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(_collaborateurs);
|
|
|
|
public Task<CollaborateurDetail?> GetCollaborateurByIdAsync(Guid collaborateurGuid, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<CollaborateurDetail?>(null);
|
|
}
|
|
|
|
private sealed class FakeSessionService : IMobileSessionService
|
|
{
|
|
private readonly IReadOnlyList<ParametreMetierDto> _parametresMetier;
|
|
|
|
public FakeSessionService(IReadOnlyList<ParametreMetierDto> parametresMetier)
|
|
=> _parametresMetier = parametresMetier;
|
|
|
|
public Task<LocalBootstrapState?> GetBootstrapAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<LocalBootstrapState?>(new LocalBootstrapState { ParametresMetier = _parametresMetier });
|
|
|
|
public Task<bool> HasValidSessionAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<bool> HasExpiredSessionAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(false);
|
|
|
|
public Task<LicenseResponse> ActivateAsync(string codeClient, string numeroLicence, CancellationToken cancellationToken = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<LicenseResponse> ValidateAsync(CancellationToken cancellationToken = default)
|
|
=> throw new NotImplementedException();
|
|
|
|
public Task<string?> GetAccessTokenAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<string?>(null);
|
|
|
|
public Task ClearSessionAsync(CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task EnsureDevelopmentSessionAsync(CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Load_AnnulationSousIOException_TokenAnnule_MasqueLErreur()
|
|
{
|
|
Guid solo = Guid.NewGuid();
|
|
var agenda = new FakeThrowingAgendaService();
|
|
var vm = new AgendaEquipeViewModel(
|
|
agenda,
|
|
new FakeEquipeService([]),
|
|
new FakeCollaborateurService([Membre(solo, "Solo")]),
|
|
_navigation,
|
|
new FakeSessionService([MobileTypeRdvParam()]),
|
|
new StubParametreMetierService(),
|
|
new RendezVousStateNotifier());
|
|
|
|
// Sélection d'un collaborateur sans erreur (le service ne lève pas encore).
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
// Puis annulation remontée sous IOException pendant un refresh annulé.
|
|
agenda.ExceptionToThrow = new IOException("Canceled");
|
|
using var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
await vm.RefreshAsync(cts.Token);
|
|
|
|
Assert.False(vm.IsLoading);
|
|
Assert.Null(vm.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Une indisponibilité réseau n'est pas une erreur utilisateur : l'agenda d'équipe
|
|
/// signale l'état hors ligne (bandeau) sans message bloquant et sans état vide
|
|
/// trompeur — même langage que l'agenda personnel.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_IOExceptionReelle_TokenNonAnnule_SignaleHorsLigne()
|
|
{
|
|
Guid solo = Guid.NewGuid();
|
|
var agenda = new FakeThrowingAgendaService { ExceptionToThrow = new IOException("Network unreachable") };
|
|
var vm = new AgendaEquipeViewModel(
|
|
agenda,
|
|
new FakeEquipeService([]),
|
|
new FakeCollaborateurService([Membre(solo, "Solo")]),
|
|
_navigation,
|
|
new FakeSessionService([MobileTypeRdvParam()]),
|
|
new StubParametreMetierService(),
|
|
new RendezVousStateNotifier());
|
|
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
await vm.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.True(vm.IsOffline);
|
|
Assert.Null(vm.ErrorMessage);
|
|
// Hors ligne ≠ planning vide : ne pas afficher « aucun rendez-vous ».
|
|
Assert.False(vm.IsEmpty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contre-épreuve : une erreur NON réseau reste un incident affiché, avec son
|
|
/// message (elle n'est pas traduite et n'active pas le bandeau hors ligne).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_ErreurMetier_ConserveLeMessageEtLErreur()
|
|
{
|
|
Guid solo = Guid.NewGuid();
|
|
var agenda = new FakeThrowingAgendaService { ExceptionToThrow = new InvalidOperationException("panne métier") };
|
|
var vm = new AgendaEquipeViewModel(
|
|
agenda,
|
|
new FakeEquipeService([]),
|
|
new FakeCollaborateurService([Membre(solo, "Solo")]),
|
|
_navigation,
|
|
new FakeSessionService([MobileTypeRdvParam()]),
|
|
new StubParametreMetierService(),
|
|
new RendezVousStateNotifier());
|
|
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
await vm.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.Equal("panne métier", vm.ErrorMessage);
|
|
Assert.False(vm.IsOffline);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aucune URL d'API configurée (appareil non inscrit) : état actionnable, pas
|
|
/// « hors ligne » — l'appareil peut être en ligne et l'utilisateur doit savoir
|
|
/// qu'il lui reste à rescanner le QR.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Load_SansUrlDApi_SignaleLInscriptionIncompleteSansBandeauHorsLigne()
|
|
{
|
|
Guid solo = Guid.NewGuid();
|
|
var agenda = new FakeThrowingAgendaService
|
|
{
|
|
ExceptionToThrow = new MobileEnrollmentRequiredException()
|
|
};
|
|
var vm = new AgendaEquipeViewModel(
|
|
agenda,
|
|
new FakeEquipeService([]),
|
|
new FakeCollaborateurService([Membre(solo, "Solo")]),
|
|
_navigation,
|
|
new FakeSessionService([MobileTypeRdvParam()]),
|
|
new StubParametreMetierService(),
|
|
new RendezVousStateNotifier());
|
|
|
|
await vm.InitializeAsync();
|
|
vm.SelectedCollaborateur = vm.Collaborateurs[0];
|
|
|
|
await vm.RefreshAsync(CancellationToken.None);
|
|
|
|
Assert.False(vm.IsOffline);
|
|
Assert.Equal(
|
|
"Inscription incomplète — rescannez le QR code pour reconnecter l'application.",
|
|
vm.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>Service d'agenda jetable dont la lecture d'équipe peut lever une exception configurée.</summary>
|
|
private sealed class FakeThrowingAgendaService : IAgendaService
|
|
{
|
|
public Exception? ExceptionToThrow { get; set; }
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForCollaborateursAsync(
|
|
IReadOnlyList<Guid> collaborateurGuids, DateTime startInclusive, DateTime endExclusive, CancellationToken cancellationToken = default)
|
|
{
|
|
if (ExceptionToThrow is { } ex)
|
|
{
|
|
throw ex;
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<AgendaItem>>([]);
|
|
}
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForCollaborateurAsync(Guid collaborateurGuid, DateTime startInclusive, DateTime endExclusive, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<AgendaItem>>([]);
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForDayAsync(DateTime date, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<IReadOnlyList<AgendaItem>> GetRendezVousForRangeAsync(DateTime startInclusive, DateTime endExclusive, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<AgendaItem?> GetRendezVousByIdAsync(Guid rendezVousGuid, CancellationToken cancellationToken = default, string? affectationId = null)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task EnrichSiteAccessAsync(AgendaItem item, CancellationToken cancellationToken = default)
|
|
=> Task.CompletedTask;
|
|
}
|
|
}
|