using System.Text.Json; using Omika.App.Local.Models; 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 Contacts d'une intervention. /// La relation utilisée est la liste explicite ContactIds du DTO /// d'intervention, résolue vers les Personnes/Contacts stockées localement. /// Aucun contact n'est déduit du Client : seule la relation explicite /// Intervention → Contact est utilisée. /// public class InterventionContactsServiceTests : LocalTestBase { private readonly InterventionContactsService _service; public InterventionContactsServiceTests() { _service = new InterventionContactsService( InterventionRepository, PersonneRepository, InterventionCacheRepository, ContextInterventionCacheRepository, ApiClient); } private static string Serialize(T value) => JsonSerializer.Serialize(value); private static LocalIntervention CreateIntervention(Guid id, params Guid[] contactIds) { var dto = new InterventionDetailsDto { Id = id.ToString(), Reference = "INT-001", Titre = "Intervention", ContactIds = contactIds.Select(c => c.ToString()).ToList() }; return new LocalIntervention { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalPersonne CreatePersonne( Guid id, string nom, string prenom, string? email = null, string? phone = null, string? mobile = null, Guid? clientId = null) { var dto = new ContactDto { Id = id.ToString(), Nom = nom, Prenom = prenom, NomAffiche = $"{prenom} {nom}", ClientId = clientId?.ToString(), Coordonnees = new ContactDetailsDto { Email = email, Phone = phone, Mobile = mobile }, Actif = true }; return new LocalPersonne { Id = id, ClientId = clientId, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } [Fact] public async Task InterventionA_RetourneUniquementSesContacts() { // Arrange Guid interventionA = Guid.NewGuid(); Guid interventionB = Guid.NewGuid(); Guid contactA = Guid.NewGuid(); Guid contactB = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(interventionA, contactA)); await InterventionRepository.UpsertAsync(CreateIntervention(interventionB, contactB)); await PersonneRepository.UpsertAsync(CreatePersonne(contactA, "Dupont", "Alice")); await PersonneRepository.UpsertAsync(CreatePersonne(contactB, "Martin", "Bob")); // Act var resultA = await _service.GetContactsForInterventionAsync(interventionA); // Assert : A ne retourne que son contact. var contact = Assert.Single(resultA); Assert.Equal(contactA, contact.ContactGuid); Assert.Equal("Alice Dupont", contact.DisplayName); } [Fact] public async Task InterventionB_NeRetournePasLesContactsDeA() { // Arrange Guid interventionA = Guid.NewGuid(); Guid interventionB = Guid.NewGuid(); Guid contactA = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(interventionA, contactA)); await InterventionRepository.UpsertAsync(CreateIntervention(interventionB)); await PersonneRepository.UpsertAsync(CreatePersonne(contactA, "Dupont", "Alice")); // Act var resultB = await _service.GetContactsForInterventionAsync(interventionB); // Assert : B n'a aucun contact (le contact de A n'est pas inclus). Assert.Empty(resultB); } [Fact] public async Task ContactSansRelation_NonInclus() { // Arrange Guid intervention = Guid.NewGuid(); Guid contactSansRelation = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention)); await PersonneRepository.UpsertAsync(CreatePersonne(contactSansRelation, "Durand", "Claire")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : le contact sans relation n'est pas inclus. Assert.Empty(result); } [Fact] public async Task AucuneAssociationBaseeSurLeClient() { // Arrange Guid clientId = Guid.NewGuid(); Guid intervention = Guid.NewGuid(); Guid contactDuClient = Guid.NewGuid(); // L'intervention n'a AUCUN ContactIds, mais un contact du même client existe. await InterventionRepository.UpsertAsync(CreateIntervention(intervention)); await PersonneRepository.UpsertAsync(CreatePersonne(contactDuClient, "Durand", "Claire", clientId: clientId)); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : aucun contact n'est déduit du Client. Assert.Empty(result); } [Fact] public async Task GuidPublicContact_Conserve() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : l'identité publique du contact est conservée. var item = Assert.Single(result); Assert.Equal(contact, item.ContactGuid); } [Fact] public async Task InterventionAvecUnContact_RetourneLeContact() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice", email: "alice@test.fr", mobile: "0600000000")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert var item = Assert.Single(result); Assert.Equal("Alice Dupont", item.DisplayName); Assert.Equal("alice@test.fr", item.Email); Assert.Equal("0600000000", item.Mobile); Assert.True(item.HasPhone); Assert.True(item.HasEmail); } [Fact] public async Task PlusieursContacts_TriesParNom() { // Arrange Guid intervention = Guid.NewGuid(); Guid zoe = Guid.NewGuid(); Guid alice = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, zoe, alice)); await PersonneRepository.UpsertAsync(CreatePersonne(zoe, "Zoe", "Zoe")); await PersonneRepository.UpsertAsync(CreatePersonne(alice, "Alice", "Alice")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : tri déterministe par nom affiché. Assert.Equal(2, result.Count); Assert.Equal("Alice Alice", result[0].DisplayName); Assert.Equal("Zoe Zoe", result[1].DisplayName); } [Fact] public async Task AucunContact_RetourneListeVide() { // Arrange Guid intervention = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention)); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert Assert.Empty(result); } [Fact] public async Task ContactIncomplet_RetourneNomConstruit() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : nom construit à partir du prénom + nom. var item = Assert.Single(result); Assert.Equal("Alice Dupont", item.DisplayName); } [Fact] public async Task TelephoneAbsent_IndiquePasDeTelephone() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice", email: "alice@test.fr")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert var item = Assert.Single(result); Assert.False(item.HasPhone); Assert.True(item.HasEmail); } [Fact] public async Task EmailAbsent_IndiquePasDEmail() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice", mobile: "0600000000")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert var item = Assert.Single(result); Assert.True(item.HasPhone); Assert.False(item.HasEmail); } [Fact] public async Task RelationInvalide_IgnoreeProprement() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); // ContactIds contient un GUID invalide + un GUID valide. var dto = new InterventionDetailsDto { Id = intervention.ToString(), Reference = "INT-001", ContactIds = ["not-a-guid", contact.ToString()] }; await InterventionRepository.UpsertAsync(new LocalIntervention { Id = intervention, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : l'entrée invalide est ignorée, le contact valide est retourné. var item = Assert.Single(result); Assert.Equal(contact, item.ContactGuid); } [Fact] public async Task InterventionInconnue_RetourneListeVide() { // Act var result = await _service.GetContactsForInterventionAsync(Guid.NewGuid()); // Assert Assert.Empty(result); } [Fact] public async Task FonctionneOffline_SansAppelApi() { // Arrange : aucune dépendance réseau, uniquement SQLite local. Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice")); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : le service fonctionne entièrement depuis SQLite. Assert.Single(result); } [Fact] public async Task GetContactById_RetourneLeContact() { // Arrange Guid contact = Guid.NewGuid(); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Dupont", "Alice", email: "alice@test.fr")); // Act var result = await _service.GetContactByIdAsync(contact); // Assert Assert.NotNull(result); Assert.Equal(contact, result.ContactGuid); Assert.Equal("Alice Dupont", result.DisplayName); Assert.Equal("alice@test.fr", result.Email); } [Fact] public async Task GetContactById_Inconnu_RetourneNull() { // Act var result = await _service.GetContactByIdAsync(Guid.NewGuid()); // Assert Assert.Null(result); } // ===================================================================== // Destinataires de courriel : uniquement les contacts de l'intervention // ===================================================================== /// /// Le choix d'un destinataire ne propose QUE les contacts de CETTE /// intervention, jamais le reste du répertoire local (qui contenait les /// contacts de toutes les autres interventions et de tous les clients). /// [Fact] public async Task ContactsDisponibles_UniquementLesContactsDeLIntervention() { // Arrange : l'intervention A a Alice ; le répertoire local contient en plus // les contacts de l'intervention B et un contact sans aucune relation. Guid interventionA = Guid.NewGuid(); Guid interventionB = Guid.NewGuid(); Guid alice = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(interventionA, alice)); await InterventionRepository.UpsertAsync(CreateIntervention(interventionB, Guid.NewGuid())); await PersonneRepository.UpsertAsync(CreatePersonne(alice, "Dupont", "Alice", email: "alice@test.fr")); await PersonneRepository.UpsertAsync(CreatePersonne(Guid.NewGuid(), "Martin", "Bob", email: "bob@test.fr")); await PersonneRepository.UpsertAsync(CreatePersonne(Guid.NewGuid(), "Durand", "Claire", email: "claire@test.fr")); // Act var result = await _service.GetContactsDisponiblesAsync(interventionA); // Assert : une seule proposition, celle de l'intervention. var contact = Assert.Single(result); Assert.Equal(alice, contact.ContactGuid); Assert.Equal("alice@test.fr", contact.Email); } /// Un contact de l'intervention sans adresse email n'est pas proposé comme destinataire. [Fact] public async Task ContactsDisponibles_ExcluentLesContactsSansEmail() { // Arrange Guid intervention = Guid.NewGuid(); Guid avecEmail = Guid.NewGuid(); Guid sansEmail = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, avecEmail, sansEmail)); await PersonneRepository.UpsertAsync(CreatePersonne(avecEmail, "Dupont", "Alice", email: "alice@test.fr")); await PersonneRepository.UpsertAsync(CreatePersonne(sansEmail, "Martin", "Bob", mobile: "0600000000")); // Act var result = await _service.GetContactsDisponiblesAsync(intervention); // Assert var contact = Assert.Single(result); Assert.Equal(avecEmail, contact.ContactGuid); } /// Intervention sans contact : aucune proposition (jamais un repli sur le répertoire). [Fact] public async Task ContactsDisponibles_InterventionSansContact_ListeVide() { // Arrange : le répertoire local est rempli, l'intervention n'a aucun contact. Guid intervention = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention)); await PersonneRepository.UpsertAsync(CreatePersonne(Guid.NewGuid(), "Dupont", "Alice", email: "alice@test.fr")); // Act var result = await _service.GetContactsDisponiblesAsync(intervention); // Assert Assert.Empty(result); } [Fact] public async Task PayloadInvalide_EntreeIgnoree() { // Arrange Guid intervention = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await InterventionRepository.UpsertAsync(CreateIntervention(intervention, contact)); await PersonneRepository.UpsertAsync(new LocalPersonne { Id = contact, PayloadJson = "not-json", UpdatedAtUtc = DateTimeOffset.UtcNow }); // Act var result = await _service.GetContactsForInterventionAsync(intervention); // Assert : l'entrée au payload invalide est ignorée proprement. Assert.Empty(result); } }