using System.Reflection; using Omika.Data.Configuration; using Omika.Data.Connection; using Omika.Data.HfSql.Metadata; using Omika.Data.Repositories; namespace Omika.Tests.Repositories; public class RepositoryBaseTests { // --- Constructeur --------------------------------------------------------- [Fact] public void Constructor_NullConnection_Throws() { Assert.Throws(() => new TestRepository(null!)); } // --- Ouverture de la connexion ------------------------------------------- [Fact] public async Task QueryAsync_ClosedConnection_OpensBeforeQuery() { var connection = new FakeConnection(); var repository = new TestRepository(connection); await repository.QueryAsync("SELECT 1"); Assert.Equal(1, connection.OpenCount); Assert.Equal("SELECT 1", connection.LastSql); } [Fact] public async Task QueryAsync_OpenConnection_DoesNotReopen() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); var repository = new TestRepository(connection); await repository.QueryAsync("SELECT 1"); Assert.Equal(0, connection.OpenCount); } [Fact] public async Task QueryAsync_OpenConnection_IsReused() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); var repository = new TestRepository(connection); await repository.QueryAsync("SELECT 1"); await repository.QueryAsync("SELECT 2"); Assert.Equal(0, connection.OpenCount); Assert.Equal(2, connection.QueryCount); } [Fact] public async Task QueryAsync_OpeningState_Throws() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Opening); var repository = new TestRepository(connection); await Assert.ThrowsAsync(() => repository.QueryAsync("SELECT 1")); Assert.Equal(0, connection.OpenCount); } [Fact] public async Task QueryAsync_ClosingState_Throws() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Closing); var repository = new TestRepository(connection); await Assert.ThrowsAsync(() => repository.QueryAsync("SELECT 1")); Assert.Equal(0, connection.OpenCount); } [Fact] public async Task QueryAsync_FaultedState_Throws() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Faulted); var repository = new TestRepository(connection); await Assert.ThrowsAsync(() => repository.QueryAsync("SELECT 1")); Assert.Equal(0, connection.OpenCount); } // --- SQL et paramètres transmis ------------------------------------------ [Fact] public async Task QueryAsync_SqlTransmittedIntact() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); var repository = new TestRepository(connection); const string sql = "SELECT * FROM T WHERE CL_PKID = ?"; await repository.QueryAsync(sql); Assert.Equal(sql, connection.LastSql); } [Fact] public async Task QueryAsync_ParametersTransmittedInSameOrder() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); var repository = new TestRepository(connection); var parameters = new object?[] { 42, "abc", null }; await repository.QueryAsync("SELECT * FROM T WHERE A = ? AND B = ? AND C = ?", parameters); Assert.NotNull(connection.LastParameters); Assert.Equal(3, connection.LastParameters!.Count); Assert.Equal(42, connection.LastParameters[0]); Assert.Equal("abc", connection.LastParameters[1]); Assert.Null(connection.LastParameters[2]); } [Fact] public async Task QueryAsync_NullParametersTransmitted() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); var repository = new TestRepository(connection); await repository.QueryAsync("SELECT 1", null); Assert.Null(connection.LastParameters); } [Fact] public async Task QueryAsync_CancellationTokenTransmitted() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); var repository = new TestRepository(connection); using var cts = new CancellationTokenSource(); await repository.QueryAsync("SELECT 1", cancellationToken: cts.Token); Assert.Equal(cts.Token, connection.LastCancellationToken); } // --- QueryRowsAsync ------------------------------------------------------- [Fact] public async Task QueryRowsAsync_MapsSingleRow() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); connection.QueryResult = [ new Dictionary { ["ID"] = 7, ["NAME"] = "Alpha" } ]; var repository = new TestRepository(connection); var rows = await repository.QueryRowsAsync("SELECT * FROM T"); var row = Assert.Single(rows); Assert.Equal(7, row.Id); Assert.Equal("Alpha", row.Name); } [Fact] public async Task QueryRowsAsync_MapsMultipleRows() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); connection.QueryResult = [ new Dictionary { ["ID"] = 1, ["NAME"] = "A" }, new Dictionary { ["ID"] = 2, ["NAME"] = "B" }, new Dictionary { ["ID"] = 3, ["NAME"] = "C" } ]; var repository = new TestRepository(connection); var rows = await repository.QueryRowsAsync("SELECT * FROM T"); Assert.Equal(3, rows.Count); Assert.Equal(1, rows[0].Id); Assert.Equal("B", rows[1].Name); Assert.Equal(3, rows[2].Id); } [Fact] public async Task QueryRowsAsync_EmptyResult_ReturnsEmptyList() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); connection.QueryResult = []; var repository = new TestRepository(connection); var rows = await repository.QueryRowsAsync("SELECT * FROM T"); Assert.Empty(rows); } [Fact] public async Task QueryRowsAsync_MappingError_Propagates() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); // Valeur incompatible avec la propriété int Id => HfsqlRowMapper lève. connection.QueryResult = [ new Dictionary { ["ID"] = "pas-un-entier", ["NAME"] = "X" } ]; var repository = new TestRepository(connection); await Assert.ThrowsAsync( () => repository.QueryRowsAsync("SELECT * FROM T")); } // --- Scalar et NonQuery --------------------------------------------------- [Fact] public async Task ExecuteScalarAsync_ReturnsScalar() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); connection.ScalarResult = 123; var repository = new TestRepository(connection); var result = await repository.ExecuteScalarAsync("SELECT COUNT(*) FROM T"); Assert.Equal(123, result); Assert.Equal("SELECT COUNT(*) FROM T", connection.LastSql); } [Fact] public async Task ExecuteNonQueryAsync_ReturnsAffectedRows() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); connection.NonQueryResult = 5; var repository = new TestRepository(connection); var result = await repository.ExecuteNonQueryAsync("UPDATE T SET X = 1"); Assert.Equal(5, result); Assert.Equal("UPDATE T SET X = 1", connection.LastSql); } // --- Propagation des erreurs infrastructure ------------------------------- [Fact] public async Task QueryAsync_InfrastructureError_Propagates() { var connection = new FakeConnection(initialState: HfsqlConnectionState.Open); connection.ThrowOnQuery = true; var repository = new TestRepository(connection); await Assert.ThrowsAsync(() => repository.QueryAsync("SELECT 1")); } [Fact] public async Task QueryAsync_OpenError_Propagates() { var connection = new FakeConnection(throwOnOpen: true); var repository = new TestRepository(connection); await Assert.ThrowsAsync(() => repository.QueryAsync("SELECT 1")); } // --- Surface publique : aucune dépendance Batik/Omika/Client/ODBC ---------- [Fact] public void RepositoryBase_DoesNotReferenceBatikOmikaClientTypes() { var type = typeof(RepositoryBase); var referenced = GetPublicSurfaceTypes(type); // Vérifie qu'aucun type métier Batik/Omika/Client n'apparaît dans la // surface publique/protégée. On teste le nom de type (et non le namespace // racine Omika.Data, qui contient légitimement IHfsqlConnection). Assert.DoesNotContain(referenced, t => t.Name.Contains("Batik", StringComparison.OrdinalIgnoreCase) || t.Name.Contains("Omika", StringComparison.OrdinalIgnoreCase) || t.Name.Contains("Client", StringComparison.OrdinalIgnoreCase)); } [Fact] public void RepositoryBase_DoesNotExposeOdbcTypes() { var type = typeof(RepositoryBase); var referenced = GetPublicSurfaceTypes(type); Assert.DoesNotContain(referenced, t => t.Namespace is not null && t.Namespace.StartsWith("System.Data.Odbc", StringComparison.Ordinal)); } private static IEnumerable GetPublicSurfaceTypes(Type type) { var members = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) .Select(m => m.DeclaringType) .Where(t => t is not null) .Cast(); var parameters = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .SelectMany(m => m.GetParameters()) .Select(p => p.ParameterType); var returnTypes = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(m => m.ReturnType); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(p => p.PropertyType); return members.Concat(parameters).Concat(returnTypes).Concat(properties); } // --- Row de test ---------------------------------------------------------- private sealed class TestRow { [HfSqlColumn("ID")] public int Id { get; set; } [HfSqlColumn("NAME")] public string? Name { get; set; } } // --- Repository de test --------------------------------------------------- private sealed class TestRepository : RepositoryBase { public TestRepository(IHfsqlConnection connection) : base(connection) { } public new Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => base.QueryAsync(sql, parameters, cancellationToken); public new Task> QueryRowsAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) where TRow : new() => base.QueryRowsAsync(sql, parameters, cancellationToken); public new Task ExecuteScalarAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => base.ExecuteScalarAsync(sql, parameters, cancellationToken); public new Task ExecuteNonQueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => base.ExecuteNonQueryAsync(sql, parameters, cancellationToken); } // --- Fake de connexion ---------------------------------------------------- private sealed class FakeConnection : IHfsqlConnection { private readonly bool _throwOnOpen; public FakeConnection( bool throwOnOpen = false, HfsqlConnectionState initialState = HfsqlConnectionState.Closed) { _throwOnOpen = throwOnOpen; State = initialState; } public HfsqlOptions Options => new(); public HfsqlConnectionState State { get; private set; } public int OpenCount { get; private set; } public int QueryCount { get; private set; } public string? LastSql { get; private set; } public IReadOnlyList? LastParameters { get; private set; } public CancellationToken LastCancellationToken { get; private set; } public IReadOnlyList> QueryResult { get; set; } = []; public object? ScalarResult { get; set; } public int NonQueryResult { get; set; } public bool ThrowOnQuery { get; set; } public Task OpenAsync(CancellationToken cancellationToken = default) { if (_throwOnOpen) throw new InvalidOperationException("Échec contrôlé de l'ouverture."); OpenCount++; State = HfsqlConnectionState.Open; return Task.CompletedTask; } public Task CloseAsync() { State = HfsqlConnectionState.Closed; return Task.CompletedTask; } public ValueTask DisposeAsync() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } public Task ExecuteScalarAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { LastSql = sql; LastParameters = parameters; LastCancellationToken = cancellationToken; return Task.FromResult(ScalarResult); } public Task ExecuteNonQueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { LastSql = sql; LastParameters = parameters; LastCancellationToken = cancellationToken; return Task.FromResult(NonQueryResult); } public Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { QueryCount++; LastSql = sql; LastParameters = parameters; LastCancellationToken = cancellationToken; if (ThrowOnQuery) throw new InvalidOperationException("Échec contrôlé de la requête."); return Task.FromResult(QueryResult); } public Task> GetColumnsAsync( string tableName, CancellationToken cancellationToken = default) => Task.FromResult>([]); } }