Premiere commit - Omika
This commit is contained in:
@@ -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>>([]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user