using System.Text.Json; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Organisation; using Omika.Data.HfSql.Connection; using Omika.Data.Repositories.Batik; using Omika.Data.Repositories.Omika; namespace Omika.Tests.Connection; /// /// Tests d'intégration OLE DB HFSQL réels du cycle de vie Collaborateur : /// CREATE → UPDATE → ARCHIVE (désactivation), le tout dans UNE transaction /// externe unique, puis ROLLBACK total. /// /// Ces tests nécessitent un serveur HFSQL accessible et le provider OLE DB /// PCSOFT.HFSQL installé. Ils sont ignorés automatiquement si la /// configuration locale n'est pas disponible ou si le serveur est injoignable. /// /// Principe : on ouvre une connexion physique avec les mots de passe de fichiers /// (Extended Properties) résolus par , on /// crée UNE transaction externe, on exécute Create puis Update puis Archive dans /// cette même transaction, on vérifie chaque étape, puis on annule (rollback) /// pour ne laisser AUCUNE trace en base. /// /// Aucun secret n'est affiché : les assertions portent uniquement sur les /// entités canoniques retournées. /// public class HfSqlOleDbCollaborateurLifecycleIntegrationTests { private const string ConfigPath = @"D:\Avalonia\Omika\config\hfsql-test.local.json"; private sealed class TestConfig { public int Port { get; set; } public string Server { get; set; } = ""; public string ProtectionPlusSecret { get; set; } = ""; public FilePasswordsConfig FilePasswords { get; set; } = new(); public DatabaseConfig Batik { get; set; } = new(); public DatabaseConfig Omika { get; set; } = new(); } private sealed class FilePasswordsConfig { public string? ProtectionStd { get; set; } public string? ProtectionPlus { get; set; } } private sealed class DatabaseConfig { public string Credential { get; set; } = ""; public string Database { get; set; } = ""; public string User { get; set; } = ""; public string? OriginalDatabaseName { get; set; } } private static TestConfig? LoadConfig() { if (!File.Exists(ConfigPath)) return null; try { var json = File.ReadAllText(ConfigPath); TestConfig? config = JsonSerializer.Deserialize( json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); // Serveur HFSQL injoignable : le test est ignoré (pas d'échec). if (config is null || !IsHfsqlServerReachable(config.Server, config.Port)) return null; return config; } catch { return null; } } private static bool IsHfsqlServerReachable(string server, int port) { try { using var client = new System.Net.Sockets.TcpClient(); return client.ConnectAsync(server, port).Wait(TimeSpan.FromSeconds(3)); } catch { return false; } } private static HfSqlConnectionOptions BuildOptions(TestConfig config, DatabaseConfig db) { return new HfSqlConnectionOptions { Server = config.Server, Port = config.Port, Database = db.Database, User = db.User, Credential = db.Credential, FilePasswords = new HfSqlFilePasswordOptions { ProtectionStd = config.FilePasswords.ProtectionStd, ProtectionPlus = config.FilePasswords.ProtectionPlus } }; } /// /// Construit les Extended Properties (mots de passe de fichiers) pour les /// fichiers nécessaires au cycle de vie Collaborateur, en fonction du mode /// d'accès observé par le résolveur. /// private static string? BuildExtendedProperties( TestConfig config, HfSqlConnectionOptions options, IReadOnlyList logicalFiles) { var factory = new HfSqlOleDbConnectionFactory(); var resolver = new HfSqlFileAccessResolver(factory, options); var profiles = logicalFiles .Select(f => new HfSqlFileAccessProfile { LogicalFile = f }) .ToList(); var resolved = resolver.ResolveAsync(profiles).GetAwaiter().GetResult(); var entries = new List(); foreach (var logicalFile in logicalFiles) { if (!resolved.TryGetValue(logicalFile, out var profile)) continue; string? password = profile.ObservedAccess switch { HfSqlFileAccessMode.ProtectionStd => config.FilePasswords.ProtectionStd, HfSqlFileAccessMode.ProtectionPlus => config.FilePasswords.ProtectionPlus, _ => null }; if (password is null) continue; entries.Add(HfSqlOleDbConnectionStringBuilder.BuildFilePasswordEntry(logicalFile, password)); } return entries.Count == 0 ? null : string.Join(";", entries); } [Fact] public async Task Batik_CreateUpdateArchive_RollsBackAtomically() { var config = LoadConfig(); if (config is null) return; // config absente : test ignoré var options = BuildOptions(config, config.Batik); var factory = new HfSqlOleDbConnectionFactory(); // Fichiers nécessaires au cycle de vie Collaborateur Batik. var logicalFiles = new[] { "Collaborateur", "ParametreMetier" }; string? extendedProperties = BuildExtendedProperties(config, options, logicalFiles); try { // 1. Connexion physique avec les mots de passe de fichiers. var physical = factory.Create(options, extendedProperties); await physical.OpenAsync(); // 2. UNE transaction externe pour tout le cycle de vie. await using var transaction = await physical.BeginTransactionAsync(); // 3. Connexion HFSQL réutilisant la connexion physique + transaction. await using var connection = new HfSqlOleDbConnection( factory, options, physical, extendedProperties); await connection.OpenAsync(); // 4. Repository Batik. var repository = new BatikCollaborateurRepository(connection); // 5. CREATE dans la transaction externe. var createCommand = new CollaborateurCreateCommand { Nom = "INTEGRATION_CYCLE_BATIK", Prenom = "Cycle", Code = "ICB", Actif = true }; Collaborateur created = await repository.CreateAsync(createCommand, transaction, CancellationToken.None); Assert.NotNull(created); Assert.Equal("INTEGRATION_CYCLE_BATIK", created.Nom); Assert.NotNull(created.SourceReference); Assert.NotNull(created.SourceReference!.NumericId); // 6. UPDATE dans la MÊME transaction. var updateCommand = new CollaborateurUpdateCommand { Nom = "INTEGRATION_CYCLE_BATIK_UPDATED", Actif = true }; Collaborateur? updated = await repository.UpdateAsync( created.SourceReference!.NumericId!.Value.ToString(), updateCommand, transaction, CancellationToken.None); Assert.NotNull(updated); Assert.Equal("INTEGRATION_CYCLE_BATIK_UPDATED", updated.Nom); // 7. ARCHIVE (désactivation) dans la MÊME transaction. bool archived = await repository.ArchiveAsync( created.SourceReference!.NumericId!.Value.ToString(), transaction, CancellationToken.None); Assert.True(archived); // 8. ROLLBACK total : aucune trace en base. await transaction.RollbackAsync(); } catch (Exception ex) when (IsServerUnavailable(ex)) { return; // serveur injoignable : test ignoré } } [Fact] public async Task Omika_CreateUpdateArchive_RollsBackAtomically() { var config = LoadConfig(); if (config is null) return; // config absente : test ignoré var options = BuildOptions(config, config.Omika); var factory = new HfSqlOleDbConnectionFactory(); // Fichiers nécessaires au cycle de vie Collaborateur Omika. var logicalFiles = new[] { "Collaborateur", "ParametreMetier" }; string? extendedProperties = BuildExtendedProperties(config, options, logicalFiles); try { // 1. Connexion physique avec les mots de passe de fichiers. var physical = factory.Create(options, extendedProperties); await physical.OpenAsync(); // 2. UNE transaction externe pour tout le cycle de vie. await using var transaction = await physical.BeginTransactionAsync(); // 3. Connexion HFSQL réutilisant la connexion physique + transaction. await using var connection = new HfSqlOleDbConnection( factory, options, physical, extendedProperties); await connection.OpenAsync(); // 4. Repository Omika. var repository = new OmikaCollaborateurRepository(connection); // 5. CREATE dans la transaction externe. var createCommand = new CollaborateurCreateCommand { Nom = "INTEGRATION_CYCLE_OMIKA", Prenom = "Cycle", Code = "ICO", Actif = true }; Collaborateur created = await repository.CreateAsync(createCommand, transaction, CancellationToken.None); Assert.NotNull(created); Assert.Equal("INTEGRATION_CYCLE_OMIKA", created.Nom); Assert.NotNull(created.SourceReference); Assert.NotNull(created.SourceReference!.EntityGuid); // 6. UPDATE dans la MÊME transaction. var updateCommand = new CollaborateurUpdateCommand { Nom = "INTEGRATION_CYCLE_OMIKA_UPDATED", Actif = true }; Collaborateur? updated = await repository.UpdateAsync( created.SourceReference!.EntityGuid!.Value.ToString(), updateCommand, transaction, CancellationToken.None); Assert.NotNull(updated); Assert.Equal("INTEGRATION_CYCLE_OMIKA_UPDATED", updated.Nom); // 7. ARCHIVE (désactivation) dans la MÊME transaction. bool archived = await repository.ArchiveAsync( created.SourceReference!.EntityGuid!.Value.ToString(), transaction, CancellationToken.None); Assert.True(archived); // 8. ROLLBACK total : aucune trace en base. await transaction.RollbackAsync(); } catch (Exception ex) when (IsServerUnavailable(ex)) { return; // serveur injoignable : test ignoré } } private static bool IsServerUnavailable(Exception ex) { var message = ex.Message; return message.Contains("provider", StringComparison.OrdinalIgnoreCase) || message.Contains("serveur", StringComparison.OrdinalIgnoreCase) || message.Contains("server", StringComparison.OrdinalIgnoreCase) || message.Contains("connexion", StringComparison.OrdinalIgnoreCase) || message.Contains("connection", StringComparison.OrdinalIgnoreCase) || message.Contains("E_FAIL", StringComparison.OrdinalIgnoreCase) || message.Contains("0x80004005", StringComparison.OrdinalIgnoreCase); } }