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

453 lines
18 KiB
C#

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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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<HfSqlFileAccessProfile>
{
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);
}
/// <summary>
/// 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).
/// </summary>
private sealed class CapturingFactory : IHfSqlOleDbConnectionFactory
{
private readonly HashSet<string> _directPass;
private readonly HashSet<string> _protectedPass;
public string? LastExtendedProperties { get; private set; }
public CapturingFactory(string[] directPass, string[]? protectedPass = null)
{
_directPass = new HashSet<string>(directPass, StringComparer.OrdinalIgnoreCase);
_protectedPass = new HashSet<string>(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);
}
}
/// <summary>
/// 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.
/// </summary>
private sealed class FakeFactory : IHfSqlOleDbConnectionFactory
{
private readonly HashSet<string> _directPass;
private readonly HashSet<string> _protectedPass;
public FakeFactory(string[] directPass, string[]? protectedPass = null)
{
_directPass = new HashSet<string>(directPass, StringComparer.OrdinalIgnoreCase);
_protectedPass = new HashSet<string>(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<string> _passingTables;
private readonly string? _extendedProperties;
private ConnectionState _state = ConnectionState.Closed;
public FakeConnection(HashSet<string> 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<string> _passingTables;
public FakeCommand(HashSet<string> 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 <Table>".
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<object>().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<object>().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() { }
}
}