566 lines
18 KiB
C#
566 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;
|
|
using Omika.Core.Contracts.Sites;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests du service de lecture des sites / lieux depuis les données locales
|
|
/// SQLite. Relations réellement démontrables localement :
|
|
/// <list type="bullet">
|
|
/// <item>Site → Client : relation explicite <c>SiteDetailsDto.ClientId</c>.</item>
|
|
/// <item>Site → Contacts : liste explicite <c>SiteDetailsDto.ContactIds</c>, résolue vers les Personnes/Contacts locales.</item>
|
|
/// <item>Site → Interventions : relation directe <c>InterventionDetailsDto.SiteId</c>.</item>
|
|
/// </list>
|
|
/// Aucune relation n'est déduite par commodité (nom, adresse, transitivité).
|
|
/// Lecture seule : aucune écriture ni Outbox n'est créée.
|
|
/// </summary>
|
|
public class SiteLocalServiceTests : LocalTestBase
|
|
{
|
|
private readonly SiteLocalService _service;
|
|
|
|
public SiteLocalServiceTests()
|
|
{
|
|
_service = new SiteLocalService(
|
|
SiteRepository, ClientRepository, PersonneRepository, RendezVousRepository,
|
|
InterventionRepository, ContextInterventionCacheRepository, SiteStateRepository, ApiClient);
|
|
}
|
|
|
|
private static string Serialize<T>(T value) => JsonSerializer.Serialize(value);
|
|
|
|
private static LocalSite CreateSite(
|
|
Guid id,
|
|
string nom,
|
|
string? code = null,
|
|
string? ville = null,
|
|
string? type = null,
|
|
Guid? clientId = null,
|
|
IReadOnlyList<string>? contactIds = null)
|
|
{
|
|
var dto = new SiteDetailsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Nom = nom,
|
|
Code = code,
|
|
Type = type is null ? null : new ReferenceValueDto { Label = type },
|
|
ClientId = clientId?.ToString(),
|
|
Adresse = ville is null
|
|
? null
|
|
: new AddressDto { City = ville, OneLine = ville },
|
|
ContactIds = contactIds ?? []
|
|
};
|
|
return new LocalSite
|
|
{
|
|
Id = id,
|
|
ClientId = clientId,
|
|
PayloadJson = Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private static LocalClient CreateClient(Guid id, string nom, string? code = null)
|
|
{
|
|
var dto = new ClientDetailsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Nom = nom,
|
|
Code = code
|
|
};
|
|
return new LocalClient
|
|
{
|
|
Id = id,
|
|
PayloadJson = Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private static LocalPersonne CreatePersonne(Guid id, string nomAffiche, string? email = null)
|
|
{
|
|
var dto = new ContactDto
|
|
{
|
|
Id = id.ToString(),
|
|
NomAffiche = nomAffiche,
|
|
Coordonnees = email is null
|
|
? null
|
|
: new ContactDetailsDto { Email = email }
|
|
};
|
|
return new LocalPersonne
|
|
{
|
|
Id = id,
|
|
PayloadJson = Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private static LocalIntervention CreateIntervention(
|
|
Guid id,
|
|
Guid siteId,
|
|
string reference,
|
|
string titre,
|
|
DateTime? datePrevueDebut = null,
|
|
string? etat = null)
|
|
{
|
|
var dto = new InterventionDetailsDto
|
|
{
|
|
Id = id.ToString(),
|
|
Reference = reference,
|
|
Titre = titre,
|
|
SiteId = siteId.ToString(),
|
|
DatePrevueDebut = datePrevueDebut,
|
|
Etat = etat is null ? null : new ReferenceValueDto { Label = etat }
|
|
};
|
|
return new LocalIntervention
|
|
{
|
|
Id = id,
|
|
SiteId = siteId,
|
|
PayloadJson = Serialize(dto),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
private async Task CacheSiteInterventionsAsync(Guid siteId, params LocalIntervention[] interventions)
|
|
{
|
|
var details = interventions
|
|
.Select(i => JsonSerializer.Deserialize<InterventionDetailsDto>(i.PayloadJson)!)
|
|
.ToList();
|
|
await ContextInterventionCacheRepository.ReplaceAsync(InterventionContextTypes.Site, siteId, details);
|
|
}
|
|
|
|
// ===================== LISTE =====================
|
|
|
|
[Fact]
|
|
public async Task AucunSite_RetourneListeVide()
|
|
{
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UnSite_RetourneLeSite()
|
|
{
|
|
// Arrange
|
|
Guid siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha", "S001"));
|
|
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert
|
|
var item = Assert.Single(result);
|
|
Assert.Equal(siteId, item.SiteGuid);
|
|
Assert.Equal("Site Alpha", item.Nom);
|
|
Assert.Equal("S001", item.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PlusieursSites_RetourneTous()
|
|
{
|
|
// Arrange
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha"));
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Beta"));
|
|
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(2, result.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Tri_DeterministeParNomPuisCode()
|
|
{
|
|
// Arrange
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Beta", "S002"));
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha", "S001"));
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha", "S003"));
|
|
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert : tri stable par nom (insensible à la casse) puis code.
|
|
Assert.Equal("Alpha", result[0].Nom);
|
|
Assert.Equal("S001", result[0].Code);
|
|
Assert.Equal("Alpha", result[1].Nom);
|
|
Assert.Equal("S003", result[1].Code);
|
|
Assert.Equal("Beta", result[2].Nom);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RechercheLocale_InsensibleALaCasse()
|
|
{
|
|
// Arrange
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha", "S001", "Paris"));
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Beta", "S002", "Lyon"));
|
|
|
|
// Act : le service retourne tout ; la recherche est gérée par le ViewModel.
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(2, result.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RechercheSansResultat_ListeVide()
|
|
{
|
|
// Arrange
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha"));
|
|
|
|
// Act : le service retourne tout ; la recherche est gérée par le ViewModel.
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert
|
|
Assert.Single(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PayloadInvalide_EntreeIgnoree()
|
|
{
|
|
// Arrange
|
|
await SiteRepository.UpsertAsync(new LocalSite
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PayloadJson = "not-json",
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
});
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Valide"));
|
|
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// 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 siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Alpha"));
|
|
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert : l'identité publique (GUID) est conservée.
|
|
var item = Assert.Single(result);
|
|
Assert.Equal(siteId, item.SiteGuid);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FonctionneOffline_SansAppelApi()
|
|
{
|
|
// Arrange : aucune dépendance réseau, uniquement SQLite local.
|
|
await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha"));
|
|
|
|
// Act
|
|
var result = await _service.GetSitesAsync();
|
|
|
|
// Assert
|
|
Assert.Single(result);
|
|
}
|
|
|
|
// ===================== DÉTAIL =====================
|
|
|
|
[Fact]
|
|
public async Task Detail_SiteTrouve()
|
|
{
|
|
// Arrange
|
|
Guid siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha", "S001"));
|
|
|
|
// Act
|
|
var result = await _service.GetSiteByIdAsync(siteId);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(siteId, result.SiteGuid);
|
|
Assert.Equal("Site Alpha", result.Nom);
|
|
Assert.Equal("S001", result.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Detail_SiteInconnu_RetourneNull()
|
|
{
|
|
// Act
|
|
var result = await _service.GetSiteByIdAsync(Guid.NewGuid());
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Detail_DonneesReellesChargees()
|
|
{
|
|
// Arrange
|
|
Guid siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha", "S001", "Paris", "Entrepôt"));
|
|
|
|
// Act
|
|
var result = await _service.GetSiteByIdAsync(siteId);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("Site Alpha", result.Nom);
|
|
Assert.Equal("S001", result.Code);
|
|
Assert.Equal("Entrepôt", result.Type);
|
|
Assert.True(result.HasAdresse);
|
|
Assert.Equal("Paris", result.Adresse!.City);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Detail_ChampsOptionnelsAbsents()
|
|
{
|
|
// Arrange : site minimal sans adresse ni type.
|
|
Guid siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha"));
|
|
|
|
// Act
|
|
var result = await _service.GetSiteByIdAsync(siteId);
|
|
|
|
// Assert : champs optionnels absents restent nuls sans erreur.
|
|
Assert.NotNull(result);
|
|
Assert.Null(result.Type);
|
|
Assert.Null(result.Adresse);
|
|
Assert.False(result.HasAdresse);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Detail_AucuneRelation_NeProvoquePasDErreur()
|
|
{
|
|
// Arrange : site sans client, contacts ni interventions.
|
|
Guid siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha"));
|
|
|
|
// Act
|
|
var result = await _service.GetSiteByIdAsync(siteId);
|
|
|
|
// Assert : collections vides sans erreur.
|
|
Assert.NotNull(result);
|
|
Assert.Empty(result.Clients);
|
|
Assert.Empty(result.Contacts);
|
|
Assert.Empty(result.Interventions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Detail_IdentitePublique_Conservee()
|
|
{
|
|
// Arrange
|
|
Guid siteId = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteId, "Alpha"));
|
|
|
|
// Act
|
|
var result = await _service.GetSiteByIdAsync(siteId);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(siteId, result.SiteGuid);
|
|
}
|
|
|
|
// ===================== RELATION SITE → CONTACTS =====================
|
|
|
|
[Fact]
|
|
public async Task SiteA_RecoitUniquementSesContacts()
|
|
{
|
|
// Arrange : Site A embarque 2 contacts, Site B en embarque 1.
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid siteB = Guid.NewGuid();
|
|
Guid contactA1 = Guid.NewGuid();
|
|
Guid contactA2 = Guid.NewGuid();
|
|
Guid contactB = Guid.NewGuid();
|
|
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A", contactIds: [contactA1.ToString(), contactA2.ToString()]));
|
|
await SiteRepository.UpsertAsync(CreateSite(siteB, "Site B", contactIds: [contactB.ToString()]));
|
|
await PersonneRepository.UpsertAsync(CreatePersonne(contactA1, "Contact A1"));
|
|
await PersonneRepository.UpsertAsync(CreatePersonne(contactA2, "Contact A2"));
|
|
await PersonneRepository.UpsertAsync(CreatePersonne(contactB, "Contact B"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// 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 ContactDuMemeClientMaisAutreSite_Exclu()
|
|
{
|
|
// Arrange : Contact lié au Site B uniquement, même si le Client est partagé.
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid siteB = Guid.NewGuid();
|
|
Guid contactB = Guid.NewGuid();
|
|
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A"));
|
|
await SiteRepository.UpsertAsync(CreateSite(siteB, "Site B", contactIds: [contactB.ToString()]));
|
|
await PersonneRepository.UpsertAsync(CreatePersonne(contactB, "Contact B"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : A n'a aucun contact de B.
|
|
Assert.NotNull(resultA);
|
|
Assert.Empty(resultA.Contacts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ContactNonRelie_Exclu()
|
|
{
|
|
// Arrange : un contact sans ContactIds sur le site.
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid contact = Guid.NewGuid();
|
|
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A"));
|
|
await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Contact Libre"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : aucun contact non relié n'est ajouté.
|
|
Assert.NotNull(resultA);
|
|
Assert.Empty(resultA.Contacts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ContactGuid_PublicConserve()
|
|
{
|
|
// Arrange
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid contactA = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A", contactIds: [contactA.ToString()]));
|
|
await PersonneRepository.UpsertAsync(CreatePersonne(contactA, "Contact A"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : le GUID public du contact est conservé.
|
|
Assert.NotNull(resultA);
|
|
var contact = Assert.Single(resultA.Contacts);
|
|
Assert.Equal(contactA, contact.ContactGuid);
|
|
}
|
|
|
|
// ===================== RELATION SITE → INTERVENTIONS =====================
|
|
|
|
[Fact]
|
|
public async Task InterventionSiteA_Incluse()
|
|
{
|
|
// Arrange
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid interventionA = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A"));
|
|
await CacheSiteInterventionsAsync(siteA, CreateIntervention(interventionA, siteA, "INT-A", "Intervention A"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert
|
|
Assert.NotNull(resultA);
|
|
var intervention = Assert.Single(resultA.Interventions);
|
|
Assert.Equal(interventionA, intervention.InterventionGuid);
|
|
Assert.Equal("INT-A", intervention.Reference);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InterventionSiteB_Exclue()
|
|
{
|
|
// Arrange
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid siteB = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A"));
|
|
await SiteRepository.UpsertAsync(CreateSite(siteB, "Site B"));
|
|
await CacheSiteInterventionsAsync(siteA, CreateIntervention(Guid.NewGuid(), siteB, "INT-B", "Intervention B"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : A n'a aucune intervention de B.
|
|
Assert.NotNull(resultA);
|
|
Assert.Empty(resultA.Interventions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InterventionSansSite_Exclue()
|
|
{
|
|
// Arrange : intervention sans SiteId.
|
|
Guid siteA = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A"));
|
|
await CacheSiteInterventionsAsync(siteA, new LocalIntervention
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
PayloadJson = Serialize(new InterventionDetailsDto
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Reference = "INT-X",
|
|
Titre = "Intervention X"
|
|
}),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : aucune intervention sans site n'est ajoutée.
|
|
Assert.NotNull(resultA);
|
|
Assert.Empty(resultA.Interventions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AucuneDeductionViaClient()
|
|
{
|
|
// Arrange : intervention liée au Client A mais pas au Site A.
|
|
Guid clientA = Guid.NewGuid();
|
|
Guid siteA = Guid.NewGuid();
|
|
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A", clientId: clientA));
|
|
await CacheSiteInterventionsAsync(siteA, new LocalIntervention
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ClientId = clientA,
|
|
PayloadJson = Serialize(new InterventionDetailsDto
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Reference = "INT-A",
|
|
Titre = "Intervention A",
|
|
ClientId = clientA.ToString()
|
|
}),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : l'intervention n'est pas déduite via le Client.
|
|
Assert.NotNull(resultA);
|
|
Assert.Empty(resultA.Interventions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InterventionGuid_PublicConserve()
|
|
{
|
|
// Arrange
|
|
Guid siteA = Guid.NewGuid();
|
|
Guid interventionA = Guid.NewGuid();
|
|
await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A"));
|
|
await CacheSiteInterventionsAsync(siteA, CreateIntervention(interventionA, siteA, "INT-A", "Intervention A"));
|
|
|
|
// Act
|
|
var resultA = await _service.GetSiteByIdAsync(siteA);
|
|
|
|
// Assert : le GUID public de l'intervention est conservé.
|
|
Assert.NotNull(resultA);
|
|
var intervention = Assert.Single(resultA.Interventions);
|
|
Assert.Equal(interventionA, intervention.InterventionGuid);
|
|
}
|
|
}
|