using System.Data.Common;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Omika.Core.Compatibility;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Planning;
using Omika.Core.Domain.Commands;
using Omika.Core.Domain.Entities.Planning;
using Omika.Core.Interfaces;
using Omika.Core.Runtime;
using Omika.Core.Services;
using Omika.Data;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.Repositories;
using Omika.Data.Repositories.Batik;
using Omika.Data.Repositories.Omika;
namespace Omika.Tests.Repositories;
///
/// Tests unitaires de : délégation,
/// comportement Unknown, mapping Batik/Omika, délégation du service, et DI.
///
[Collection("ApplicationRuntimeContext")]
public class AdaptiveSuiviTempsRepositoryTests : IDisposable
{
public AdaptiveSuiviTempsRepositoryTests()
{
ApplicationRuntimeContext.Reset();
}
public void Dispose()
{
ApplicationRuntimeContext.Reset();
}
// =========================================================================
// DELEGATION SEARCH
// =========================================================================
[Fact]
public async Task SearchAsync_BatikDetected_DelegatesToBatikRepository()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")];
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
await adaptive.SearchAsync(new SuiviTempsSearchCriteria());
Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes.");
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task SearchAsync_OmikaDetected_DelegatesToOmikaRepository()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")];
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
await adaptive.SearchAsync(new SuiviTempsSearchCriteria());
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
[Fact]
public async Task SearchAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.SearchAsync(new SuiviTempsSearchCriteria()));
Assert.Contains("not been detected", ex.Message);
Assert.Equal(0, batikConnection.QueryCount);
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task SearchAsync_EmptyCriteria_DoesNotCrash()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.SuiviTempsRows = [];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
PagedResult result = await adaptive.SearchAsync(new SuiviTempsSearchCriteria());
Assert.NotNull(result);
Assert.Empty(result.Items);
}
// =========================================================================
// MAPPING BATIK
// =========================================================================
[Fact]
public async Task SearchAsync_BatikMapping_ProducesCanonicalSuiviTemps()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
PagedResult result = await adaptive.SearchAsync(new SuiviTempsSearchCriteria());
Assert.Single(result.Items);
Assert.Equal("Réunion", result.Items[0].Description);
}
// =========================================================================
// MAPPING OMIKA
// =========================================================================
[Fact]
public async Task SearchAsync_OmikaMapping_ProducesCanonicalSuiviTemps()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")];
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
PagedResult result = await adaptive.SearchAsync(new SuiviTempsSearchCriteria());
Assert.Single(result.Items);
Assert.Equal("Réunion", result.Items[0].Description);
}
// =========================================================================
// DELEGATION GET BY ID
// =========================================================================
[Fact]
public async Task GetByIdAsync_BatikDetected_DelegatesToBatik()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
SuiviTemps? result = await adaptive.GetByIdAsync(Guid.NewGuid().ToString("D"));
Assert.NotNull(result);
Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes.");
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task GetByIdAsync_OmikaDetected_DelegatesToOmika()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")];
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
SuiviTemps? result = await adaptive.GetByIdAsync(Guid.NewGuid().ToString("D"));
Assert.NotNull(result);
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
[Fact]
public async Task GetByIdAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.GetByIdAsync(Guid.NewGuid().ToString("D")));
Assert.Contains("not been detected", ex.Message);
Assert.Equal(0, batikConnection.QueryCount);
Assert.Equal(0, omikaConnection.QueryCount);
}
// =========================================================================
// DELEGATION CREATE
// =========================================================================
[Fact]
public async Task CreateAsync_BatikDetected_DelegatesToBatik()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
SuiviTemps result = await adaptive.CreateAsync(
new SuiviTempsCreateCommand { Date = DateTime.Now, DureeMinutes = 60 },
CancellationToken.None);
Assert.NotNull(result);
Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes.");
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task CreateAsync_OmikaDetected_DelegatesToOmika()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")];
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
SuiviTemps result = await adaptive.CreateAsync(
new SuiviTempsCreateCommand { Date = DateTime.Now, DureeMinutes = 60 },
CancellationToken.None);
Assert.NotNull(result);
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
[Fact]
public async Task CreateAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikSuiviTempsRepository(batikConnection);
var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection);
var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.CreateAsync(
new SuiviTempsCreateCommand { Date = DateTime.Now, DureeMinutes = 60 }));
Assert.Contains("not been detected", ex.Message);
Assert.Equal(0, batikConnection.QueryCount);
Assert.Equal(0, omikaConnection.QueryCount);
}
// =========================================================================
// SERVICE DELEGATION
// =========================================================================
[Fact]
public async Task Service_SearchAsync_DelegatesToRepository()
{
var repo = new FakeSuiviTempsRepository();
var service = new SuiviTempsService(repo);
PagedResult result = await service.SearchAsync(new SuiviTempsSearchCriteria());
Assert.True(repo.SearchCalled);
Assert.Empty(result.Items);
}
[Fact]
public async Task Service_GetByIdAsync_DelegatesToRepository()
{
var repo = new FakeSuiviTempsRepository();
var service = new SuiviTempsService(repo);
SuiviTempsDto? result = await service.GetByIdAsync("abc");
Assert.True(repo.GetByIdCalled);
Assert.Null(result);
}
[Fact]
public async Task Service_CreateAsync_DelegatesToRepository()
{
var repo = new FakeSuiviTempsRepository();
var service = new SuiviTempsService(repo);
SuiviTempsDto result = await service.CreateAsync(
new CreateSuiviTempsRequest { Date = DateTime.Now, DureeMinutes = 60 });
Assert.True(repo.CreateCalled);
Assert.NotNull(result);
}
// =========================================================================
// DI TESTS
// =========================================================================
[Fact]
public void AddOmikaData_RegistersBatikSuiviTempsRepositoryAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(BatikSuiviTempsRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_RegistersOmikaSuiviTempsRepositoryAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(OmikaSuiviTempsRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_RegistersISuiviTempsRepositoryAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_ISuiviTempsRepositoryPointsToAdaptiveSuiviTempsRepository()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsRepository));
Assert.Equal(typeof(AdaptiveSuiviTempsRepository), descriptor.ImplementationType);
}
[Fact]
public void AddOmikaData_RegistersISuiviTempsServiceAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsService));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_ISuiviTempsServicePointsToSuiviTempsService()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsService));
Assert.Equal(typeof(SuiviTempsService), descriptor.ImplementationType);
}
[Fact]
public void AddOmikaData_OnlyOneISuiviTempsRepositoryRegistered()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptors = services.Where(d => d.ServiceType == typeof(ISuiviTempsRepository)).ToList();
Assert.Single(descriptors);
}
[Fact]
public async Task AddOmikaData_ResolvesAllSuiviTempsRepositories_WithFakeConnection()
{
var services = new ServiceCollection();
services.AddSingleton(new ConfigurationBuilder().Build());
services.AddOmikaData();
var oldDescriptor = services.Single(d => d.ServiceType == typeof(IHfsqlConnection));
services.Remove(oldDescriptor);
services.AddScoped();
var provider = services.BuildServiceProvider();
await using var scope = provider.CreateAsyncScope();
var suiviTempsRepo = scope.ServiceProvider.GetRequiredService();
Assert.IsType(suiviTempsRepo);
var batikRepo = scope.ServiceProvider.GetRequiredService();
Assert.NotNull(batikRepo);
var omikaRepo = scope.ServiceProvider.GetRequiredService();
Assert.NotNull(omikaRepo);
var service = scope.ServiceProvider.GetRequiredService();
Assert.IsType(service);
}
// =========================================================================
// ARCHITECTURE
// =========================================================================
[Fact]
public void Repository_ImplementsISuiviTempsRepository()
{
Assert.True(typeof(ISuiviTempsRepository).IsAssignableFrom(typeof(AdaptiveSuiviTempsRepository)));
}
[Fact]
public void Repository_ContainsNoSql()
{
string source = ReadSourceFile(typeof(AdaptiveSuiviTempsRepository));
Assert.DoesNotContain("SELECT ", source, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("FROM ", source, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("WHERE ", source, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Repository_DoesNotDependOnHfsqlConnection()
{
var type = typeof(AdaptiveSuiviTempsRepository);
bool hasHfsqlConnectionCtor = type.GetConstructors()
.Any(c => c.GetParameters()
.Any(p => typeof(IHfsqlConnection).IsAssignableFrom(p.ParameterType)));
Assert.False(hasHfsqlConnectionCtor,
"AdaptiveSuiviTempsRepository ne doit pas dépendre directement de IHfsqlConnection.");
}
// =========================================================================
// HELPERS
// =========================================================================
private static string ReadSourceFile(Type type)
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
var dir = new DirectoryInfo(baseDir);
while (dir != null && !File.Exists(Path.Combine(dir.FullName, "Omika.slnx")))
dir = dir.Parent;
string root = dir?.FullName ?? baseDir;
return File.ReadAllText(Path.Combine(root, "src", "Omika.Data", "Repositories", type.Name + ".cs"));
}
// --- Dictionnaires de lignes physiques ----------------------------------
private static Dictionary BatikSuiviTempsDict(
long pkid, string? libelle, string? reference)
=> new()
{
["SDT_PKID"] = pkid,
["SDT_DateDebut"] = new DateTime(2026, 1, 15, 9, 0, 0),
["SDT_IDIntervention"] = 0L,
["SDT_IDCollaborateur"] = 0L,
["SDT_IDPartenaire"] = 0L,
["SDT_NbMinutes"] = 60,
["SDT_Libelle"] = libelle,
["SDT_IDTypeTemps"] = 0L,
["SDT_IDRdv"] = 0L,
["SDT_Ponderation"] = 0L,
["SDT_IDImport"] = 0L,
["SDT_DateHeureFin"] = new DateTime(2026, 1, 15, 10, 0, 0),
["GUID"] = Guid.NewGuid().ToString("D"),
["SDT_IDTypeRdv"] = 0L,
["SDT_Controle"] = false,
["SDT_IDGroupe"] = 0L,
["CreationDate"] = new DateTime(2026, 1, 15, 9, 0, 0),
["CreationUser"] = 0L,
["ModifDate"] = new DateTime(2026, 1, 15, 9, 0, 0),
["ModifUser"] = 0L
};
private static Dictionary OmikaSuiviTempsDict(
long pkid, string? libelle, string? reference)
=> new()
{
["SDT_PKID"] = pkid,
["SDT_DateDebut"] = new DateTime(2026, 1, 15, 9, 0, 0),
["SDT_IDIntervention"] = 0L,
["SDT_IDCollaborateur"] = 0L,
["SDT_IDPartenaire"] = 0L,
["SDT_NbMinutes"] = 60,
["SDT_Libelle"] = libelle,
["SDT_IDTypeTemps"] = 0L,
["SDT_IDRdv"] = 0L,
["SDT_Ponderation"] = 0L,
["SDT_IDImport"] = 0L,
["SDT_DateHeureFin"] = new DateTime(2026, 1, 15, 10, 0, 0),
["GUID"] = Guid.NewGuid().ToString("D"),
["SDT_IDTypeRdv"] = 0L,
["SDT_Controle"] = false,
["SDT_IDGroupe"] = 0L,
["CreationDate"] = new DateTime(2026, 1, 15, 9, 0, 0),
["CreationUser"] = 0L,
["ModifDate"] = new DateTime(2026, 1, 15, 9, 0, 0),
["ModifUser"] = 0L,
["SDT_CleType"] = null,
["SDT_CleTypeRDV"] = null,
["SDT_NoteColb"] = null,
["SDT_NoteAdmin"] = null,
["SDT_GUID_Rdv_Colb"] = null
};
// =========================================================================
// FakeSuiviTempsRepository (service)
// =========================================================================
private sealed class FakeSuiviTempsRepository : ISuiviTempsRepository
{
public bool SearchCalled { get; private set; }
public bool GetByIdCalled { get; private set; }
public bool CreateCalled { get; private set; }
public Task> SearchAsync(
SuiviTempsSearchCriteria criteria, CancellationToken cancellationToken = default)
{
SearchCalled = true;
return Task.FromResult(new PagedResult([], 1, 50, 0, 0));
}
public Task GetByIdAsync(
string id, CancellationToken cancellationToken = default)
{
GetByIdCalled = true;
return Task.FromResult(null);
}
public Task CreateAsync(
SuiviTempsCreateCommand command, CancellationToken cancellationToken = default)
{
CreateCalled = true;
return Task.FromResult(new SuiviTemps
{
Id = Guid.NewGuid(),
Date = command.Date,
DureeMinutes = command.DureeMinutes
});
}
}
// =========================================================================
// SpyingHfsqlConnection
// =========================================================================
private sealed class SpyingHfsqlConnection : IHfsqlConnection, IHfsqlWriteConnection
{
public HfsqlOptions Options => new();
public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed;
public DbTransaction? ActiveTransaction => null;
public int QueryCount { get; private set; }
public bool ThrowOnQuery { get; set; }
public string? LastSql { get; private set; }
public IReadOnlyList