Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -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")
});
}
}
}