using System.Text.Json; using Omika.App.Local.Models; using Omika.App.Local.Infrastructure; using Omika.App.Local.Services; using Omika.Core.Contracts.Clients; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Interventions; namespace Omika.Tests.Mobile.Local; /// /// Tests du service de lecture des clients depuis les données locales SQLite. /// Relations réellement démontrables localement : /// /// Client → Sites : liste Sites embarquée dans le DTO Client. /// Client → Contacts : liste Contacts embarquée dans le DTO Client. /// Client → Dossiers : DossierDetailsDto.ClientId == ClientGuid. /// Client → Interventions : InterventionDetailsDto.ClientId == ClientGuid. /// /// Aucune relation n'est déduite par commodité. Lecture seule : aucune écriture /// ni Outbox n'est créée. /// public class ClientLocalServiceTests : LocalTestBase { private readonly ClientLocalService _service; public ClientLocalServiceTests() { _service = new ClientLocalService(ClientRepository, DossierRepository, ContextInterventionCacheRepository, ClientStateRepository, ApiClient); } private static string Serialize(T value) => JsonSerializer.Serialize(value); private static LocalClient CreateClient( Guid id, string nom, string? code = null, string? ville = null, string? statut = null, string? telephone = null, string? email = null, IReadOnlyList? sites = null, IReadOnlyList? contacts = null) { var dto = new ClientDetailsDto { Id = id.ToString(), Nom = nom, Code = code, Statut = statut is null ? null : new ReferenceValueDto { Label = statut }, Coordonnees = telephone is null && email is null ? null : new ContactDetailsDto { Phone = telephone, Email = email }, AdressePrincipale = ville is null ? null : new AdresseDto { AdressePostale = new AddressDto { City = ville } }, Sites = sites ?? [], Contacts = contacts ?? [] }; return new LocalClient { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalDossier CreateDossier( Guid id, Guid clientId, string reference, string titre, DateTime? dateOuverture = null, string? statut = null) { var dto = new DossierDetailsDto { Id = id.ToString(), RefInterne = reference, Titre = titre, ClientId = clientId.ToString(), DateOuverture = dateOuverture, Statut = statut is null ? null : new ReferenceValueDto { Label = statut } }; return new LocalDossier { Id = id, ClientId = clientId, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalIntervention CreateIntervention( Guid id, Guid clientId, string reference, string titre, DateTime? datePrevueDebut = null, string? etat = null) { var dto = new InterventionDetailsDto { Id = id.ToString(), Reference = reference, Titre = titre, ClientId = clientId.ToString(), DatePrevueDebut = datePrevueDebut, Etat = etat is null ? null : new ReferenceValueDto { Label = etat } }; return new LocalIntervention { Id = id, ClientId = clientId, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private async Task CacheClientInterventionsAsync(Guid clientId, params LocalIntervention[] interventions) { var details = interventions .Select(i => JsonSerializer.Deserialize(i.PayloadJson)!) .ToList(); await ContextInterventionCacheRepository.ReplaceAsync(InterventionContextTypes.Client, clientId, details); } // ===================== LISTE ===================== [Fact] public async Task AucunClient_RetourneListeVide() { // Act var result = await _service.GetClientsAsync(); // Assert Assert.Empty(result); } [Fact] public async Task UnClient_RetourneLeClient() { // Arrange Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha", "C001")); // Act var result = await _service.GetClientsAsync(); // Assert var item = Assert.Single(result); Assert.Equal(clientId, item.ClientGuid); Assert.Equal("Client Alpha", item.Nom); Assert.Equal("C001", item.Code); } [Fact] public async Task PlusieursClients_RetourneTous() { // Arrange await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha")); await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Beta")); // Act var result = await _service.GetClientsAsync(); // Assert Assert.Equal(2, result.Count); } [Fact] public async Task Tri_DeterministeParNomPuisCode() { // Arrange await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Beta", "C002")); await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha", "C001")); await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha", "C003")); // Act var result = await _service.GetClientsAsync(); // Assert : tri stable par nom (insensible à la casse) puis code. Assert.Equal("Alpha", result[0].Nom); Assert.Equal("C001", result[0].Code); Assert.Equal("Alpha", result[1].Nom); Assert.Equal("C003", result[1].Code); Assert.Equal("Beta", result[2].Nom); } [Fact] public async Task RechercheLocale_InsensibleALaCasse() { // Arrange await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha", "C001", "Paris")); await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Beta", "C002", "Lyon")); // Act : le service retourne tout ; la recherche est gérée par le ViewModel. var result = await _service.GetClientsAsync(); // Assert Assert.Equal(2, result.Count); } [Fact] public async Task RechercheSansResultat_ListeVide() { // Arrange await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha")); // Act : le service retourne tout ; la recherche est gérée par le ViewModel. var result = await _service.GetClientsAsync(); // Assert Assert.Single(result); } [Fact] public async Task PayloadInvalide_EntreeIgnoree() { // Arrange await ClientRepository.UpsertAsync(new LocalClient { Id = Guid.NewGuid(), PayloadJson = "not-json", UpdatedAtUtc = DateTimeOffset.UtcNow }); await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Valide")); // Act var result = await _service.GetClientsAsync(); // Assert : l'entrée invalide est ignorée proprement. var item = Assert.Single(result); Assert.Equal("Valide", item.Nom); } [Fact] public async Task IdentitePublique_Conservee() { // Arrange Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Alpha")); // Act var result = await _service.GetClientsAsync(); // Assert : l'identité publique (GUID) est conservée. var item = Assert.Single(result); Assert.Equal(clientId, item.ClientGuid); } [Fact] public async Task FonctionneOffline_SansAppelApi() { // Arrange : aucune dépendance réseau, uniquement SQLite local. await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha")); // Act var result = await _service.GetClientsAsync(); // Assert Assert.Single(result); } // ===================== DÉTAIL ===================== [Fact] public async Task Detail_ClientTrouve() { // Arrange Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha", "C001")); // Act var result = await _service.GetClientByIdAsync(clientId); // Assert Assert.NotNull(result); Assert.Equal(clientId, result.ClientGuid); Assert.Equal("Client Alpha", result.Nom); Assert.Equal("C001", result.Code); } [Fact] public async Task Detail_ClientInconnu_RetourneNull() { // Act var result = await _service.GetClientByIdAsync(Guid.NewGuid()); // Assert Assert.Null(result); } [Fact] public async Task Detail_DonneesReellesChargees() { // Arrange Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha", "C001", "Paris", "Actif", "0102030405", "a@b.fr")); // Act var result = await _service.GetClientByIdAsync(clientId); // Assert Assert.NotNull(result); Assert.Equal("Client Alpha", result.Nom); Assert.Equal("C001", result.Code); Assert.Equal("Actif", result.Statut); Assert.Equal("0102030405", result.Phone); Assert.Equal("a@b.fr", result.Email); Assert.True(result.HasPhone); Assert.True(result.HasEmail); Assert.True(result.HasAdresse); } [Fact] public async Task Detail_ChampsOptionnelsAbsents() { // Arrange : client minimal sans coordonnées ni adresse. Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha")); // Act var result = await _service.GetClientByIdAsync(clientId); // Assert : champs optionnels absents restent nuls sans erreur. Assert.NotNull(result); Assert.Null(result.Phone); Assert.Null(result.Email); Assert.Null(result.AdressePrincipale); Assert.False(result.HasPhone); Assert.False(result.HasEmail); Assert.False(result.HasAdresse); } [Fact] public async Task Detail_AucuneRelation_NeProvoquePasDErreur() { // Arrange : client sans sites, contacts, dossiers ni interventions. Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha")); // Act var result = await _service.GetClientByIdAsync(clientId); // Assert : collections vides sans erreur. Assert.NotNull(result); Assert.Empty(result.Sites); Assert.Empty(result.Contacts); Assert.Empty(result.Dossiers); Assert.Empty(result.Interventions); } [Fact] public async Task Detail_IdentitePublique_Conservee() { // Arrange Guid clientId = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientId, "Alpha")); // Act var result = await _service.GetClientByIdAsync(clientId); // Assert Assert.NotNull(result); Assert.Equal(clientId, result.ClientGuid); } // ===================== RELATION CLIENT → CONTACTS ===================== [Fact] public async Task ClientA_RecoitUniquementSesContacts() { // Arrange : Client A embarque 2 contacts, Client B en embarque 1. Guid clientA = Guid.NewGuid(); Guid clientB = Guid.NewGuid(); Guid contactA1 = Guid.NewGuid(); Guid contactA2 = Guid.NewGuid(); Guid contactB = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A", contacts: [ new ContactDto { Id = contactA1.ToString(), NomAffiche = "Contact A1" }, new ContactDto { Id = contactA2.ToString(), NomAffiche = "Contact A2" } ])); await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B", contacts: [ new ContactDto { Id = contactB.ToString(), NomAffiche = "Contact B" } ])); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : A reçoit uniquement ses contacts. Assert.NotNull(resultA); Assert.Equal(2, resultA.Contacts.Count); Assert.Contains(resultA.Contacts, c => c.ContactGuid == contactA1); Assert.Contains(resultA.Contacts, c => c.ContactGuid == contactA2); Assert.DoesNotContain(resultA.Contacts, c => c.ContactGuid == contactB); } [Fact] public async Task ContactDuClientB_Exclu() { // Arrange Guid clientA = Guid.NewGuid(); Guid clientB = Guid.NewGuid(); Guid contactB = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B", contacts: [ new ContactDto { Id = contactB.ToString(), NomAffiche = "Contact B" } ])); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : A n'a aucun contact de B. Assert.NotNull(resultA); Assert.Empty(resultA.Contacts); } [Fact] public async Task ContactSansClient_Exclu() { // Arrange : un contact sans ClientId n'est pas rattaché à un client. Guid clientA = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : aucun contact sans rattachement n'est ajouté. Assert.NotNull(resultA); Assert.Empty(resultA.Contacts); } // ===================== RELATION CLIENT → DOSSIERS ===================== [Fact] public async Task ClientA_RecoitUniquementSesDossiers() { // Arrange Guid clientA = Guid.NewGuid(); Guid clientB = Guid.NewGuid(); Guid dossierA = Guid.NewGuid(); Guid dossierB = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B")); await DossierRepository.UpsertAsync(CreateDossier(dossierA, clientA, "DOS-A", "Dossier A")); await DossierRepository.UpsertAsync(CreateDossier(dossierB, clientB, "DOS-B", "Dossier B")); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : A reçoit uniquement ses dossiers. Assert.NotNull(resultA); var dossier = Assert.Single(resultA.Dossiers); Assert.Equal(dossierA, dossier.DossierGuid); Assert.Equal("DOS-A", dossier.RefInterne); } [Fact] public async Task DossierDuClientB_Exclu() { // Arrange Guid clientA = Guid.NewGuid(); Guid clientB = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B")); await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), clientB, "DOS-B", "Dossier B")); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : A n'a aucun dossier de B. Assert.NotNull(resultA); Assert.Empty(resultA.Dossiers); } // ===================== RELATION CLIENT → INTERVENTIONS ===================== [Fact] public async Task ClientA_RecoitUniquementSesInterventions() { // Arrange Guid clientA = Guid.NewGuid(); Guid clientB = Guid.NewGuid(); Guid interventionA = Guid.NewGuid(); Guid interventionB = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B")); await CacheClientInterventionsAsync( clientA, CreateIntervention(interventionA, clientA, "INT-A", "Intervention A"), CreateIntervention(interventionB, clientB, "INT-B", "Intervention B")); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : A reçoit uniquement ses interventions. Assert.NotNull(resultA); var intervention = Assert.Single(resultA.Interventions); Assert.Equal(interventionA, intervention.InterventionGuid); Assert.Equal("INT-A", intervention.Reference); } [Fact] public async Task InterventionDuClientB_Exclue() { // Arrange Guid clientA = Guid.NewGuid(); Guid clientB = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B")); await CacheClientInterventionsAsync( clientA, CreateIntervention(Guid.NewGuid(), clientB, "INT-B", "Intervention B")); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert : A n'a aucune intervention de B. Assert.NotNull(resultA); Assert.Empty(resultA.Interventions); } // ===================== RELATION CLIENT → SITES ===================== [Fact] public async Task ClientA_RecoitSesSitesEmbarques() { // Arrange : les sites sont embarqués dans le DTO Client. Guid clientA = Guid.NewGuid(); Guid siteA = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A", sites: [ new SiteDto { Id = siteA.ToString(), Nom = "Site A", Code = "SITE-A" } ])); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert Assert.NotNull(resultA); var site = Assert.Single(resultA.Sites); Assert.Equal(siteA, site.SiteGuid); Assert.Equal("Site A", site.Nom); Assert.Equal("SITE-A", site.Code); } [Fact] public async Task ClientSansSites_ListeVide() { // Arrange Guid clientA = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); // Act var resultA = await _service.GetClientByIdAsync(clientA); // Assert Assert.NotNull(resultA); Assert.Empty(resultA.Sites); } }