233 lines
9.7 KiB
C#
233 lines
9.7 KiB
C#
using Omika.App.Local.Models;
|
|
using Omika.Core.Contracts.Mobile.Sync;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// Tests de l'Outbox locale : OperationId unique, conservation pendant retry,
|
|
/// ordre CREATE → UPDATE → DELETE/ARCHIVE, REJECTED non renvoyé, récupération
|
|
/// après restart.
|
|
/// </summary>
|
|
public class OutboxTests : LocalTestBase
|
|
{
|
|
private static OutboxOperation CreateOperation(
|
|
SyncOperationType type,
|
|
Guid? requestedGuid = null,
|
|
OutboxState state = OutboxState.Pending)
|
|
=> new()
|
|
{
|
|
OperationId = Guid.NewGuid(),
|
|
EntityType = "Client",
|
|
OperationType = type,
|
|
RequestedEntityGuid = requestedGuid ?? Guid.NewGuid(),
|
|
Payload = "{}",
|
|
State = state
|
|
};
|
|
|
|
[Fact]
|
|
public async Task NouvelleMutation_OperationIdUnique()
|
|
{
|
|
// Arrange
|
|
var op1 = CreateOperation(SyncOperationType.Create);
|
|
var op2 = CreateOperation(SyncOperationType.Create);
|
|
|
|
// Act
|
|
await OutboxRepository.AddAsync(op1);
|
|
await OutboxRepository.AddAsync(op2);
|
|
|
|
// Assert
|
|
Assert.NotEqual(op1.OperationId, op2.OperationId);
|
|
Assert.Equal(2, await OutboxRepository.CountPendingAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OperationId_ConservePendantRetry()
|
|
{
|
|
// Arrange
|
|
var op = CreateOperation(SyncOperationType.Create);
|
|
await OutboxRepository.AddAsync(op);
|
|
Guid originalId = op.OperationId;
|
|
|
|
// Act : simuler un retry (mise à jour de l'état et du compteur).
|
|
op.State = OutboxState.Sending;
|
|
op.RetryCount = 1;
|
|
await OutboxRepository.UpdateAsync(op);
|
|
|
|
op.State = OutboxState.Pending;
|
|
op.RetryCount = 2;
|
|
await OutboxRepository.UpdateAsync(op);
|
|
|
|
// Assert : l'OperationId n'a jamais changé.
|
|
OutboxOperation? loaded = await OutboxRepository.GetByIdAsync(originalId);
|
|
Assert.NotNull(loaded);
|
|
Assert.Equal(originalId, loaded.OperationId);
|
|
Assert.Equal(2, loaded.RetryCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ordre_CreateAvantUpdate()
|
|
{
|
|
// Arrange
|
|
var createOp = CreateOperation(SyncOperationType.Create);
|
|
var updateOp = CreateOperation(SyncOperationType.Update);
|
|
|
|
// Act : insérer l'UPDATE d'abord, puis le CREATE.
|
|
await OutboxRepository.AddAsync(updateOp);
|
|
await OutboxRepository.AddAsync(createOp);
|
|
|
|
// Assert : GetReadyAsync retourne CREATE avant UPDATE.
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
Assert.Equal(2, ready.Count);
|
|
Assert.Equal(SyncOperationType.Create, ready[0].OperationType);
|
|
Assert.Equal(SyncOperationType.Update, ready[1].OperationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ordre_UpdateAvantDelete()
|
|
{
|
|
// Arrange
|
|
var updateOp = CreateOperation(SyncOperationType.Update);
|
|
var deleteOp = CreateOperation(SyncOperationType.Delete);
|
|
|
|
// Act : insérer le DELETE d'abord, puis l'UPDATE.
|
|
await OutboxRepository.AddAsync(deleteOp);
|
|
await OutboxRepository.AddAsync(updateOp);
|
|
|
|
// Assert : GetReadyAsync retourne UPDATE avant DELETE.
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
Assert.Equal(2, ready.Count);
|
|
Assert.Equal(SyncOperationType.Update, ready[0].OperationType);
|
|
Assert.Equal(SyncOperationType.Delete, ready[1].OperationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Rejected_NonRenvoyeAutomatiquement()
|
|
{
|
|
// Arrange
|
|
var op = CreateOperation(SyncOperationType.Create, state: OutboxState.Rejected);
|
|
op.LastError = "ERR001: rejeté";
|
|
await OutboxRepository.AddAsync(op);
|
|
|
|
// Act
|
|
IReadOnlyList<OutboxOperation> ready = await OutboxRepository.GetReadyAsync();
|
|
|
|
// Assert : l'opération REJECTED n'est pas dans les prêtes à envoyer.
|
|
Assert.Empty(ready);
|
|
|
|
// L'opération est conservée pour diagnostic.
|
|
OutboxOperation? loaded = await OutboxRepository.GetByIdAsync(op.OperationId);
|
|
Assert.NotNull(loaded);
|
|
Assert.Equal(OutboxState.Rejected, loaded.State);
|
|
Assert.Equal("ERR001: rejeté", loaded.LastError);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RecuperationApresRestart_EtatTemporaire()
|
|
{
|
|
// Arrange : simuler un crash avec des opérations en Sending et WaitingResult.
|
|
var sendingOp = CreateOperation(SyncOperationType.Create, state: OutboxState.Sending);
|
|
var waitingOp = CreateOperation(SyncOperationType.Update, state: OutboxState.WaitingResult);
|
|
await OutboxRepository.AddAsync(sendingOp);
|
|
await OutboxRepository.AddAsync(waitingOp);
|
|
|
|
// Act : récupération au démarrage.
|
|
int recovered = await OutboxRepository.RecoverTemporaryStatesAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(2, recovered);
|
|
|
|
OutboxOperation? sendingLoaded = await OutboxRepository.GetByIdAsync(sendingOp.OperationId);
|
|
OutboxOperation? waitingLoaded = await OutboxRepository.GetByIdAsync(waitingOp.OperationId);
|
|
Assert.Equal(OutboxState.Pending, sendingLoaded!.State);
|
|
Assert.Equal(OutboxState.Pending, waitingLoaded!.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTemporaryState_RetourneSendingEtWaitingResult()
|
|
{
|
|
// Arrange
|
|
var sendingOp = CreateOperation(SyncOperationType.Create, state: OutboxState.Sending);
|
|
var waitingOp = CreateOperation(SyncOperationType.Update, state: OutboxState.WaitingResult);
|
|
var pendingOp = CreateOperation(SyncOperationType.Create, state: OutboxState.Pending);
|
|
await OutboxRepository.AddAsync(sendingOp);
|
|
await OutboxRepository.AddAsync(waitingOp);
|
|
await OutboxRepository.AddAsync(pendingOp);
|
|
|
|
// Act
|
|
IReadOnlyList<OutboxOperation> temporary = await OutboxRepository.GetTemporaryStateAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(2, temporary.Count);
|
|
Assert.All(temporary, op => Assert.True(
|
|
op.State == OutboxState.Sending || op.State == OutboxState.WaitingResult));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RecuperationEnvoiInterrompu_NeTouchePasLesEtatsSuivisParStatut()
|
|
{
|
|
// Arrange : un envoi interrompu (à récupérer) et une opération déjà reçue
|
|
// par le serveur, en attente de résultat (à ne surtout pas réémettre).
|
|
var interrupted = CreateOperation(SyncOperationType.Create, state: OutboxState.Sending);
|
|
var accepted = CreateOperation(SyncOperationType.Update, state: OutboxState.Accepted);
|
|
var waiting = CreateOperation(SyncOperationType.Update, state: OutboxState.WaitingResult);
|
|
await OutboxRepository.AddAsync(interrupted);
|
|
await OutboxRepository.AddAsync(accepted);
|
|
await OutboxRepository.AddAsync(waiting);
|
|
|
|
// Act
|
|
int recovered = await OutboxRepository.RecoverSendingStatesAsync();
|
|
|
|
// Assert : seul l'envoi interrompu est remis en attente.
|
|
Assert.Equal(1, recovered);
|
|
|
|
OutboxOperation? interruptedLoaded = await OutboxRepository.GetByIdAsync(interrupted.OperationId);
|
|
Assert.Equal(OutboxState.Pending, interruptedLoaded!.State);
|
|
|
|
OutboxOperation? acceptedLoaded = await OutboxRepository.GetByIdAsync(accepted.OperationId);
|
|
Assert.Equal(OutboxState.Accepted, acceptedLoaded!.State);
|
|
|
|
OutboxOperation? waitingLoaded = await OutboxRepository.GetByIdAsync(waiting.OperationId);
|
|
Assert.Equal(OutboxState.WaitingResult, waitingLoaded!.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetNotYetSent_RetourneUniquementPendingEtSending()
|
|
{
|
|
// Arrange : opérations à envoyer (Pending/Sending) et déjà envoyées
|
|
// (Accepted/WaitingResult).
|
|
var pending = CreateOperation(SyncOperationType.Create, state: OutboxState.Pending);
|
|
var sending = CreateOperation(SyncOperationType.Create, state: OutboxState.Sending);
|
|
var accepted = CreateOperation(SyncOperationType.Create, state: OutboxState.Accepted);
|
|
var waiting = CreateOperation(SyncOperationType.Create, state: OutboxState.WaitingResult);
|
|
await OutboxRepository.AddAsync(pending);
|
|
await OutboxRepository.AddAsync(sending);
|
|
await OutboxRepository.AddAsync(accepted);
|
|
await OutboxRepository.AddAsync(waiting);
|
|
|
|
// Act
|
|
IReadOnlyList<OutboxOperation> notYetSent = await OutboxRepository.GetNotYetSentAsync();
|
|
|
|
// Assert : seules les opérations non encore envoyées sont affichées.
|
|
Assert.Equal(2, notYetSent.Count);
|
|
Assert.All(notYetSent, op => Assert.True(
|
|
op.State == OutboxState.Pending || op.State == OutboxState.Sending));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CountNotYetSent_ExclutEnvoyees_MaisCountPendingGardeTout()
|
|
{
|
|
// Arrange
|
|
await OutboxRepository.AddAsync(CreateOperation(SyncOperationType.Create, state: OutboxState.Pending));
|
|
await OutboxRepository.AddAsync(CreateOperation(SyncOperationType.Create, state: OutboxState.Sending));
|
|
await OutboxRepository.AddAsync(CreateOperation(SyncOperationType.Create, state: OutboxState.Accepted));
|
|
await OutboxRepository.AddAsync(CreateOperation(SyncOperationType.Create, state: OutboxState.WaitingResult));
|
|
await OutboxRepository.AddAsync(CreateOperation(SyncOperationType.Create, state: OutboxState.Succeeded));
|
|
await OutboxRepository.AddAsync(CreateOperation(SyncOperationType.Create, state: OutboxState.Rejected));
|
|
|
|
// Act / Assert : le badge « à envoyer » ne compte que Pending/Sending,
|
|
// tandis que le compteur moteur (travail restant) conserve les états suivis.
|
|
Assert.Equal(2, await OutboxRepository.CountNotYetSentAsync());
|
|
Assert.Equal(4, await OutboxRepository.CountPendingAsync());
|
|
}
|
|
}
|