248 lines
8.0 KiB
C#
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;
|
|
}
|
|
}
|