Files
Omika/tests/Omika.Tests/Mobile/Local/IdentityReconciliationTests.cs
T
2026-10-23 18:46:50 +02:00

179 lines
6.0 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
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());
}
}