using Omika.App.Local.Models;
using Omika.Core.Contracts.Mobile.Sync;
namespace Omika.Tests.Mobile.Local;
///
/// Tests de l'Outbox locale : OperationId unique, conservation pendant retry,
/// ordre CREATE → UPDATE → DELETE/ARCHIVE, REJECTED non renvoyé, récupération
/// après restart.
///
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 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 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 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 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 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());
}
}