Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,441 @@
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<ArgumentNullException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<string, object?> { ["ID"] = 7, ["NAME"] = "Alpha" }
];
var repository = new TestRepository(connection);
var rows = await repository.QueryRowsAsync<TestRow>("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<string, object?> { ["ID"] = 1, ["NAME"] = "A" },
new Dictionary<string, object?> { ["ID"] = 2, ["NAME"] = "B" },
new Dictionary<string, object?> { ["ID"] = 3, ["NAME"] = "C" }
];
var repository = new TestRepository(connection);
var rows = await repository.QueryRowsAsync<TestRow>("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<TestRow>("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<string, object?> { ["ID"] = "pas-un-entier", ["NAME"] = "X" }
];
var repository = new TestRepository(connection);
await Assert.ThrowsAsync<InvalidOperationException>(
() => repository.QueryRowsAsync<TestRow>("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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<Type> 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<Type>();
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<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
=> base.QueryAsync(sql, parameters, cancellationToken);
public new Task<IReadOnlyList<TRow>> QueryRowsAsync<TRow>(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
where TRow : new()
=> base.QueryRowsAsync<TRow>(sql, parameters, cancellationToken);
public new Task<object?> ExecuteScalarAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
=> base.ExecuteScalarAsync(sql, parameters, cancellationToken);
public new Task<int> ExecuteNonQueryAsync(
string sql,
IReadOnlyList<object?>? 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<object?>? LastParameters { get; private set; }
public CancellationToken LastCancellationToken { get; private set; }
public IReadOnlyList<IReadOnlyDictionary<string, object?>> 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<object?> ExecuteScalarAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
LastSql = sql;
LastParameters = parameters;
LastCancellationToken = cancellationToken;
return Task.FromResult(ScalarResult);
}
public Task<int> ExecuteNonQueryAsync(
string sql,
IReadOnlyList<object?>? parameters = null,
CancellationToken cancellationToken = default)
{
LastSql = sql;
LastParameters = parameters;
LastCancellationToken = cancellationToken;
return Task.FromResult(NonQueryResult);
}
public Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
string sql,
IReadOnlyList<object?>? 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<IReadOnlyList<HfsqlColumnMetadata>> GetColumnsAsync(
string tableName,
CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<HfsqlColumnMetadata>>([]);
}
}