using Omika.App.Local.Models; using Omika.App.Local.Services; using Omika.App.Local.Services.PayloadRewriters; using Omika.Core.Contracts.Mobile.Sync; namespace Omika.Tests.Mobile.Local; /// /// Tests de la réconciliation des identités (A → B). /// Cas CREATE Batik : le mobile crée localement A, le serveur répond B. /// Vérifie : entité principale, relations enfants, Outbox, mapping, idempotence. /// public class IdentityReconciliationTests : LocalTestBase { private const string InterventionType = "Intervention"; private readonly MobileIdentityReconciliationService _reconciliation; public IdentityReconciliationTests() { _reconciliation = new MobileIdentityReconciliationService( Database, IdentityMappingRepository, OutboxRepository, [new InterventionPayloadRewriter(), new RendezVousPayloadRewriter()]); } [Fact] public async Task CreateBatik_EntitePrincipale_DevientB() { // Arrange Guid a = Guid.NewGuid(); Guid b = Guid.NewGuid(); var intervention = new LocalIntervention { Id = a, PayloadJson = $$"""{"Id":"{{a}}","ClientId":"{{Guid.NewGuid()}}","SiteId":"{{Guid.NewGuid()}}"}""" }; await InterventionRepository.UpsertAsync(intervention); // Act await _reconciliation.ReconcileAsync(InterventionType, a, b); // Assert : l'entité principale A devient B. LocalIntervention? loaded = await InterventionRepository.GetByIdAsync(b); Assert.NotNull(loaded); Assert.Equal(b, loaded.Id); Assert.Null(await InterventionRepository.GetByIdAsync(a)); } [Fact] public async Task CreateBatik_RelationEnfant_DevientB() { // Arrange Guid a = Guid.NewGuid(); Guid b = Guid.NewGuid(); Guid rdvId = Guid.NewGuid(); var intervention = new LocalIntervention { Id = a, PayloadJson = $$"""{"Id":"{{a}}"}""" }; await InterventionRepository.UpsertAsync(intervention); var rdv = new LocalRendezVous { Id = rdvId, InterventionId = a, PayloadJson = $$"""{"Id":"{{rdvId}}","InterventionId":"{{a}}"}""" }; await RendezVousRepository.UpsertAsync(rdv); // Act await _reconciliation.ReconcileAsync(InterventionType, a, b); // Assert : la relation enfant RendezVous.InterventionGuid devient B. LocalRendezVous? loadedRdv = await RendezVousRepository.GetByIdAsync(rdvId); Assert.NotNull(loadedRdv); Assert.Equal(b, loadedRdv.InterventionId); } [Fact] public async Task CreateBatik_OutboxPending_UtiliseB() { // Arrange Guid a = Guid.NewGuid(); Guid b = Guid.NewGuid(); var op = new OutboxOperation { OperationId = Guid.NewGuid(), EntityType = InterventionType, OperationType = SyncOperationType.Update, RequestedEntityGuid = a, Payload = $$"""{"Id":"{{a}}","ClientId":"{{Guid.NewGuid()}}"}""", State = OutboxState.Pending }; await OutboxRepository.AddAsync(op); // Act await _reconciliation.ReconcileAsync(InterventionType, a, b); // Assert : l'Outbox pending utilise B. OutboxOperation? loaded = await OutboxRepository.GetByIdAsync(op.OperationId); Assert.NotNull(loaded); Assert.Equal(b, loaded.RequestedEntityGuid); Assert.Equal(b, loaded.CanonicalEntityGuid); Assert.Contains(b.ToString(), loaded.Payload); Assert.DoesNotContain(a.ToString(), loaded.Payload); } [Fact] public async Task CreateBatik_MappingPersiste() { // Arrange Guid a = Guid.NewGuid(); Guid b = Guid.NewGuid(); // Act await _reconciliation.ReconcileAsync(InterventionType, a, b); // Assert : le mapping A → B persiste. IdentityMapping? mapping = await IdentityMappingRepository.GetAsync(InterventionType, a); Assert.NotNull(mapping); Assert.Equal(b, mapping.CanonicalEntityGuid); } [Fact] public async Task CreateBatik_NouvelleResolution_RetourneB() { // Arrange Guid a = Guid.NewGuid(); Guid b = Guid.NewGuid(); await _reconciliation.ReconcileAsync(InterventionType, a, b); // Act Guid resolved = await _reconciliation.ResolveAsync(InterventionType, a); // Assert Assert.Equal(b, resolved); } [Fact] public async Task CreateBatik_SecondTraitement_Idempotent() { // Arrange Guid a = Guid.NewGuid(); Guid b = Guid.NewGuid(); var intervention = new LocalIntervention { Id = a, PayloadJson = $$"""{"Id":"{{a}}"}""" }; await InterventionRepository.UpsertAsync(intervention); // Act : premier traitement. await _reconciliation.ReconcileAsync(InterventionType, a, b); // Act : second traitement de la même réconciliation. await _reconciliation.ReconcileAsync(InterventionType, a, b); // Assert : toujours B, pas d'effet dangereux. LocalIntervention? loaded = await InterventionRepository.GetByIdAsync(b); Assert.NotNull(loaded); Assert.Equal(b, loaded.Id); Assert.Equal(1, await InterventionRepository.CountAsync()); } [Fact] public async Task Omika_AB_Identiques_AucunEffet() { // Arrange Guid a = Guid.NewGuid(); var intervention = new LocalIntervention { Id = a, PayloadJson = $$"""{"Id":"{{a}}"}""" }; await InterventionRepository.UpsertAsync(intervention); // Act : A == B. await _reconciliation.ReconcileAsync(InterventionType, a, a); // Assert : aucun effet dangereux. LocalIntervention? loaded = await InterventionRepository.GetByIdAsync(a); Assert.NotNull(loaded); Assert.Equal(a, loaded.Id); Assert.Equal(1, await InterventionRepository.CountAsync()); } }