using System.Text.Json; using Omika.App.Local.Models; using Omika.App.Local.Infrastructure; using Omika.App.Local.Services; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Interventions; using Omika.Core.Contracts.Organisation; using Omika.Core.Contracts.Planning; namespace Omika.Tests.Mobile.Local; /// /// Tests du service de lecture des collaborateurs depuis les données locales /// SQLite. Relations réellement démontrables localement : /// /// Collaborateur → RendezVous : RendezVousDetailsDto.Collaborateurs /// contient une affectation dont CollaborateurId correspond au GUID public. /// Collaborateur → Interventions : InterventionDetailsDto.CollaborateurIds /// contient le GUID public du collaborateur. /// /// Aucune relation n'est déduite par transitivité ni par commodité. Lecture /// seule : aucune écriture ni Outbox n'est créée. /// public class CollaborateurLocalServiceTests : LocalTestBase { private readonly CollaborateurLocalService _service; public CollaborateurLocalServiceTests() { _service = new CollaborateurLocalService(CollaborateurRepository, RendezVousRepository, ContextInterventionCacheRepository, ApiClient, SessionService); } private static string Serialize(T value) => JsonSerializer.Serialize(value); private static LocalCollaborateur CreateCollaborateur( Guid id, string? nom = null, string? prenom = null, string? nomAffiche = null, string? code = null, string? statut = null, string? telephone = null, string? mobile = null, string? email = null, bool actif = true, bool estArchive = false) { var dto = new CollaborateurDetailsDto { Id = id.ToString(), Nom = nom, Prenom = prenom, NomAffiche = nomAffiche, Code = code, Statut = statut is null ? null : new ReferenceValueDto { Label = statut }, Coordonnees = telephone is null && mobile is null && email is null ? null : new ContactDetailsDto { Phone = telephone, Mobile = mobile, Email = email }, Actif = actif, EstArchive = estArchive }; return new LocalCollaborateur { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalRendezVous CreateRendezVous( Guid id, Guid collaborateurId, DateTime debut, DateTime fin, string titre = "Rendez-vous", string? statut = "Planifié") { var dto = new RendezVousDetailsDto { Id = id.ToString(), Titre = titre, Debut = debut, Fin = fin, Statut = statut is null ? null : new ReferenceValueDto { Label = statut }, Collaborateurs = [ new AffectationCollaborateurDto { CollaborateurId = collaborateurId.ToString() } ] }; return new LocalRendezVous { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalRendezVous CreateRendezVousSansCollaborateur( Guid id, DateTime debut, DateTime fin, string titre = "Rendez-vous") { var dto = new RendezVousDetailsDto { Id = id.ToString(), Titre = titre, Debut = debut, Fin = fin, Collaborateurs = [] }; return new LocalRendezVous { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalIntervention CreateIntervention( Guid id, Guid collaborateurId, string reference, string titre, DateTime? datePrevueDebut = null, string? etat = null) { var dto = new InterventionDetailsDto { Id = id.ToString(), Reference = reference, Titre = titre, DatePrevueDebut = datePrevueDebut, Etat = etat is null ? null : new ReferenceValueDto { Label = etat }, CollaborateurIds = [collaborateurId.ToString()] }; return new LocalIntervention { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalIntervention CreateInterventionSansCollaborateur( Guid id, string reference, string titre) { var dto = new InterventionDetailsDto { Id = id.ToString(), Reference = reference, Titre = titre, CollaborateurIds = [] }; return new LocalIntervention { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private async Task CacheCollaborateurInterventionsAsync(Guid collaborateurId, params LocalIntervention[] interventions) { var details = interventions .Select(i => JsonSerializer.Deserialize(i.PayloadJson)!) .ToList(); await ContextInterventionCacheRepository.ReplaceAsync( InterventionContextTypes.Collaborateur, collaborateurId, details); } // ===================== LISTE ===================== [Fact] public async Task AucunCollaborateur_RetourneListeVide() { // Act var result = await _service.GetCollaborateursAsync(); // Assert Assert.Empty(result); } [Fact] public async Task UnCollaborateur_RetourneLeCollaborateur() { // Arrange Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean", code: "C001")); // Act var result = await _service.GetCollaborateursAsync(); // Assert var item = Assert.Single(result); Assert.Equal(collaborateurId, item.CollaborateurGuid); Assert.Equal("Jean Dupont", item.DisplayName); Assert.Equal("C001", item.Code); } [Fact] public async Task PlusieursCollaborateurs_RetourneTous() { // Arrange await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean")); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Martin", "Paul")); // Act var result = await _service.GetCollaborateursAsync(); // Assert Assert.Equal(2, result.Count); } [Fact] public async Task Tri_DeterministeParNomAffichePuisCode() { // Arrange await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Beta", "B", code: "C002")); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Alpha", "A", code: "C001")); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Alpha", "A", code: "C003")); // Act var result = await _service.GetCollaborateursAsync(); // Assert : tri stable par nom affiché (insensible à la casse) puis code. Assert.Equal("A Alpha", result[0].DisplayName); Assert.Equal("C001", result[0].Code); Assert.Equal("A Alpha", result[1].DisplayName); Assert.Equal("C003", result[1].Code); Assert.Equal("B Beta", result[2].DisplayName); } [Fact] public async Task RechercheLocale_InsensibleALaCasse() { // Arrange await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean", code: "C001")); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Martin", "Paul", code: "C002")); // Act : le service retourne tout ; la recherche est gérée par le ViewModel. var result = await _service.GetCollaborateursAsync(); // Assert Assert.Equal(2, result.Count); } [Fact] public async Task RechercheSansResultat_ListeVide() { // Arrange await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean")); // Act : le service retourne tout ; la recherche est gérée par le ViewModel. var result = await _service.GetCollaborateursAsync(); // Assert Assert.Single(result); } [Fact] public async Task PayloadInvalide_EntreeIgnoree() { // Arrange await CollaborateurRepository.UpsertAsync(new LocalCollaborateur { Id = Guid.NewGuid(), PayloadJson = "not-json", UpdatedAtUtc = DateTimeOffset.UtcNow }); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean")); // Act var result = await _service.GetCollaborateursAsync(); // Assert : l'entrée invalide est ignorée proprement. var item = Assert.Single(result); Assert.Equal("Jean Dupont", item.DisplayName); } [Fact] public async Task IdentitePublique_Conservee() { // Arrange Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean")); // Act var result = await _service.GetCollaborateursAsync(); // Assert : l'identité publique (GUID) est conservée. var item = Assert.Single(result); Assert.Equal(collaborateurId, item.CollaborateurGuid); } [Fact] public async Task FonctionneOffline_SansAppelApi() { // Arrange : aucune dépendance réseau, uniquement SQLite local. await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean")); // Act var result = await _service.GetCollaborateursAsync(); // Assert Assert.Single(result); } // ===================== DÉTAIL ===================== [Fact] public async Task Detail_CollaborateurTrouve() { // Arrange Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean", code: "C001")); // Act var result = await _service.GetCollaborateurByIdAsync(collaborateurId); // Assert Assert.NotNull(result); Assert.Equal(collaborateurId, result.CollaborateurGuid); Assert.Equal("Jean Dupont", result.NomAffiche); Assert.Equal("C001", result.Code); } [Fact] public async Task Detail_CollaborateurInconnu_RetourneNull() { // Act var result = await _service.GetCollaborateurByIdAsync(Guid.NewGuid()); // Assert Assert.Null(result); } [Fact] public async Task Detail_DonneesReellesChargees() { // Arrange Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur( collaborateurId, "Dupont", "Jean", code: "C001", statut: "Actif", telephone: "0102030405", mobile: "0607080910", email: "jean@omika.fr")); // Act var result = await _service.GetCollaborateurByIdAsync(collaborateurId); // Assert Assert.NotNull(result); Assert.Equal("Dupont", result.Nom); Assert.Equal("Jean", result.Prenom); Assert.Equal("C001", result.Code); Assert.Equal("Actif", result.Statut); Assert.Equal("0102030405", result.Phone); Assert.Equal("0607080910", result.Mobile); Assert.Equal("jean@omika.fr", result.Email); Assert.True(result.HasPhone); Assert.True(result.HasEmail); } [Fact] public async Task Detail_ChampsOptionnelsAbsents() { // Arrange : collaborateur minimal sans coordonnées ni statut. Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean")); // Act var result = await _service.GetCollaborateurByIdAsync(collaborateurId); // Assert : champs optionnels absents restent nuls sans erreur. Assert.NotNull(result); Assert.Null(result.Phone); Assert.Null(result.Mobile); Assert.Null(result.Email); Assert.Null(result.Statut); Assert.False(result.HasPhone); Assert.False(result.HasEmail); } [Fact] public async Task Detail_IdentitePublique_Conservee() { // Arrange Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean")); // Act var result = await _service.GetCollaborateurByIdAsync(collaborateurId); // Assert Assert.NotNull(result); Assert.Equal(collaborateurId, result.CollaborateurGuid); } [Fact] public async Task Detail_AucuneRelation_NeProvoquePasDErreur() { // Arrange : collaborateur sans rendez-vous ni interventions. Guid collaborateurId = Guid.NewGuid(); await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean")); // Act var result = await _service.GetCollaborateurByIdAsync(collaborateurId); // Assert : collections vides sans erreur. Assert.NotNull(result); Assert.Empty(result.RendezVous); Assert.Empty(result.Interventions); } }