using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Omika.Core.Compatibility;
using Omika.Core.Contracts.Clients;
using Omika.Core.Contracts.Common;
using Omika.Core.Domain.Commands;
using Omika.Core.Domain.Entities.Clients;
using Omika.Core.Interfaces;
using Omika.Core.Runtime;
using Omika.Data;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.HfSql.Metadata;
using Omika.Data.Repositories;
using Omika.Data.Repositories.Batik;
using Omika.Data.Repositories.Omika;
using Omika.Data.SchemaDetection;
namespace Omika.Tests.Repositories;
///
/// Tests unitaires de : délégation,
/// comportement Unknown, propagation d'erreurs, architecture, et DI.
///
/// Les tests qui manipulent le contexte statique global
/// sont exécutés dans la collection
/// "ApplicationRuntimeContext" qui sérialise leur exécution.
///
/// Stratégie : les repositories Batik/Omika sont des instances réelles (sealed),
/// connectées à un qui enregistre les SQL.
/// On distingue le repository appelé par les colonnes caractéristiques dans le SQL
/// (Omika inclut GUID, CL_CleType ; Batik non).
///
[Collection("ApplicationRuntimeContext")]
public class AdaptiveClientRepositoryTests : IDisposable
{
public AdaptiveClientRepositoryTests()
{
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();
// Donner assez de données pour que le pipeline SearchAsync fonctionne.
batikConnection.ClientRows = [BatikClientDict(1, "Test", "C001")];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.SearchAsync(new ClientSearchCriteria { Nom = "Test" });
// Batik a exécuté des requêtes SQL, Omika aucune.
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.ClientRows = [OmikaClientDict(1, "Test", "C001")];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.SearchAsync(new ClientSearchCriteria { Nom = "Test" });
// Omika a exécuté des requêtes SQL, Batik aucune.
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
// =========================================================================
// DELEGATION CREATE
// =========================================================================
[Fact]
public async Task CreateAsync_BatikDetected_DelegatesToBatikRepository()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
// Le fake ne fournit pas d'identité auto-générée : le pipeline Create
// lève en fin de parcours, mais la délégation vers Batik a bien eu lieu.
await Assert.ThrowsAsync(() =>
adaptive.CreateAsync(new ClientCreateCommand { Nom = "Test" }));
// Batik a exécuté des requêtes SQL (INSERT + référentiels), Omika aucune.
Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes.");
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task CreateAsync_OmikaDetected_DelegatesToOmikaRepository()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await Assert.ThrowsAsync(() =>
adaptive.CreateAsync(new ClientCreateCommand { Nom = "Test" }));
// Omika a exécuté des requêtes SQL (INSERT + référentiels), Batik aucune.
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
[Fact]
public async Task SearchAsync_BatikDetected_UsesBatikingSqlColumns()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = [BatikClientDict(1, "Test", "C001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.SearchAsync(new ClientSearchCriteria());
// Le SQL Batik contient CL_RaisonBlocage (majuscule L) mais pas GUID.
string clientSql = batikConnection.AllSql.First(s => s.Contains("FROM Client", StringComparison.OrdinalIgnoreCase));
Assert.Contains("CL_RaisonBlocage", clientSql);
Assert.DoesNotContain("GUID", clientSql);
Assert.DoesNotContain("CL_CleType", clientSql);
}
[Fact]
public async Task SearchAsync_OmikaDetected_UsesOmikaSqlColumns()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.ClientRows = [OmikaClientDict(1, "Test", "C001")];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.SearchAsync(new ClientSearchCriteria());
// Le SQL Omika contient GUID, CL_CleType (colonnes spécifiques Omika).
string clientSql = omikaConnection.AllSql.First(s => s.Contains("FROM Client", StringComparison.OrdinalIgnoreCase));
Assert.Contains("GUID", clientSql);
Assert.Contains("CL_CleType", clientSql);
}
[Fact]
public async Task SearchAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
// DetectedVersion reste Unknown.
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.SearchAsync(new ClientSearchCriteria()));
Assert.Contains("not been detected", ex.Message);
// Aucun repository n'a été contacté.
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.ClientRows = [];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
PagedResult result = await adaptive.SearchAsync(new ClientSearchCriteria());
Assert.NotNull(result);
Assert.Empty(result.Items);
}
// =========================================================================
// DELEGATION DETAIL
// =========================================================================
[Fact]
public async Task GetDetailsAsync_BatikDetected_DelegatesToBatik()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = [BatikClientDict(42, "Dupont", "C001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
Client? result = await adaptive.GetDetailsAsync("42");
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 GetDetailsAsync_OmikaDetected_DelegatesToOmika()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.ClientRows = [OmikaClientDict(42, "Dupont", "C001")];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
Client? result = await adaptive.GetDetailsAsync("42");
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 GetDetailsAsync_BatikDetected_UsesClPkidQuery()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = [BatikClientDict(42, "Dupont", "C001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.GetDetailsAsync("42");
// Batik interroge par CL_PKID = ? (pas de GUID).
string clientSql = batikConnection.AllSql.First(s => s.Contains("FROM Client", StringComparison.OrdinalIgnoreCase));
Assert.Contains("CL_PKID", clientSql);
Assert.DoesNotContain("GUID", clientSql);
}
[Fact]
public async Task GetDetailsAsync_OmikaDetected_UsesGuidOrClPkidQuery()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var guid = Guid.NewGuid();
omikaConnection.ClientRows = [OmikaClientDict(42, "Dupont", "C001", guid: guid.ToString("D"))];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.GetDetailsAsync(guid.ToString("D"));
// Omika interroge par GUID = ?.
string clientSql = omikaConnection.AllSql.First(s => s.Contains("FROM Client", StringComparison.OrdinalIgnoreCase));
Assert.Contains("GUID", clientSql);
}
[Fact]
public async Task GetDetailsAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.GetDetailsAsync("42"));
Assert.Contains("not been detected", ex.Message);
Assert.Equal(0, batikConnection.QueryCount);
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task GetDetailsAsync_NullFromRepository_ReturnsNull()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = []; // Aucun client → null.
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
Client? result = await adaptive.GetDetailsAsync("999");
Assert.Null(result);
}
[Fact]
public async Task GetDetailsAsync_CancellationTokenTransmitted()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.ClientRows = [OmikaClientDict(42, "Dupont", "C001")];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
using var cts = new CancellationTokenSource();
await adaptive.GetDetailsAsync("42", cts.Token);
Assert.Equal(cts.Token, omikaConnection.LastCancellationToken);
}
// =========================================================================
// PROPAGATION D'ERREURS
// =========================================================================
[Fact]
public async Task SearchAsync_BatikInfrastructureError_Propagated()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection { ThrowOnQuery = true };
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await Assert.ThrowsAsync(
() => adaptive.SearchAsync(new ClientSearchCriteria()));
}
[Fact]
public async Task GetDetailsAsync_OmikaInfrastructureError_Propagated()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection { ThrowOnQuery = true };
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await Assert.ThrowsAsync(
() => adaptive.GetDetailsAsync("42"));
}
// =========================================================================
// DI TESTS
// =========================================================================
[Fact]
public void AddOmikaData_RegistersBatikClientRepositoryAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(BatikClientRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_RegistersOmikaClientRepositoryAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(OmikaClientRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_RegistersIClientRepositoryAsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(IClientRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_IClientRepositoryPointsToAdaptiveClientRepository()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(IClientRepository));
Assert.Equal(typeof(AdaptiveClientRepository), descriptor.ImplementationType);
}
[Fact]
public void AddOmikaData_BatikRepositoryDoesNotDependOnIClientRepository()
{
// BatikClientRepository doit être enregistré par son type concret,
// sans dépendre de IClientRepository (pas de résolution récursive).
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(BatikClientRepository));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
// ImplementationFactory est null car BatikClientRepository est enregistré
// par type concret, pas via une factory qui résoudrait IClientRepository.
Assert.Null(descriptor.ImplementationFactory);
}
[Fact]
public void AddOmikaData_PhysicalRepositoriesRegisteredIndividually()
{
var services = new ServiceCollection();
services.AddOmikaData();
// Les repositories physiques doivent être enregistrés par leur type concret.
var batikDescriptor = services.Single(d => d.ServiceType == typeof(BatikClientRepository));
var omikaDescriptor = services.Single(d => d.ServiceType == typeof(OmikaClientRepository));
Assert.Equal(ServiceLifetime.Scoped, batikDescriptor.Lifetime);
Assert.Equal(ServiceLifetime.Scoped, omikaDescriptor.Lifetime);
// Les types d'implémentation correspondent aux repositories concrets.
Assert.Equal(typeof(BatikClientRepository), batikDescriptor.ImplementationType);
Assert.Equal(typeof(OmikaClientRepository), omikaDescriptor.ImplementationType);
}
[Fact]
public void AddOmikaData_HfsqlConnectionRemainsScoped()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptor = services.Single(d => d.ServiceType == typeof(IHfsqlConnection));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
}
[Fact]
public void AddOmikaData_OnlyOneIClientRepositoryRegistered()
{
var services = new ServiceCollection();
services.AddOmikaData();
var descriptors = services.Where(d => d.ServiceType == typeof(IClientRepository)).ToList();
Assert.Single(descriptors);
}
[Fact]
public void AddOmikaData_ResolvesAllClientRepositories_WithFakeConnection()
{
// Construit le graphe DI réel : remplace IHfsqlConnection par un fake
// mémoire pour éviter toute connexion ODBC/HFSQL réelle.
var services = new ServiceCollection();
// Configuration minimale (vide) pour satisfaire IConfiguration requis
// par AddOptions().Configure(...).
services.AddSingleton(new ConfigurationBuilder().Build());
services.AddOmikaData();
// Remplacer IHfsqlConnection → HfsqlConnection par un fake Scoped.
var oldDescriptor = services.Single(d => d.ServiceType == typeof(IHfsqlConnection));
services.Remove(oldDescriptor);
services.AddScoped();
var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
// IClientRepository doit se résoudre en AdaptiveClientRepository.
var clientRepo = scope.ServiceProvider.GetRequiredService();
Assert.IsType(clientRepo);
// Les repositories physiques sont résolvables individuellement.
var batikRepo = scope.ServiceProvider.GetRequiredService();
Assert.NotNull(batikRepo);
var omikaRepo = scope.ServiceProvider.GetRequiredService();
Assert.NotNull(omikaRepo);
}
[Fact]
public void AddOmikaData_IClientRepositoryIsScoped()
{
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();
// Même scope → même instance.
using var scope1 = provider.CreateScope();
var repoA = scope1.ServiceProvider.GetRequiredService();
var repoB = scope1.ServiceProvider.GetRequiredService();
Assert.Same(repoA, repoB);
// Scope différent → instance différente.
using var scope2 = provider.CreateScope();
var repoC = scope2.ServiceProvider.GetRequiredService();
Assert.NotSame(repoA, repoC);
}
// =========================================================================
// DELEGATION UPDATE
// =========================================================================
[Fact]
public async Task UpdateAsync_BatikDetected_DelegatesToBatik()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = [BatikClientDict(42, "Dupont", "C001")];
batikConnection.ParametreMetierRows = [];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.UpdateAsync("42", new ClientUpdateCommand { Nom = "Updated" },
CancellationToken.None);
Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes.");
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task UpdateAsync_OmikaDetected_DelegatesToOmika()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.ClientRows = [OmikaClientDict(42, "Dupont", "C001")];
omikaConnection.ParametreMetierRows = [];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
await adaptive.UpdateAsync("42", new ClientUpdateCommand { Nom = "Updated" },
CancellationToken.None);
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
[Fact]
public async Task UpdateAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
// DetectedVersion reste Unknown.
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.UpdateAsync("42", new ClientUpdateCommand()));
Assert.Contains("not been detected", ex.Message);
Assert.Equal(0, batikConnection.QueryCount);
Assert.Equal(0, omikaConnection.QueryCount);
}
// =========================================================================
// DELEGATION ARCHIVE
// =========================================================================
[Fact]
public async Task ArchiveAsync_BatikDetected_Existing_ReturnsTrue()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = [BatikClientDict(42, "Dupont", "C001")];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
bool result = await adaptive.ArchiveAsync("42");
Assert.True(result);
Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes.");
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task ArchiveAsync_BatikDetected_Absent_ReturnsFalse()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik);
var batikConnection = new SpyingHfsqlConnection();
batikConnection.ClientRows = [];
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
bool result = await adaptive.ArchiveAsync("42");
Assert.False(result);
Assert.Equal(0, omikaConnection.QueryCount);
}
[Fact]
public async Task ArchiveAsync_OmikaDetected_Existing_ReturnsTrue()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.ClientRows = [OmikaClientDict(42, "Dupont", "C001")];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
bool result = await adaptive.ArchiveAsync("42");
Assert.True(result);
Assert.Equal(0, batikConnection.QueryCount);
Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes.");
}
[Fact]
public async Task ArchiveAsync_OmikaDetected_Absent_ReturnsFalse()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
omikaConnection.ClientRows = [];
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
bool result = await adaptive.ArchiveAsync("42");
Assert.False(result);
Assert.Equal(0, batikConnection.QueryCount);
}
[Fact]
public async Task ArchiveAsync_UnknownDetected_ThrowsInvalidOperationException()
{
ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto);
var batikConnection = new SpyingHfsqlConnection();
var omikaConnection = new SpyingHfsqlConnection();
var batikRepo = new BatikClientRepository(batikConnection);
var omikaRepo = new OmikaClientRepository(omikaConnection);
var adaptive = new AdaptiveClientRepository(batikRepo, omikaRepo);
var ex = await Assert.ThrowsAsync(
() => adaptive.ArchiveAsync("42"));
Assert.Contains("not been detected", ex.Message);
Assert.Equal(0, batikConnection.QueryCount);
Assert.Equal(0, omikaConnection.QueryCount);
}
// =========================================================================
// ARCHITECTURE
// =========================================================================
[Fact]
public void Repository_ImplementsIClientRepository()
{
Assert.True(typeof(IClientRepository).IsAssignableFrom(typeof(AdaptiveClientRepository)));
}
[Fact]
public void Repository_ContainsNoSql()
{
string source = ReadSourceFile(typeof(AdaptiveClientRepository));
Assert.DoesNotContain("SELECT ", source, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("FROM ", source, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("WHERE ", source, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Repository_ContainsNoOdbcTypes()
{
var type = typeof(AdaptiveClientRepository);
var referenced = GetPublicSurfaceTypes(type);
Assert.DoesNotContain(referenced, t =>
t.Namespace is not null && t.Namespace.StartsWith("System.Data.Odbc", StringComparison.Ordinal));
}
[Fact]
public void Repository_ContainsNoGuidOrLongParsing()
{
string source = ReadSourceFile(typeof(AdaptiveClientRepository));
Assert.DoesNotContain("Guid.", source, StringComparison.Ordinal);
Assert.DoesNotContain("long.TryParse", source, StringComparison.Ordinal);
Assert.DoesNotContain("long.Parse", source, StringComparison.Ordinal);
}
[Fact]
public void Repository_ContainsNoHfsqlRowReferences()
{
var type = typeof(AdaptiveClientRepository);
var referenced = GetPublicSurfaceTypes(type);
Assert.DoesNotContain(referenced, t =>
t.Name.Contains("Row", StringComparison.OrdinalIgnoreCase)
&& t.Namespace is not null
&& t.Namespace.Contains("HfSql", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Repository_ContainsNoBatikMapperReferences()
{
var type = typeof(AdaptiveClientRepository);
var referenced = GetPublicSurfaceTypes(type);
Assert.DoesNotContain(referenced, t =>
t.Namespace is not null
&& t.Namespace.Contains("Mapping.Batik", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Repository_ContainsNoOmikaMapperReferences()
{
var type = typeof(AdaptiveClientRepository);
var referenced = GetPublicSurfaceTypes(type);
Assert.DoesNotContain(referenced, t =>
t.Namespace is not null
&& t.Namespace.Contains("Mapping.Omika", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void Repository_DoesNotDependOnHfsqlConnection()
{
var type = typeof(AdaptiveClientRepository);
bool hasHfsqlConnectionCtor = type.GetConstructors()
.Any(c => c.GetParameters()
.Any(p => typeof(IHfsqlConnection).IsAssignableFrom(p.ParameterType)));
Assert.False(hasHfsqlConnectionCtor,
"AdaptiveClientRepository ne doit pas dépendre directement de IHfsqlConnection.");
}
[Fact]
public void Repository_DoesNotDependOnDatabaseSchemaDetector()
{
var type = typeof(AdaptiveClientRepository);
var referenced = GetPublicSurfaceTypes(type);
Assert.DoesNotContain(referenced, t =>
t.Name.Contains("DatabaseSchemaDetector", StringComparison.OrdinalIgnoreCase));
}
// =========================================================================
// HELPERS
// =========================================================================
private static IEnumerable GetPublicSurfaceTypes(Type type)
{
var members = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
.Select(m => m.DeclaringType)
.Where(t => t is not null)
.Cast();
var parameters = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.SelectMany(m => m.GetParameters())
.Select(p => p.ParameterType);
var returnTypes = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Select(m => m.ReturnType);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Select(p => p.PropertyType);
var ctorParams = type.GetConstructors()
.SelectMany(c => c.GetParameters())
.Select(p => p.ParameterType);
return members.Concat(parameters).Concat(returnTypes).Concat(properties).Concat(ctorParams);
}
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 BatikClientDict(
long pkid, string? nom, string? code,
long typeId = 0, long statutId = 0, long adressePrincipaleId = 0)
=> new()
{
["CL_PKID"] = pkid,
["CL_Nom"] = nom,
["CL_CodeClient"] = code,
["CL_IDType"] = typeId,
["CL_IDStatut"] = statutId,
["CL_IDAdressePrincipale"] = adressePrincipaleId,
["CL_Blocage"] = false,
["Archive"] = false
};
private static Dictionary OmikaClientDict(
long pkid, string? nom, string? code,
long typeId = 0, long statutId = 0, long adressePrincipaleId = 0,
string? guid = null, string? typeCle = null, string? statutCle = null)
=> new()
{
["CL_PKID"] = pkid,
["CL_Nom"] = nom,
["CL_CodeClient"] = code,
["CL_IDType"] = typeId,
["CL_IDStatut"] = statutId,
["CL_CleType"] = typeCle,
["CL_CleStatut"] = statutCle,
["CL_IDAdressePrincipale"] = adressePrincipaleId,
["CL_Blocage"] = false,
["Archive"] = false,
["Cl_RaisonBlocage"] = null,
["CL_Email"] = null,
["CL_Tel"] = null,
["CL_Fax"] = null,
["CL_IDImport"] = null,
["CL_CodeComptable"] = null,
["CL_CodeTVA"] = null,
["CL_SIREN"] = null,
["CL_SIRET"] = null,
["CL_Pays"] = null,
["CL_InfoSup"] = null,
["CL_FormeJuridique"] = null,
["CL_RepertoireDocs"] = null,
["CL_IDContactPrincipal"] = 0L,
["CL_IDLieuPrincipal"] = 0L,
["CL_IDApporteur"] = 0L,
["CL_IDApportOrigine"] = 0L,
["CL_IDModeReglement"] = 0L,
["CL_CleApporteur"] = null,
["CL_CleApporteurOrigine"] = null,
["CL_CleModeReglement"] = null,
["GUID"] = guid,
["ModifDate"] = DateTime.MinValue,
["CreationDate"] = DateTime.MinValue
};
// =========================================================================
// SpyingHfsqlConnection
// =========================================================================
private sealed class SpyingHfsqlConnection : IHfsqlConnection
{
public HfsqlOptions Options => new();
public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed;
public int QueryCount { get; private set; }
public bool ThrowOnQuery { get; set; }
public string? LastSql { get; private set; }
public IReadOnlyList