179 lines
6.0 KiB
C#
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());
|
|
}
|
|
}
|