314 lines
12 KiB
C#
314 lines
12 KiB
C#
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")
|
|
});
|
|
}
|
|
}
|
|
}
|