using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Omika.Core.Compatibility; using Omika.Core.Contracts.Common; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Organisation; using Omika.Core.Interfaces; using Omika.Core.Runtime; 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, propagation d'erreurs, architecture, et DI. /// [Collection("ApplicationRuntimeContext")] public class AdaptiveCollaborateurRepositoryTests : IDisposable { public AdaptiveCollaborateurRepositoryTests() { 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.CollaborateurRows = [BatikCollaborateurDict(1, "Dupont", "Jean")]; var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); await adaptive.SearchAsync(new CollaborateurSearchCriteria { Search = "Dupont" }); 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.CollaborateurRows = [OmikaCollaborateurDict(1, "Dupont", "Jean")]; var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); await adaptive.SearchAsync(new CollaborateurSearchCriteria { Search = "Dupont" }); 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 BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.SearchAsync(new CollaborateurSearchCriteria())); 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.CollaborateurRows = []; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); PagedResult result = await adaptive.SearchAsync(new CollaborateurSearchCriteria()); 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.CollaborateurRows = [BatikCollaborateurDict(42, "Dupont", "Jean")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); Collaborateur? 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.CollaborateurRows = [OmikaCollaborateurDict(42, "Dupont", "Jean")]; var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); Collaborateur? 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_UnknownDetected_ThrowsInvalidOperationException() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(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.CollaborateurRows = []; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); Collaborateur? result = await adaptive.GetDetailsAsync("999"); Assert.Null(result); } // ========================================================================= // 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 BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); await Assert.ThrowsAsync( () => adaptive.SearchAsync(new CollaborateurSearchCriteria())); } [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 BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); await Assert.ThrowsAsync( () => adaptive.GetDetailsAsync("42")); } // ========================================================================= // DI TESTS // ========================================================================= [Fact] public void AddOmikaData_RegistersBatikCollaborateurRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(BatikCollaborateurRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_RegistersOmikaCollaborateurRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(OmikaCollaborateurRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_RegistersICollaborateurRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(ICollaborateurRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_ICollaborateurRepositoryPointsToAdaptiveCollaborateurRepository() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(ICollaborateurRepository)); Assert.Equal(typeof(AdaptiveCollaborateurRepository), descriptor.ImplementationType); } [Fact] public void AddOmikaData_OnlyOneICollaborateurRepositoryRegistered() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptors = services.Where(d => d.ServiceType == typeof(ICollaborateurRepository)).ToList(); Assert.Single(descriptors); } [Fact] public async Task AddOmikaData_ResolvesAllCollaborateurRepositories_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 collaborateurRepo = scope.ServiceProvider.GetRequiredService(); Assert.IsType(collaborateurRepo); var batikRepo = scope.ServiceProvider.GetRequiredService(); Assert.NotNull(batikRepo); var omikaRepo = scope.ServiceProvider.GetRequiredService(); Assert.NotNull(omikaRepo); } // ========================================================================= // DELEGATION UPDATE // ========================================================================= [Fact] public async Task UpdateAsync_BatikDetected_DelegatesToBatik() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); batikConnection.CollaborateurRows = [BatikCollaborateurDict(42, "Dupont", "Jean")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); await adaptive.UpdateAsync("42", new CollaborateurUpdateCommand { 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.CollaborateurRows = [OmikaCollaborateurDict(42, "Dupont", "Jean")]; var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); await adaptive.UpdateAsync(Guid.NewGuid().ToString("D"), new CollaborateurUpdateCommand { 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); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.UpdateAsync("42", new CollaborateurUpdateCommand())); 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.CollaborateurRows = [BatikCollaborateurDict(42, "Dupont", "Jean")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(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_OmikaDetected_Existing_ReturnsTrue() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); omikaConnection.CollaborateurRows = [OmikaCollaborateurDict(42, "Dupont", "Jean")]; var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(batikRepo, omikaRepo); bool result = await adaptive.ArchiveAsync(Guid.NewGuid().ToString("D")); 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_UnknownDetected_ThrowsInvalidOperationException() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikCollaborateurRepository(batikConnection); var omikaRepo = new OmikaCollaborateurRepository(omikaConnection); var adaptive = new AdaptiveCollaborateurRepository(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); } // ========================================================================= // RÉGRESSION : GUID Collaborateur TRONQUÉ (32 caractères, WinDev) // ========================================================================= [Fact] public async Task GetUtilisateurByCollaborateurAsync_Omika_GuidTronque_InterrogeToutesLesFormes() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var connection = new SpyingHfsqlConnection(); // GUID physique stocké TRONQUÉ à 32 caractères (WinDev a omis les 4 // derniers hexadécimaux), comme observé en base réelle. connection.CollaborateurRows = [ new Dictionary { ["CO_PKID"] = 281474976710675L, ["GUID"] = "62224eca-4565-4f47-9197-5c743940" } ]; var repo = new OmikaCollaborateurRepository(connection); // Identifiant canonique (forme « D » complétée par PhysicalGuidParser : + "0000"). await repo.GetUtilisateurByCollaborateurAsync("62224eca-4565-4f47-9197-5c7439400000"); // La résolution doit tester TOUTES les formes de stockage (D, N, tronquée), // pas seulement la forme « D » (sinon le GUID tronqué ne matcherait jamais). string sqlCollaborateur = connection.AllSql.First( sql => sql.Contains("FROM Collaborateur", StringComparison.OrdinalIgnoreCase)); Assert.Contains("GUID IN (", sqlCollaborateur); Assert.Contains("?, ?, ?", sqlCollaborateur); } [Fact] public async Task GetDetailsAsync_Omika_GuidTronque_InterrogeToutesLesFormes() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var connection = new SpyingHfsqlConnection(); // GUID physique stocké TRONQUÉ à 32 caractères (WinDev a omis les 4 // derniers hexadécimaux), comme observé en base réelle. connection.CollaborateurRows = [ new Dictionary { ["CO_PKID"] = 281474976710675L, ["GUID"] = "62224eca-4565-4f47-9197-5c743940", ["CO_Nom"] = "Dupont", ["CO_Prenom"] = "Jean" } ]; var repo = new OmikaCollaborateurRepository(connection); // Identifiant canonique (forme « D » complétée par PhysicalGuidParser : + "0000"). await repo.GetDetailsAsync("62224eca-4565-4f47-9197-5c7439400000"); // La lecture par identifiant doit tester TOUTES les formes de stockage // (D, N, tronquée) : un GUID = ? en forme « D » ne matcherait jamais un // GUID physique tronqué → « Collaborateur introuvable ». string sqlCollaborateur = connection.AllSql.First( sql => sql.Contains("FROM Collaborateur", StringComparison.OrdinalIgnoreCase)); Assert.Contains("GUID IN (", sqlCollaborateur); Assert.Contains("?, ?, ?", sqlCollaborateur); } // ========================================================================= // ARCHITECTURE // ========================================================================= [Fact] public void Repository_ImplementsICollaborateurRepository() { Assert.True(typeof(ICollaborateurRepository).IsAssignableFrom(typeof(AdaptiveCollaborateurRepository))); } [Fact] public void Repository_ContainsNoSql() { string source = ReadSourceFile(typeof(AdaptiveCollaborateurRepository)); 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(AdaptiveCollaborateurRepository); 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_ContainsNoHfsqlRowReferences() { var type = typeof(AdaptiveCollaborateurRepository); 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_DoesNotDependOnHfsqlConnection() { var type = typeof(AdaptiveCollaborateurRepository); bool hasHfsqlConnectionCtor = type.GetConstructors() .Any(c => c.GetParameters() .Any(p => typeof(IHfsqlConnection).IsAssignableFrom(p.ParameterType))); Assert.False(hasHfsqlConnectionCtor, "AdaptiveCollaborateurRepository ne doit pas dépendre directement de IHfsqlConnection."); } // ========================================================================= // 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 BatikCollaborateurDict( long pkid, string? nom, string? prenom) => new() { ["CL_PKID"] = pkid, ["CL_Nom"] = nom, ["CL_Prenom"] = prenom, ["CL_Code"] = "JD", ["CL_EMail"] = "jean@example.com", ["CL_Telephone"] = "0102030405", ["CL_IDCivilite"] = 0L, ["CL_IDStatut"] = 0L, ["CL_IDFonction"] = 0L, ["CL_IDSociete"] = 0L, ["CL_CouleurPlanning"] = "#FF0000", ["CL_Actif"] = true, ["Archive"] = false }; private static Dictionary OmikaCollaborateurDict( long pkid, string? nom, string? prenom) => new() { ["CL_PKID"] = pkid, ["CL_Nom"] = nom, ["CL_Prenom"] = prenom, ["CL_Code"] = "JD", ["CL_EMail"] = "jean@example.com", ["CL_Telephone"] = "0102030405", ["CL_CleCivilite"] = null, ["CL_CleStatut"] = null, ["CL_CleFonction"] = null, ["CL_IdSociete"] = 0L, ["CL_CouleurPlanning"] = "#FF0000", ["CL_Actif"] = true, ["GUID"] = null, ["Archive"] = false }; // ========================================================================= // 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> CollaborateurRows { 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) { return Task.FromResult>([]); } public ValueTask DisposeAsync() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } private IReadOnlyList> ResolveRows(string sql) { if (ContainsFrom(sql, "Collaborateur") || ContainsFrom(sql, "Collaborateurs")) return CollaborateurRows; 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; } } // ========================================================================= // MinimalFakeHfsqlConnection (DI) // ========================================================================= private sealed class MinimalFakeHfsqlConnection : IHfsqlConnection { public HfsqlOptions Options => new(); public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed; 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) => 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() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } } }