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? LastParameters { get; private set; } public CancellationToken LastCancellationToken { get; private set; } public IReadOnlyList AllSql => _allSql; private readonly List _allSql = []; public IReadOnlyList> ClientRows { get; set; } = []; public IReadOnlyList> ParametreMetierRows { get; set; } = []; public IReadOnlyList> AdresseRows { get; set; } = []; public IReadOnlyList> LieuRows { get; set; } = []; public IReadOnlyList> PersonneRows { get; set; } = []; public IReadOnlyList> ClientAdresseRows { get; set; } = []; public IReadOnlyList> ClientLieuRows { get; set; } = []; public IReadOnlyList> LieuPersonneRows { get; set; } = []; private void Record(string sql, IReadOnlyList? parameters, CancellationToken cancellationToken) { LastSql = sql; LastParameters = parameters; LastCancellationToken = cancellationToken; QueryCount++; _allSql.Add(sql); } public Task OpenAsync(CancellationToken cancellationToken = default) { State = HfsqlConnectionState.Open; return Task.CompletedTask; } public Task CloseAsync() { State = HfsqlConnectionState.Closed; return Task.CompletedTask; } public Task ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { Record(sql, parameters, cancellationToken); return Task.FromResult(null); } public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { Record(sql, parameters, cancellationToken); return Task.FromResult(0); } public Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { Record(sql, parameters, cancellationToken); if (ThrowOnQuery) throw new InvalidOperationException("Panne d'infrastructure simulée."); return Task.FromResult(ResolveRows(sql)); } public Task> GetColumnsAsync(string tableName, CancellationToken cancellationToken = default) => Task.FromResult>([]); public ValueTask DisposeAsync() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } private IReadOnlyList> ResolveRows(string sql) { if (ContainsFrom(sql, "Client_Adresse")) return ClientAdresseRows; if (ContainsFrom(sql, "Client_Lieu")) return ClientLieuRows; if (ContainsFrom(sql, "Lieu_Personne")) return LieuPersonneRows; if (ContainsFrom(sql, "ParametreMetier")) return ParametreMetierRows; if (ContainsFrom(sql, "Adresse")) return AdresseRows; if (ContainsFrom(sql, "Lieu")) return LieuRows; if (ContainsFrom(sql, "Personne")) return PersonneRows; if (ContainsFrom(sql, "Client")) return ClientRows; return []; } private static bool ContainsFrom(string sql, string table) { string marker = $"FROM {table}"; int index = sql.IndexOf(marker, StringComparison.OrdinalIgnoreCase); while (index >= 0) { int next = index + marker.Length; if (next >= sql.Length || (!char.IsLetterOrDigit(sql[next]) && sql[next] != '_')) return true; index = sql.IndexOf(marker, index + 1, StringComparison.OrdinalIgnoreCase); } return false; } } /// /// Implémentation minimale de utilisée /// uniquement pour les tests de résolution DI. Ne contacte aucune base. /// private sealed class MinimalFakeHfsqlConnection : IHfsqlConnection, IDisposable { public HfsqlOptions Options => new(); public HfsqlConnectionState State => HfsqlConnectionState.Closed; public void Dispose() { } public Task OpenAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task CloseAsync() => Task.CompletedTask; public Task ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => Task.FromResult(null); public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => Task.FromResult(0); public Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => Task.FromResult>>([]); public Task> GetColumnsAsync(string tableName, CancellationToken cancellationToken = default) => Task.FromResult>([]); public ValueTask DisposeAsync() => ValueTask.CompletedTask; } }