570 lines
18 KiB
C#
570 lines
18 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Tests du service de lecture des clients depuis les données locales SQLite.
|
|
/// Relations réellement démontrables localement :
|
|
/// <list type="bullet">
|
|
/// <item>Client → Sites : liste <c>Sites</c> embarquée dans le DTO Client.</item>
|
|
/// <item>Client → Contacts : liste <c>Contacts</c> embarquée dans le DTO Client.</item>
|
|
/// <item>Client → Dossiers : <c>DossierDetailsDto.ClientId == ClientGuid</c>.</item>
|
|
/// <item>Client → Interventions : <c>InterventionDetailsDto.ClientId == ClientGuid</c>.</item>
|
|
/// </list>
|
|
/// Aucune relation n'est déduite par commodité. Lecture seule : aucune écriture
|
|
/// ni Outbox n'est créée.
|
|
/// </summary>
|
|
public class ClientLocalServiceTests : LocalTestBase
|
|
{
|
|
private readonly ClientLocalService _service;
|
|
|
|
public ClientLocalServiceTests()
|
|
{
|
|
_service = new ClientLocalService(ClientRepository, DossierRepository, ContextInterventionCacheRepository, ClientStateRepository, ApiClient);
|
|
}
|
|
|
|
private static string Serialize<T>(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<SiteDto>? sites = null,
|
|
IReadOnlyList<ContactDto>? 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<InterventionDetailsDto>(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);
|
|
}
|
|
}
|