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

248 lines
8.0 KiB
C#

using Omika.Core.Contracts.Mobile.Sync;
using Omika.Core.Interfaces.Mobile;
using Omika.Core.Services.Mobile;
namespace Omika.Tests.Mobile;
/// <summary>
/// Tests de la réception et du suivi des opérations de synchronisation.
/// </summary>
public class SyncOperationServiceTests
{
private const string DeviceId = "device-1";
private static MobileTokenIdentity Identity() => new()
{
CodeClient = "BATIK.01",
DeviceId = DeviceId,
UserId = "user-1",
CollaborateurId = "collab-1",
NumeroLicence = "lic-1"
};
private static SyncOperation ValidOperation(SyncOperationType type = SyncOperationType.Create)
{
return new SyncOperation
{
OperationId = Guid.NewGuid(),
DeviceId = DeviceId,
EntityType = "Client",
OperationType = type,
EntityGuid = Guid.NewGuid(),
Payload = "{}"
};
}
[Fact]
public async Task Receive_Valid_Operation_Returns_Received()
{
// Arrange
var repo = new FakeSyncTransferRepository();
var service = new SyncOperationService(repo);
var op = ValidOperation();
// Act
var receipt = await service.ReceiveAsync(op, Identity());
// Assert
Assert.Equal("RECEIVED", receipt.Status);
Assert.Equal(op.OperationId, receipt.OperationId);
Assert.True(repo.Inserted);
Assert.True(repo.Logged);
}
[Fact]
public async Task Receive_Empty_OperationId_Throws()
{
// Arrange
var repo = new FakeSyncTransferRepository();
var service = new SyncOperationService(repo);
var op = ValidOperation() with { OperationId = Guid.Empty };
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => service.ReceiveAsync(op, Identity()));
}
[Fact]
public async Task Receive_Incoherent_DeviceId_Throws()
{
// Arrange
var repo = new FakeSyncTransferRepository();
var service = new SyncOperationService(repo);
var op = ValidOperation();
// Act & Assert : DeviceId de l'opération != identité authentifiée.
var otherIdentity = Identity() with { DeviceId = "autre-device" };
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => service.ReceiveAsync(op, otherIdentity));
}
[Fact]
public async Task Receive_Duplicate_Operation_Does_Not_Insert_Twice()
{
// Arrange
var repo = new FakeSyncTransferRepository();
var service = new SyncOperationService(repo);
var op = ValidOperation();
// Premier envoi.
await service.ReceiveAsync(op, Identity());
// Le repository signale que l'opération existe déjà.
repo.ExistingState = SyncOperationState.Pending;
// Act : second envoi (dupliqué).
var receipt = await service.ReceiveAsync(op, Identity());
// Assert : RECEIVED sans nouvelle insertion.
Assert.Equal("RECEIVED", receipt.Status);
Assert.Equal(1, repo.InsertCount);
}
[Fact]
public async Task GetStatus_Unknown_Operation_Returns_StatusNotAvailable()
{
// Arrange
var repo = new FakeSyncTransferRepository();
var service = new SyncOperationService(repo);
// Act
var status = await service.GetStatusAsync(Guid.NewGuid(), DeviceId);
// Assert
Assert.Equal(SyncStatus.StatusNotAvailable, status.Status);
}
[Fact]
public async Task GetStatus_Pending_Operation_Returns_Pending()
{
// Arrange
var repo = new FakeSyncTransferRepository
{
Status = new SyncStatusResponse
{
OperationId = Guid.NewGuid(),
Status = SyncStatus.Pending,
DeviceId = DeviceId
}
};
var service = new SyncOperationService(repo);
// Act
var status = await service.GetStatusAsync(repo.Status.OperationId, DeviceId);
// Assert
Assert.Equal(SyncStatus.Pending, status.Status);
}
[Fact]
public async Task GetStatus_Success_Operation_Returns_Success()
{
// Arrange
var repo = new FakeSyncTransferRepository
{
Status = new SyncStatusResponse
{
OperationId = Guid.NewGuid(),
Status = SyncStatus.Success,
DeviceId = DeviceId
}
};
var service = new SyncOperationService(repo);
// Act
var status = await service.GetStatusAsync(repo.Status.OperationId, DeviceId);
// Assert
Assert.Equal(SyncStatus.Success, status.Status);
}
[Fact]
public async Task GetStatus_Rejected_Operation_Returns_Rejected()
{
// Arrange
var repo = new FakeSyncTransferRepository
{
Status = new SyncStatusResponse
{
OperationId = Guid.NewGuid(),
Status = SyncStatus.Rejected,
DeviceId = DeviceId
}
};
var service = new SyncOperationService(repo);
// Act
var status = await service.GetStatusAsync(repo.Status.OperationId, DeviceId);
// Assert
Assert.Equal(SyncStatus.Rejected, status.Status);
}
[Fact]
public async Task GetStatus_Empty_OperationId_Throws()
{
// Arrange
var repo = new FakeSyncTransferRepository();
var service = new SyncOperationService(repo);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => service.GetStatusAsync(Guid.Empty, DeviceId));
}
// ===== Fake =====
private sealed class FakeSyncTransferRepository : ISyncTransferRepository
{
public bool Inserted { get; private set; }
public bool Logged { get; private set; }
public int InsertCount { get; private set; }
public SyncOperationState ExistingState { get; set; } = SyncOperationState.Unknown;
public SyncStatusResponse? Status { get; set; }
public Task<bool> ExistsAsync(Guid operationId, CancellationToken cancellationToken = default)
=> Task.FromResult(ExistingState != SyncOperationState.Unknown);
public Task<SyncOperationState> GetExistingStateAsync(Guid operationId, CancellationToken cancellationToken = default)
=> Task.FromResult(ExistingState);
public Task InsertToSrvAsync(SyncEnvelope envelope, CancellationToken cancellationToken = default)
{
Inserted = true;
InsertCount++;
return Task.CompletedTask;
}
public Task<SyncStatusResponse?> GetStatusAsync(Guid operationId, string deviceId, CancellationToken cancellationToken = default)
=> Task.FromResult(Status);
public Task<SyncLogEntry?> GetTerminalResultAsync(Guid operationId, CancellationToken cancellationToken = default)
=> Task.FromResult<SyncLogEntry?>(null);
public Task WriteLogAsync(SyncLogEntry entry, CancellationToken cancellationToken = default)
{
Logged = true;
return Task.CompletedTask;
}
public Task<int> PurgeLogsAsync(DateTime cutoffUtc, CancellationToken cancellationToken = default)
=> Task.FromResult(0);
public Task<int> TransferToReceptionAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(0);
public Task<IReadOnlyList<SyncEnvelope>> GetPendingOperationsAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<SyncEnvelope>>(Array.Empty<SyncEnvelope>());
public Task MarkSuccessAsync(SyncLogEntry entry, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task MarkRejectedAsync(SyncLogEntry entry, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task IncrementRetryAsync(Guid operationId, int newRetryCount, DateTime nextAttemptUtc, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
}