442 lines
15 KiB
C#
442 lines
15 KiB
C#
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>>([]);
|
|
}
|
|
}
|