using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Omika.Core.Compatibility; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Planning; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Planning; using Omika.Core.Interfaces; using Omika.Core.Runtime; using Omika.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 AdaptiveRendezVousRepositoryTests : IDisposable { public AdaptiveRendezVousRepositoryTests() { 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.RendezVousRows = [BatikRendezVousDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); await adaptive.SearchAsync(new RendezVousSearchCriteria { Search = "Réunion" }); 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.RendezVousRows = [OmikaRendezVousDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); await adaptive.SearchAsync(new RendezVousSearchCriteria { Search = "Réunion" }); 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 BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.SearchAsync(new RendezVousSearchCriteria())); 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.RendezVousRows = []; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); PagedResult result = await adaptive.SearchAsync(new RendezVousSearchCriteria()); 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.RendezVousRows = [BatikRendezVousDict(42, "Dupont", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); RendezVous? 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.RendezVousRows = [OmikaRendezVousDict(42, "Dupont", "INT-001")]; var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); RendezVous? 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 BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(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.RendezVousRows = []; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); RendezVous? 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 BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); await Assert.ThrowsAsync( () => adaptive.SearchAsync(new RendezVousSearchCriteria())); } [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 BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); await Assert.ThrowsAsync( () => adaptive.GetDetailsAsync("42")); } // ========================================================================= // DI TESTS // ========================================================================= [Fact] public void AddOmikaData_RegistersBatikRendezVousRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(BatikRendezVousRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_RegistersOmikaRendezVousRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(OmikaRendezVousRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_RegistersIRendezVousRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(IRendezVousRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_IRendezVousRepositoryPointsToAdaptiveRendezVousRepository() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(IRendezVousRepository)); Assert.Equal(typeof(AdaptiveRendezVousRepository), descriptor.ImplementationType); } [Fact] public void AddOmikaData_OnlyOneIRendezVousRepositoryRegistered() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptors = services.Where(d => d.ServiceType == typeof(IRendezVousRepository)).ToList(); Assert.Single(descriptors); } [Fact] public async Task AddOmikaData_ResolvesAllRendezVousRepositories_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 rendezVousRepo = scope.ServiceProvider.GetRequiredService(); Assert.IsType(rendezVousRepo); 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.RendezVousRows = [BatikRendezVousDict(42, "Dupont", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); await adaptive.UpdateAsync("42", new RendezVousUpdateCommand { Titre = "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.RendezVousRows = [OmikaRendezVousDict(42, "Dupont", "INT-001")]; var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); await adaptive.UpdateAsync(Guid.NewGuid().ToString("D"), new RendezVousUpdateCommand { Titre = "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 BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.UpdateAsync("42", new RendezVousUpdateCommand())); Assert.Contains("not been detected", ex.Message); Assert.Equal(0, batikConnection.QueryCount); Assert.Equal(0, omikaConnection.QueryCount); } // ========================================================================= // DELEGATION CANCEL // ========================================================================= [Fact] public async Task CancelAsync_BatikDetected_Existing_ReturnsTrue() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); batikConnection.RendezVousRows = [BatikRendezVousDict(42, "Dupont", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); bool result = await adaptive.CancelAsync("42", new AnnulationRendezVousCommand()); 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 CancelAsync_OmikaDetected_Existing_ReturnsTrue() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); omikaConnection.RendezVousRows = [OmikaRendezVousDict(42, "Dupont", "INT-001")]; var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); bool result = await adaptive.CancelAsync(Guid.NewGuid().ToString("D"), new AnnulationRendezVousCommand()); 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 CancelAsync_UnknownDetected_ThrowsInvalidOperationException() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.CancelAsync("42", new AnnulationRendezVousCommand())); 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.RendezVousRows = [BatikRendezVousDict(42, "Dupont", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(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.RendezVousRows = [OmikaRendezVousDict(42, "Dupont", "INT-001")]; var batikRepo = new BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(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 BatikRendezVousRepository(batikConnection); var omikaRepo = new OmikaRendezVousRepository(omikaConnection); var adaptive = new AdaptiveRendezVousRepository(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_ImplementsIRendezVousRepository() { Assert.True(typeof(IRendezVousRepository).IsAssignableFrom(typeof(AdaptiveRendezVousRepository))); } [Fact] public void Repository_ContainsNoSql() { string source = ReadSourceFile(typeof(AdaptiveRendezVousRepository)); 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(AdaptiveRendezVousRepository); 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(AdaptiveRendezVousRepository); 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(AdaptiveRendezVousRepository); bool hasHfsqlConnectionCtor = type.GetConstructors() .Any(c => c.GetParameters() .Any(p => typeof(IHfsqlConnection).IsAssignableFrom(p.ParameterType))); Assert.False(hasHfsqlConnectionCtor, "AdaptiveRendezVousRepository 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 BatikRendezVousDict( long pkid, string? objet, string? reference) => new() { ["PL_PKID"] = pkid, ["PL_Objet"] = objet, ["PL_DateDebut"] = new DateTime(2026, 1, 15, 9, 0, 0), ["PL_DateFin"] = new DateTime(2026, 1, 15, 10, 0, 0), ["PL_IDTypeActivite"] = 0L, ["PL_IDStatut"] = 0L, ["PL_IDCollaborateur"] = 0L, ["PL_IDOrigineAnnul"] = 0UL, ["PL_IDMotifAnnul"] = 0UL, ["PL_Prive"] = false, ["PL_Superposable"] = false, ["Archive"] = false }; private static Dictionary OmikaRendezVousDict( long pkid, string? objet, string? reference) => new() { ["RV_PKID"] = pkid, ["RV_Objet"] = objet, ["RV_Date"] = new DateOnly(2026, 1, 15), ["RV_CleType"] = null, ["RV_CleStatut"] = null, ["RV_IdLieu"] = 0UL, ["RV_IdDossier"] = 0L, ["RV_IdInter"] = 0L, ["RV_IdSociete"] = 0L, ["RV_CleOrigineAnnu"] = null, ["RV_CleMotifAnnu"] = null, ["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> RendezVousRows { 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, "Planning_RDV") || ContainsFrom(sql, "RDV_Planning")) return RendezVousRows; 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; } } }