Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/ResilientDataQueryExecutorTests.cs

217 lines
7.9 KiB
C#

using System.Data;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;
public sealed class ResilientDataQueryExecutorTests
{
[Fact]
public async Task ExecuteAsync_Success_PreservesSchemaRowsAndDbNull()
{
var source = new DataTable();
source.Columns.Add("CODE", typeof(int));
source.Columns.Add("NAME", typeof(string));
source.Rows.Add(7, DBNull.Value);
source.Rows.Add(8, "sample");
var plan = ConnectionPlan.Success(source);
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory);
var result = await executor.ExecuteAsync(
DataSourceKind.Oracle,
"Stocks",
"SELECT CODE, NAME FROM SAMPLE");
Assert.Equal("Stocks", result.TableName);
Assert.Equal(2, result.Columns.Count);
Assert.Equal("CODE", result.Columns[0].ColumnName);
Assert.Equal(typeof(int), result.Columns[0].DataType);
Assert.Equal("NAME", result.Columns[1].ColumnName);
Assert.Equal(typeof(string), result.Columns[1].DataType);
Assert.Equal(2, result.Rows.Count);
Assert.Equal(7, result.Rows[0]["CODE"]);
Assert.Equal(DBNull.Value, result.Rows[0]["NAME"]);
Assert.Equal("sample", result.Rows[1]["NAME"]);
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
Assert.Equal(5, plan.ObservedCommandTimeoutSeconds);
}
[Fact]
public async Task ExecuteAsync_CanceledBeforeOpen_DoesNotRetry()
{
var plan = new ConnectionPlan
{
OpenAsync = token =>
{
token.ThrowIfCancellationRequested();
return Task.CompletedTask;
}
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory, transient: true, retryCount: 2);
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => executor.ExecuteAsync(
DataSourceKind.Oracle,
"Canceled",
"SELECT 1 FROM DUAL",
cancellation.Token));
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
}
[Fact]
public async Task ExecuteAsync_CanceledDuringCommand_DoesNotRetry()
{
var commandStarted = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
var plan = new ConnectionPlan
{
ExecuteReaderAsync = async token =>
{
commandStarted.SetResult();
await Task.Delay(Timeout.InfiniteTimeSpan, token);
throw new InvalidOperationException("Unreachable.");
}
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory, transient: true, retryCount: 2);
using var cancellation = new CancellationTokenSource();
var operation = executor.ExecuteAsync(
DataSourceKind.MariaDb,
"Canceled",
"SELECT 1",
cancellation.Token);
await commandStarted.Task;
await cancellation.CancelAsync();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => operation);
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
}
[Fact]
public async Task ExecuteAsync_OverallTimeout_ThrowsSafeTimeoutAndDoesNotRetryCancellation()
{
var plan = new ConnectionPlan
{
ExecuteReaderAsync = async token =>
{
await Task.Delay(Timeout.InfiniteTimeSpan, token);
throw new InvalidOperationException("Unreachable.");
}
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory, transient: true, retryCount: 2, timeoutSeconds: 1);
var exception = await Assert.ThrowsAsync<DatabaseQueryTimeoutException>(
() => executor.ExecuteAsync(
DataSourceKind.Oracle,
"Timeout",
"SELECT 1 FROM DUAL"));
Assert.Equal(DataSourceKind.Oracle, exception.DataSource);
Assert.True(exception.IsTransient);
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
}
[Fact]
public async Task ExecuteAsync_TransientReadFailure_RetriesWithFreshConnectionAndSucceeds()
{
var first = new ConnectionPlan
{
OpenAsync = _ => Task.FromException(new TestDatabaseException("temporary"))
};
var data = new DataTable();
data.Columns.Add("VALUE", typeof(int));
data.Rows.Add(1);
var second = ConnectionPlan.Success(data);
var factory = new FakeConnectionFactory(first, second);
var executor = CreateExecutor(factory, transient: true, retryCount: 2);
var result = await executor.ExecuteAsync(
DataSourceKind.MariaDb,
"Retry",
"SELECT VALUE FROM SAMPLE");
Assert.Single(result.Rows.Cast<DataRow>());
Assert.Equal(2, factory.CreateCount);
Assert.Equal(2, factory.CreatedConnections.Count);
Assert.NotSame(factory.CreatedConnections[0], factory.CreatedConnections[1]);
Assert.True(first.Disposed);
Assert.True(second.Disposed);
}
[Fact]
public async Task ExecuteAsync_PermanentFailure_AttemptsOnceAndUsesSafePublicMessage()
{
const string host = "host-sensitive-sentinel";
const string user = "user-sensitive-sentinel";
const string password = "password-sensitive-sentinel";
var plan = new ConnectionPlan
{
OpenAsync = _ => Task.FromException(
new TestDatabaseException($"{host}|{user}|{password}"))
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory, transient: false, retryCount: 2);
var exception = await Assert.ThrowsAsync<DatabaseQueryException>(
() => executor.ExecuteAsync(
DataSourceKind.Oracle,
"Permanent",
"SELECT 1 FROM DUAL"));
Assert.False(exception.IsTransient);
Assert.Equal(1, factory.CreateCount);
Assert.Null(exception.InnerException);
Assert.DoesNotContain(host, exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain(user, exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain(password, exception.Message, StringComparison.Ordinal);
}
[Fact]
public async Task ExecuteAsync_NonReadQuery_DoesNotRetryTransientFailure()
{
var plan = new ConnectionPlan
{
ExecuteReaderAsync = _ => Task.FromException<System.Data.Common.DbDataReader>(
new TestDatabaseException("temporary"))
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory, transient: true, retryCount: 2);
var exception = await Assert.ThrowsAsync<DatabaseQueryException>(
() => executor.ExecuteAsync(
DataSourceKind.MariaDb,
"Mutation",
"UPDATE SAMPLE SET VALUE = 1"));
Assert.True(exception.IsTransient);
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
}
private static ResilientDataQueryExecutor CreateExecutor(
FakeConnectionFactory factory,
bool transient = false,
int retryCount = 0,
int timeoutSeconds = 5) =>
new(
factory,
new DatabaseResilienceOptions
{
OperationTimeoutSeconds = timeoutSeconds,
MaximumRetryCount = retryCount,
InitialRetryDelayMilliseconds = 0
},
new StubErrorDetector(transient));
}