Files
Omika/tests/Omika.Tests/Connection/HfSqlOleDbCollaborateurLifecycleIntegrationTests.cs
T
2026-10-23 18:46:50 +02:00

313 lines
12 KiB
C#

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;
/// <summary>
/// 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
/// <c>PCSOFT.HFSQL</c> 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 <see cref="HfSqlFileAccessResolver"/>, 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.
/// </summary>
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<TestConfig>(
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
}
};
}
/// <summary>
/// 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.
/// </summary>
private static string? BuildExtendedProperties(
TestConfig config,
HfSqlConnectionOptions options,
IReadOnlyList<string> 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<string>();
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);
}
}