Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
using System.Text.Json;
|
||||
using Omika.Core.Compatibility;
|
||||
using Omika.Core.Contracts.Common;
|
||||
using Omika.Core.Contracts.Mobile.Sync;
|
||||
using Omika.Core.Contracts.Planning;
|
||||
using Omika.Core.Interfaces;
|
||||
using Omika.Core.Interfaces.Mobile;
|
||||
using Omika.Core.Runtime;
|
||||
using Omika.Core.Services.Mobile;
|
||||
using Omika.Worker;
|
||||
using Xunit;
|
||||
|
||||
namespace Omika.Tests.Mobile;
|
||||
|
||||
/// <summary>
|
||||
/// Tests du handler d'opération de synchronisation SuiviTemps.
|
||||
/// Le handler est COMMUN (aucune condition Batik/Omika) : il délègue le
|
||||
/// traitement physique à <see cref="ISuiviTempsService"/> et l'adaptation
|
||||
/// d'identité publique à <see cref="IMobileEntityIdentityResolver"/>.
|
||||
///
|
||||
/// Le modèle métier SuiviTemps ne supporte que la création : UPDATE, ARCHIVE et
|
||||
/// DELETE retournent PERMANENT_FAILURE.
|
||||
/// </summary>
|
||||
[Collection("ApplicationRuntimeContext")]
|
||||
public class SuiviTempsSyncOperationHandlerTests
|
||||
{
|
||||
private static SyncOperation CreateOperation(
|
||||
SyncOperationType type,
|
||||
Guid? entityGuid = null,
|
||||
string? payload = null)
|
||||
{
|
||||
return new SyncOperation
|
||||
{
|
||||
OperationId = Guid.NewGuid(),
|
||||
DeviceId = "device-1",
|
||||
EntityType = SyncEntityTypes.SuiviTemps,
|
||||
OperationType = type,
|
||||
EntityGuid = entityGuid ?? Guid.NewGuid(),
|
||||
Payload = payload ?? ValidPayload()
|
||||
};
|
||||
}
|
||||
|
||||
private static string ValidPayload() => JsonSerializer.Serialize(new SuiviTempsSyncPayload
|
||||
{
|
||||
CollaborateurId = "11111111-1111-1111-1111-111111111111",
|
||||
InterventionId = "22222222-2222-2222-2222-222222222222",
|
||||
RendezVousId = "33333333-3333-3333-3333-333333333333",
|
||||
Date = new DateTime(2026, 8, 14, 9, 0, 0),
|
||||
DureeMinutes = 60,
|
||||
TypeTempsKey = "TYPE-1",
|
||||
Description = "Saisie de temps test"
|
||||
});
|
||||
|
||||
private static SuiviTempsSyncOperationHandler CreateHandler(
|
||||
ISuiviTempsService service,
|
||||
IMobileEntityIdentityResolver? resolver = null)
|
||||
=> new(service, resolver ?? new MobileEntityIdentityResolver());
|
||||
|
||||
[Fact]
|
||||
public async Task Create_ValidPayload_ReturnsSuccessWithCanonicalGuid()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var requestedGuid = Guid.NewGuid();
|
||||
var physicalGuid = Guid.NewGuid();
|
||||
var fake = new FakeSuiviTempsService
|
||||
{
|
||||
ExistingById = null,
|
||||
CreatedId = physicalGuid.ToString("D")
|
||||
};
|
||||
var handler = CreateHandler(fake);
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Create, requestedGuid, ValidPayload()));
|
||||
|
||||
// Succès, service appelé UNE fois.
|
||||
Assert.Equal(SyncOperationResultKind.Success, result.Kind);
|
||||
Assert.Equal(1, fake.CreateCallCount);
|
||||
Assert.Equal(physicalGuid.ToString("D"), result.CreatedEntityId);
|
||||
|
||||
// CAS B : le contrat de création ne porte pas de GUID métier, le
|
||||
// repository génère le GUID physique. Requested et Canonical diffèrent.
|
||||
Assert.Equal(requestedGuid, result.RequestedEntityGuid);
|
||||
Assert.Equal(physicalGuid, result.CanonicalEntityGuid);
|
||||
Assert.NotEqual(requestedGuid, result.CanonicalEntityGuid);
|
||||
|
||||
// Le payload est mappé vers le vrai contrat métier.
|
||||
Assert.Equal("11111111-1111-1111-1111-111111111111", fake.LastCreateRequest?.CollaborateurId);
|
||||
Assert.Equal("22222222-2222-2222-2222-222222222222", fake.LastCreateRequest?.InterventionId);
|
||||
Assert.Equal("33333333-3333-3333-3333-333333333333", fake.LastCreateRequest?.RendezVousId);
|
||||
Assert.Equal(60, fake.LastCreateRequest?.DureeMinutes);
|
||||
Assert.Equal("TYPE-1", fake.LastCreateRequest?.TypeTemps?.Key);
|
||||
Assert.Equal("Saisie de temps test", fake.LastCreateRequest?.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_AlreadyExists_IsIdempotent()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var entityGuid = Guid.NewGuid();
|
||||
var fake = new FakeSuiviTempsService
|
||||
{
|
||||
ExistingById = new SuiviTempsDto { Id = entityGuid.ToString("D") }
|
||||
};
|
||||
var handler = CreateHandler(fake);
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Create, entityGuid, ValidPayload()));
|
||||
|
||||
// Déjà créée : SUCCESS sans recréer.
|
||||
Assert.Equal(SyncOperationResultKind.Success, result.Kind);
|
||||
Assert.Equal(0, fake.CreateCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_InvalidPayload_ReturnsPermanentFailure()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var handler = CreateHandler(new FakeSuiviTempsService());
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Create, payload: "not-json"));
|
||||
|
||||
Assert.Equal(SyncOperationResultKind.PermanentFailure, result.Kind);
|
||||
Assert.Equal("SUIVI_TEMPS_INVALID_PAYLOAD", result.ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_EmptyEntityGuid_ReturnsPermanentFailure()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var handler = CreateHandler(new FakeSuiviTempsService());
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Create, Guid.Empty, ValidPayload()));
|
||||
|
||||
Assert.Equal(SyncOperationResultKind.PermanentFailure, result.Kind);
|
||||
Assert.Equal("SUIVI_TEMPS_INVALID_ENTITY_GUID", result.ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_ReturnsPermanentFailure_AndDoesNotCreate()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var fake = new FakeSuiviTempsService();
|
||||
var handler = CreateHandler(fake);
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Update, payload: ValidPayload()));
|
||||
|
||||
Assert.Equal(SyncOperationResultKind.PermanentFailure, result.Kind);
|
||||
Assert.Equal("SUIVI_TEMPS_OPERATION_NOT_SUPPORTED", result.ErrorCode);
|
||||
Assert.Equal(0, fake.CreateCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Archive_ReturnsPermanentFailure_AndDoesNotCreate()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var fake = new FakeSuiviTempsService();
|
||||
var handler = CreateHandler(fake);
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Archive, payload: ValidPayload()));
|
||||
|
||||
Assert.Equal(SyncOperationResultKind.PermanentFailure, result.Kind);
|
||||
Assert.Equal("SUIVI_TEMPS_OPERATION_NOT_SUPPORTED", result.ErrorCode);
|
||||
Assert.Equal(0, fake.CreateCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_ReturnsPermanentFailure_AndDoesNotCreate()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var fake = new FakeSuiviTempsService();
|
||||
var handler = CreateHandler(fake);
|
||||
|
||||
SyncOperationResult result = await handler.ApplyAsync(
|
||||
CreateOperation(SyncOperationType.Delete, payload: ValidPayload()));
|
||||
|
||||
Assert.Equal(SyncOperationResultKind.PermanentFailure, result.Kind);
|
||||
Assert.Equal("SUIVI_TEMPS_OPERATION_NOT_SUPPORTED", result.ErrorCode);
|
||||
Assert.Equal(0, fake.CreateCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportedEntityTypes_ContainsSuiviTemps()
|
||||
{
|
||||
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
|
||||
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
|
||||
|
||||
var handler = CreateHandler(new FakeSuiviTempsService());
|
||||
|
||||
Assert.Contains(SyncEntityTypes.SuiviTemps, handler.SupportedEntityTypes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_DependsOnlyOnCommonAbstractions()
|
||||
{
|
||||
// Le handler public ne doit dépendre que du service métier commun et du
|
||||
// résolveur d'identité. Aucune dépendance vers un repository physique
|
||||
// Batik/Omika.
|
||||
var ctor = typeof(SuiviTempsSyncOperationHandler)
|
||||
.GetConstructors()
|
||||
.Single();
|
||||
|
||||
Assert.Equal(2, ctor.GetParameters().Length);
|
||||
Assert.Equal(typeof(ISuiviTempsService), ctor.GetParameters()[0].ParameterType);
|
||||
Assert.Equal(typeof(IMobileEntityIdentityResolver), ctor.GetParameters()[1].ParameterType);
|
||||
}
|
||||
|
||||
// --- Payload sérialisation / désérialisation JSON -------------------------
|
||||
|
||||
[Fact]
|
||||
public void Payload_RoundTrips_ThroughJson()
|
||||
{
|
||||
var payload = new SuiviTempsSyncPayload
|
||||
{
|
||||
CollaborateurId = "11111111-1111-1111-1111-111111111111",
|
||||
InterventionId = "22222222-2222-2222-2222-222222222222",
|
||||
RendezVousId = "33333333-3333-3333-3333-333333333333",
|
||||
Date = new DateTime(2026, 8, 14, 9, 0, 0),
|
||||
DureeMinutes = 45,
|
||||
TypeTempsKey = "TYPE-2",
|
||||
Description = "Libellé"
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(payload);
|
||||
SuiviTempsSyncPayload? roundTripped = JsonSerializer.Deserialize<SuiviTempsSyncPayload>(
|
||||
json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
Assert.NotNull(roundTripped);
|
||||
Assert.Equal(payload.CollaborateurId, roundTripped.CollaborateurId);
|
||||
Assert.Equal(payload.InterventionId, roundTripped.InterventionId);
|
||||
Assert.Equal(payload.RendezVousId, roundTripped.RendezVousId);
|
||||
Assert.Equal(payload.Date, roundTripped.Date);
|
||||
Assert.Equal(payload.DureeMinutes, roundTripped.DureeMinutes);
|
||||
Assert.Equal(payload.TypeTempsKey, roundTripped.TypeTempsKey);
|
||||
Assert.Equal(payload.Description, roundTripped.Description);
|
||||
}
|
||||
|
||||
// --- Registry -------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Registry_MapsSuiviTempsToSuiviTempsHandler()
|
||||
{
|
||||
var registry = new SyncOperationHandlerRegistry(new ISyncOperationHandler[]
|
||||
{
|
||||
new SuiviTempsSyncOperationHandler(new FakeSuiviTempsService(), new MobileEntityIdentityResolver())
|
||||
});
|
||||
|
||||
ISyncOperationHandler? handler = registry.GetHandler(SyncEntityTypes.SuiviTemps);
|
||||
|
||||
Assert.NotNull(handler);
|
||||
Assert.IsType<SuiviTempsSyncOperationHandler>(handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_UnknownEntityType_ReturnsNull()
|
||||
{
|
||||
var registry = new SyncOperationHandlerRegistry(new ISyncOperationHandler[]
|
||||
{
|
||||
new SuiviTempsSyncOperationHandler(new FakeSuiviTempsService(), new MobileEntityIdentityResolver())
|
||||
});
|
||||
|
||||
Assert.Null(registry.GetHandler("UnknownType"));
|
||||
}
|
||||
|
||||
private sealed class FakeSuiviTempsService : ISuiviTempsService
|
||||
{
|
||||
public SuiviTempsDto? ExistingById { get; set; }
|
||||
public string? CreatedId { get; set; }
|
||||
|
||||
public CreateSuiviTempsRequest? LastCreateRequest { get; private set; }
|
||||
public int CreateCallCount { get; private set; }
|
||||
|
||||
public Task<PagedResult<SuiviTempsDto>> SearchAsync(
|
||||
SuiviTempsSearchCriteria criteria,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new PagedResult<SuiviTempsDto>([], 1, 10, 0, 0));
|
||||
|
||||
public Task<SuiviTempsDto?> GetByIdAsync(
|
||||
string id,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(ExistingById);
|
||||
|
||||
public Task<SuiviTempsDto> CreateAsync(
|
||||
CreateSuiviTempsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
CreateCallCount++;
|
||||
LastCreateRequest = request;
|
||||
return Task.FromResult(new SuiviTempsDto
|
||||
{
|
||||
Id = CreatedId ?? Guid.NewGuid().ToString("D")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user