333 lines
12 KiB
C#
333 lines
12 KiB
C#
using System.Text.Json;
|
|
using Omika.Core.Domain.Commands;
|
|
using Omika.Core.Domain.Entities.Planning;
|
|
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 RendezVous :
|
|
/// CREATE → UPDATE → CANCEL → ARCHIVE, 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 Cancel 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 HfSqlOleDbRendezVousLifecycleIntegrationTests
|
|
{
|
|
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 RendezVous, 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_CreateUpdateCancelArchive_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 RendezVous Batik.
|
|
var logicalFiles = new[]
|
|
{
|
|
"Planning_RDV", "Planning_RDV_Personne", "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 BatikRendezVousRepository(connection);
|
|
|
|
// 5. CREATE dans la transaction externe.
|
|
var createCommand = new RendezVousCreateCommand
|
|
{
|
|
Titre = "INTEGRATION_CYCLE_BATIK",
|
|
Description = "Cycle de vie Batik",
|
|
Debut = new DateTime(2026, 2, 1, 9, 0, 0),
|
|
Fin = new DateTime(2026, 2, 1, 10, 0, 0)
|
|
};
|
|
|
|
RendezVous created = await repository.CreateAsync(createCommand, transaction, CancellationToken.None);
|
|
Assert.NotNull(created);
|
|
Assert.Equal("INTEGRATION_CYCLE_BATIK", created.Titre);
|
|
Assert.NotEqual(Guid.Empty, created.Id);
|
|
|
|
// 6. UPDATE dans la MÊME transaction.
|
|
var updateCommand = new RendezVousUpdateCommand
|
|
{
|
|
Titre = "INTEGRATION_CYCLE_BATIK_UPDATED"
|
|
};
|
|
|
|
RendezVous? updated = await repository.UpdateAsync(
|
|
created.Id.ToString(), updateCommand, transaction, CancellationToken.None);
|
|
Assert.NotNull(updated);
|
|
Assert.Equal("INTEGRATION_CYCLE_BATIK_UPDATED", updated.Titre);
|
|
|
|
// 7. CANCEL dans la MÊME transaction.
|
|
var annulation = new AnnulationRendezVousCommand
|
|
{
|
|
DateAnnulation = DateTime.Now,
|
|
OrigineKey = null,
|
|
MotifKey = null
|
|
};
|
|
|
|
bool cancelled = await repository.CancelAsync(
|
|
created.Id.ToString(), annulation, transaction, CancellationToken.None);
|
|
Assert.True(cancelled);
|
|
|
|
// 8. ARCHIVE dans la MÊME transaction.
|
|
bool archived = await repository.ArchiveAsync(
|
|
created.Id.ToString(), transaction, CancellationToken.None);
|
|
Assert.True(archived);
|
|
|
|
// 9. 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_CreateUpdateCancelArchive_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 RendezVous Omika.
|
|
var logicalFiles = new[]
|
|
{
|
|
"RDV_Planning", "RDV_Collaborateur", "RDV_Contact", "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 OmikaRendezVousRepository(connection);
|
|
|
|
// 5. CREATE dans la transaction externe.
|
|
var createCommand = new RendezVousCreateCommand
|
|
{
|
|
Titre = "INTEGRATION_CYCLE_OMIKA",
|
|
Description = "Cycle de vie Omika",
|
|
Debut = new DateTime(2026, 2, 1, 9, 0, 0),
|
|
Fin = new DateTime(2026, 2, 1, 10, 0, 0)
|
|
};
|
|
|
|
RendezVous created = await repository.CreateAsync(createCommand, transaction, CancellationToken.None);
|
|
Assert.NotNull(created);
|
|
Assert.Equal("INTEGRATION_CYCLE_OMIKA", created.Titre);
|
|
Assert.NotEqual(Guid.Empty, created.Id);
|
|
|
|
// 6. UPDATE dans la MÊME transaction.
|
|
var updateCommand = new RendezVousUpdateCommand
|
|
{
|
|
Titre = "INTEGRATION_CYCLE_OMIKA_UPDATED"
|
|
};
|
|
|
|
RendezVous? updated = await repository.UpdateAsync(
|
|
created.Id.ToString(), updateCommand, transaction, CancellationToken.None);
|
|
Assert.NotNull(updated);
|
|
Assert.Equal("INTEGRATION_CYCLE_OMIKA_UPDATED", updated.Titre);
|
|
|
|
// 7. CANCEL dans la MÊME transaction.
|
|
var annulation = new AnnulationRendezVousCommand
|
|
{
|
|
DateAnnulation = DateTime.Now,
|
|
OrigineKey = null,
|
|
MotifKey = null
|
|
};
|
|
|
|
bool cancelled = await repository.CancelAsync(
|
|
created.Id.ToString(), annulation, transaction, CancellationToken.None);
|
|
Assert.True(cancelled);
|
|
|
|
// 8. ARCHIVE dans la MÊME transaction.
|
|
bool archived = await repository.ArchiveAsync(
|
|
created.Id.ToString(), transaction, CancellationToken.None);
|
|
Assert.True(archived);
|
|
|
|
// 9. 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);
|
|
}
|
|
}
|