using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Interfaces.Mobile; using Omika.Core.Services.Mobile; namespace Omika.Tests.Mobile; /// /// Tests de la réception et du suivi des opérations de synchronisation. /// 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(() => 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( () => 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(() => 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 ExistsAsync(Guid operationId, CancellationToken cancellationToken = default) => Task.FromResult(ExistingState != SyncOperationState.Unknown); public Task 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 GetStatusAsync(Guid operationId, string deviceId, CancellationToken cancellationToken = default) => Task.FromResult(Status); public Task GetTerminalResultAsync(Guid operationId, CancellationToken cancellationToken = default) => Task.FromResult(null); public Task WriteLogAsync(SyncLogEntry entry, CancellationToken cancellationToken = default) { Logged = true; return Task.CompletedTask; } public Task PurgeLogsAsync(DateTime cutoffUtc, CancellationToken cancellationToken = default) => Task.FromResult(0); public Task TransferToReceptionAsync(CancellationToken cancellationToken = default) => Task.FromResult(0); public Task> GetPendingOperationsAsync(CancellationToken cancellationToken = default) => Task.FromResult>(Array.Empty()); 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; } }