using System.Collections; using System.Data; using System.Data.Common; using Omika.Data.HfSql.Connection; namespace Omika.Tests.Connection; public class HfSqlFileAccessResolverTests { private static HfSqlConnectionOptions CreateOptions() => new() { Server = "hfsql-serveur", Port = 4900, Database = "Omika", User = "tech", Credential = "secret", FilePasswords = new HfSqlFilePasswordOptions { ProtectionStd = "STD_PASSWORD", ProtectionPlus = "PLUS_PASSWORD" } }; [Fact] public async Task ResolveAsync_DirectPass_ReturnsDirect() { var factory = new FakeFactory(directPass: ["Client"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; var result = await resolver.ResolveAsync(files); Assert.Equal(HfSqlFileAccessMode.Direct, result["Client"].ObservedAccess); } [Fact] public async Task ResolveAsync_DirectFail_ProtectedPass_ReturnsProtectedMode() { var factory = new FakeFactory( directPass: [], protectedPass: ["Client"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; var result = await resolver.ResolveAsync(files); Assert.Equal(HfSqlFileAccessMode.ProtectionStd, result["Client"].ObservedAccess); } [Fact] public async Task ResolveAsync_DirectFail_ProtectedPlusPass_ReturnsProtectionPlus() { var factory = new FakeFactory( directPass: [], protectedPass: ["ParametreSysteme"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "ParametreSysteme", ExpectedGroup = "ProtectionPlus" } }; var result = await resolver.ResolveAsync(files); Assert.Equal(HfSqlFileAccessMode.ProtectionPlus, result["ParametreSysteme"].ObservedAccess); } [Fact] public async Task ResolveAsync_DirectFail_ProtectedFail_ReturnsFailed() { var factory = new FakeFactory( directPass: [], protectedPass: []); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; var result = await resolver.ResolveAsync(files); Assert.Equal(HfSqlFileAccessMode.Failed, result["Client"].ObservedAccess); } [Fact] public async Task ResolveAsync_NoExpectedGroup_DirectFail_ReturnsFailed() { var factory = new FakeFactory( directPass: [], protectedPass: []); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = null } }; var result = await resolver.ResolveAsync(files); // Sans groupe attendu, pas de tentative protégée : le fichier reste Failed. Assert.Equal(HfSqlFileAccessMode.Failed, result["Client"].ObservedAccess); } [Fact] public async Task ResolveAsync_GetProfile_ReturnsCachedProfile() { var factory = new FakeFactory(directPass: ["Client"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; await resolver.ResolveAsync(files); var profile = resolver.GetProfile("Client"); Assert.NotNull(profile); Assert.Equal(HfSqlFileAccessMode.Direct, profile!.ObservedAccess); } [Fact] public async Task ResolveAsync_GetProfile_UnknownFile_ReturnsNull() { var factory = new FakeFactory(directPass: ["Client"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; await resolver.ResolveAsync(files); Assert.Null(resolver.GetProfile("Inexistant")); } [Fact] public async Task ResolveAsync_InvalidIdentifier_ReturnsFailed() { var factory = new FakeFactory(directPass: []); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Bad;Name", ExpectedGroup = "ProtectionStd" } }; var result = await resolver.ResolveAsync(files); Assert.Equal(HfSqlFileAccessMode.Failed, result["Bad;Name"].ObservedAccess); } [Fact] public async Task ResolveAsync_ExplicitStdPassword_UsedInExtendedProperties() { // Vérifie que le resolver utilise la valeur EXACTE de FilePasswords.ProtectionStd // dans les Extended Properties (aucune dérivation). var factory = new CapturingFactory(directPass: [], protectedPass: ["Client"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; await resolver.ResolveAsync(files); Assert.NotNull(factory.LastExtendedProperties); Assert.Contains("Password=Client:STD_PASSWORD", factory.LastExtendedProperties); Assert.DoesNotContain("DeriveProtectionStd", factory.LastExtendedProperties); } [Fact] public async Task ResolveAsync_ExplicitPlusPassword_UsedInExtendedProperties() { // Vérifie que le resolver utilise la valeur EXACTE de FilePasswords.ProtectionPlus // dans les Extended Properties (aucune dérivation). var factory = new CapturingFactory(directPass: [], protectedPass: ["ParametreSysteme"]); var resolver = new HfSqlFileAccessResolver(factory, CreateOptions()); var files = new List { new() { LogicalFile = "ParametreSysteme", ExpectedGroup = "ProtectionPlus" } }; await resolver.ResolveAsync(files); Assert.NotNull(factory.LastExtendedProperties); Assert.Contains("Password=ParametreSysteme:PLUS_PASSWORD", factory.LastExtendedProperties); Assert.DoesNotContain("DeriveProtectionPlus", factory.LastExtendedProperties); } [Fact] public async Task ResolveAsync_NoPasswordsConfigured_DirectOnly() { // Sans passwords configurés, le resolver ne tente aucune connexion protégée. var options = new HfSqlConnectionOptions { Server = "hfsql-serveur", Port = 4900, Database = "Omika", User = "tech", Credential = "secret", FilePasswords = new HfSqlFilePasswordOptions() // aucun password }; var factory = new CapturingFactory(directPass: ["Client"], protectedPass: ["Client"]); var resolver = new HfSqlFileAccessResolver(factory, options); var files = new List { new() { LogicalFile = "Client", ExpectedGroup = "ProtectionStd" } }; var result = await resolver.ResolveAsync(files); // Direct PASS : pas de connexion protégée nécessaire. Assert.Equal(HfSqlFileAccessMode.Direct, result["Client"].ObservedAccess); Assert.Null(factory.LastExtendedProperties); } [Fact] public void ToString_DoesNotExposeSecrets() { var options = CreateOptions(); var str = options.ToString(); Assert.DoesNotContain("STD_PASSWORD", str); Assert.DoesNotContain("PLUS_PASSWORD", str); Assert.DoesNotContain("secret", str); } /// /// Fabrique simulée qui capture les Extended Properties pour vérifier que /// les mots de passe exacts des options sont utilisés (aucune dérivation). /// private sealed class CapturingFactory : IHfSqlOleDbConnectionFactory { private readonly HashSet _directPass; private readonly HashSet _protectedPass; public string? LastExtendedProperties { get; private set; } public CapturingFactory(string[] directPass, string[]? protectedPass = null) { _directPass = new HashSet(directPass, StringComparer.OrdinalIgnoreCase); _protectedPass = new HashSet(protectedPass ?? [], StringComparer.OrdinalIgnoreCase); } public DbConnection Create(HfSqlConnectionOptions options, string? extendedProperties = null) { if (!string.IsNullOrWhiteSpace(extendedProperties)) { LastExtendedProperties = extendedProperties; } bool isProtected = !string.IsNullOrWhiteSpace(extendedProperties); return new FakeConnection( isProtected ? _protectedPass : _directPass, isProtected ? extendedProperties : null); } } /// /// Fabrique simulée : contrôle quels fichiers passent en Direct et lesquels /// passent en protégé. Vérifie aussi que les mots de passe sont bien fournis /// dans les Extended Properties de la connexion protégée. /// private sealed class FakeFactory : IHfSqlOleDbConnectionFactory { private readonly HashSet _directPass; private readonly HashSet _protectedPass; public FakeFactory(string[] directPass, string[]? protectedPass = null) { _directPass = new HashSet(directPass, StringComparer.OrdinalIgnoreCase); _protectedPass = new HashSet(protectedPass ?? [], StringComparer.OrdinalIgnoreCase); } public DbConnection Create(HfSqlConnectionOptions options, string? extendedProperties = null) { bool isProtected = !string.IsNullOrWhiteSpace(extendedProperties); return new FakeConnection( isProtected ? _protectedPass : _directPass, isProtected ? extendedProperties : null); } } private sealed class FakeConnection : DbConnection { private readonly HashSet _passingTables; private readonly string? _extendedProperties; private ConnectionState _state = ConnectionState.Closed; public FakeConnection(HashSet passingTables, string? extendedProperties) { _passingTables = passingTables; _extendedProperties = extendedProperties; } public override string ConnectionString { get; set; } = ""; public override string Database => "Omika"; public override string DataSource => "hfsql-serveur:4900"; public override string ServerVersion => "1.0"; public override ConnectionState State => _state; public override void ChangeDatabase(string databaseName) { } public override void Close() => _state = ConnectionState.Closed; public override void Open() => _state = ConnectionState.Open; protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotSupportedException(); protected override DbCommand CreateDbCommand() => new FakeCommand(_passingTables); protected override void Dispose(bool disposing) { _state = ConnectionState.Closed; base.Dispose(disposing); } } private sealed class FakeCommand : DbCommand { private readonly HashSet _passingTables; public FakeCommand(HashSet passingTables) { _passingTables = passingTables; } public override string CommandText { get; set; } = ""; public override int CommandTimeout { get; set; } public override CommandType CommandType { get; set; } public override bool DesignTimeVisible { get; set; } public override UpdateRowSource UpdatedRowSource { get; set; } protected override DbConnection? DbConnection { get; set; } protected override DbParameterCollection DbParameterCollection { get; } = new FakeParameterCollection(); protected override DbTransaction? DbTransaction { get; set; } public override void Cancel() { } public override int ExecuteNonQuery() => 0; public override object? ExecuteScalar() => null; public override void Prepare() { } protected override DbParameter CreateDbParameter() => new FakeParameter(); protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { // Extraire le nom de table depuis "SELECT TOP 1 * FROM ". var table = CommandText.Split("FROM ", StringSplitOptions.RemoveEmptyEntries)[^1].Trim(); if (_passingTables.Contains(table)) { return new FakeReader(); } throw new InvalidOperationException("Code erreur : 70114"); } } private sealed class FakeReader : DbDataReader { public override int FieldCount => 1; public override bool HasRows => true; public override bool IsClosed => false; public override int RecordsAffected => 0; public override object this[int ordinal] => throw new NotSupportedException(); public override object this[string name] => throw new NotSupportedException(); public override int Depth => 0; public override bool GetBoolean(int ordinal) => throw new NotSupportedException(); public override byte GetByte(int ordinal) => throw new NotSupportedException(); public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); public override char GetChar(int ordinal) => throw new NotSupportedException(); public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); public override string GetDataTypeName(int ordinal) => throw new NotSupportedException(); public override DateTime GetDateTime(int ordinal) => throw new NotSupportedException(); public override decimal GetDecimal(int ordinal) => throw new NotSupportedException(); public override double GetDouble(int ordinal) => throw new NotSupportedException(); public override Type GetFieldType(int ordinal) => throw new NotSupportedException(); public override float GetFloat(int ordinal) => throw new NotSupportedException(); public override Guid GetGuid(int ordinal) => throw new NotSupportedException(); public override short GetInt16(int ordinal) => throw new NotSupportedException(); public override int GetInt32(int ordinal) => throw new NotSupportedException(); public override long GetInt64(int ordinal) => throw new NotSupportedException(); public override string GetName(int ordinal) => throw new NotSupportedException(); public override int GetOrdinal(string name) => throw new NotSupportedException(); public override string GetString(int ordinal) => throw new NotSupportedException(); public override object GetValue(int ordinal) => throw new NotSupportedException(); public override int GetValues(object[] values) => throw new NotSupportedException(); public override bool IsDBNull(int ordinal) => throw new NotSupportedException(); public override bool NextResult() => false; public override bool Read() => true; public override IEnumerator GetEnumerator() => Array.Empty().GetEnumerator(); } private sealed class FakeParameterCollection : DbParameterCollection { public override int Count => 0; public override object SyncRoot => this; public override int Add(object value) => 0; public override void AddRange(Array values) { } public override void Clear() { } public override bool Contains(object value) => false; public override bool Contains(string value) => false; public override void CopyTo(Array array, int index) { } public override IEnumerator GetEnumerator() => Array.Empty().GetEnumerator(); public override int IndexOf(object value) => -1; public override int IndexOf(string value) => -1; public override void Insert(int index, object value) { } public override void Remove(object value) { } public override void RemoveAt(int index) { } public override void RemoveAt(string parameterName) { } protected override DbParameter GetParameter(int index) => new FakeParameter(); protected override DbParameter GetParameter(string parameterName) => new FakeParameter(); protected override void SetParameter(int index, DbParameter value) { } protected override void SetParameter(string parameterName, DbParameter value) { } } private sealed class FakeParameter : DbParameter { public override DbType DbType { get; set; } public override ParameterDirection Direction { get; set; } public override bool IsNullable { get; set; } public override string ParameterName { get; set; } = ""; public override string SourceColumn { get; set; } = ""; public override object? Value { get; set; } public override bool SourceColumnNullMapping { get; set; } public override int Size { get; set; } public override void ResetDbType() { } } }