feat: migrate legacy playout workflow and scenes

This commit is contained in:
2026-07-10 23:58:45 +09:00
parent 491a740505
commit 8dae7b8e0d
128 changed files with 39177 additions and 205 deletions

View File

@@ -6,6 +6,228 @@ namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;
public sealed class ResilientDataQueryExecutorTests
{
[Theory]
[InlineData(DataSourceKind.Oracle, ':')]
[InlineData(DataSourceKind.MariaDb, '@')]
public async Task ExecuteAsync_ParameterizedSpec_BindsValuesTypesAndDbNullWithoutSyncCalls(
DataSourceKind source,
char marker)
{
var data = new DataTable();
data.Columns.Add("VALUE", typeof(int));
data.Rows.Add(1);
var plan = ConnectionPlan.Success(data);
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory);
var query = new DataQuerySpec(
$"SELECT value FROM sample WHERE code = {marker}code AND optional_value = {marker}optional_value",
[
new DataQueryParameter("optional_value", null),
new DataQueryParameter("code", "005930", DbType.String)
]);
var result = await executor.ExecuteAsync(source, "Parameterized", query);
Assert.Single(result.Rows.Cast<DataRow>());
Assert.Equal(query.Sql, plan.ObservedCommandText);
Assert.Collection(
plan.ObservedParameters,
parameter =>
{
Assert.Equal("code", parameter.Name);
Assert.Equal("005930", parameter.Value);
Assert.Equal(DbType.String, parameter.ExplicitDbType);
Assert.Equal(ParameterDirection.Input, parameter.Direction);
},
parameter =>
{
Assert.Equal("optional_value", parameter.Name);
Assert.Equal(DBNull.Value, parameter.Value);
Assert.Null(parameter.ExplicitDbType);
Assert.Equal(ParameterDirection.Input, parameter.Direction);
});
Assert.True(plan.ObservedExecuteCancellationToken.CanBeCanceled);
Assert.Equal(0, plan.SynchronousOpenCount);
Assert.Equal(0, plan.SynchronousExecuteCount);
Assert.True(plan.Disposed);
}
[Theory]
[InlineData(DataSourceKind.Oracle, ':')]
[InlineData(DataSourceKind.MariaDb, '@')]
public async Task ExecuteAsync_ParameterizedSpec_PropagatesCancellationWithoutRetryOrSyncFallback(
DataSourceKind source,
char marker)
{
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);
var query = new DataQuerySpec(
$"SELECT value FROM sample WHERE code = {marker}code",
[new DataQueryParameter("code", 7, DbType.Int32)]);
using var cancellation = new CancellationTokenSource();
var operation = executor.ExecuteAsync(source, "Canceled", query, cancellation.Token);
await commandStarted.Task;
await cancellation.CancelAsync();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => operation);
Assert.Equal(1, factory.CreateCount);
Assert.Equal(0, plan.SynchronousOpenCount);
Assert.Equal(0, plan.SynchronousExecuteCount);
Assert.True(plan.Disposed);
}
[Theory]
[InlineData(DataSourceKind.Oracle, ':')]
[InlineData(DataSourceKind.MariaDb, '@')]
public async Task ExecuteAsync_ParameterizedCancellation_DoesNotExposeProviderMessage(
DataSourceKind source,
char marker)
{
const string sensitiveName = "cancel_secret_name";
const string sensitiveValue = "cancel-value-sensitive-sentinel";
var commandStarted = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
var plan = new ConnectionPlan
{
ExecuteReaderAsync = async token =>
{
commandStarted.SetResult();
try
{
await Task.Delay(Timeout.InfiniteTimeSpan, token);
}
catch (OperationCanceledException)
{
throw new OperationCanceledException(
$"{sensitiveName}|{sensitiveValue}",
token);
}
throw new InvalidOperationException("Unreachable.");
}
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory, transient: true, retryCount: 2);
var query = new DataQuerySpec(
$"SELECT value FROM sample WHERE code = {marker}{sensitiveName}",
[new DataQueryParameter(sensitiveName, sensitiveValue)]);
using var cancellation = new CancellationTokenSource();
var operation = executor.ExecuteAsync(source, "Canceled", query, cancellation.Token);
await commandStarted.Task;
await cancellation.CancelAsync();
var exception = await Assert.ThrowsAsync<OperationCanceledException>(() => operation);
Assert.DoesNotContain(sensitiveName, exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain(sensitiveValue, exception.Message, StringComparison.Ordinal);
Assert.Equal(cancellation.Token, exception.CancellationToken);
Assert.Null(exception.InnerException);
Assert.Equal(1, factory.CreateCount);
Assert.Equal(0, plan.SynchronousOpenCount);
Assert.Equal(0, plan.SynchronousExecuteCount);
Assert.True(plan.Disposed);
}
[Theory]
[InlineData(DataSourceKind.Oracle, ':')]
[InlineData(DataSourceKind.MariaDb, '@')]
public async Task ExecuteAsync_ParameterizedProviderFailure_DoesNotExposeNameOrValue(
DataSourceKind source,
char marker)
{
const string sensitiveName = "account_secret_name";
const string sensitiveValue = "value-sensitive-sentinel";
var plan = new ConnectionPlan
{
ExecuteReaderAsync = _ => Task.FromException<System.Data.Common.DbDataReader>(
new TestDatabaseException($"{sensitiveName}|{sensitiveValue}"))
};
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory);
var query = new DataQuerySpec(
$"SELECT value FROM sample WHERE account = {marker}{sensitiveName}",
[new DataQueryParameter(sensitiveName, sensitiveValue, DbType.String)]);
var exception = await Assert.ThrowsAsync<DatabaseQueryException>(
() => executor.ExecuteAsync(source, "Sensitive", query));
Assert.Null(exception.InnerException);
Assert.DoesNotContain(sensitiveName, exception.Message, StringComparison.Ordinal);
Assert.DoesNotContain(sensitiveValue, exception.Message, StringComparison.Ordinal);
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
}
[Theory]
[InlineData(DataSourceKind.Oracle, ':')]
[InlineData(DataSourceKind.MariaDb, '@')]
public async Task ExecuteAsync_ParameterizedTransientReadFailure_RecreatesBoundCommandOnRetry(
DataSourceKind source,
char marker)
{
var first = new ConnectionPlan
{
ExecuteReaderAsync = _ => Task.FromException<System.Data.Common.DbDataReader>(
new TestDatabaseException("temporary"))
};
var data = new DataTable();
data.Columns.Add("VALUE", typeof(int));
data.Rows.Add(42);
var second = ConnectionPlan.Success(data);
var factory = new FakeConnectionFactory(first, second);
var executor = CreateExecutor(factory, transient: true, retryCount: 1);
var query = new DataQuerySpec(
$"SELECT value FROM sample WHERE code = {marker}code",
[new DataQueryParameter("code", 42, DbType.Int32)]);
var result = await executor.ExecuteAsync(source, "Retry", query);
Assert.Equal(42, Assert.Single(result.Rows.Cast<DataRow>())["VALUE"]);
Assert.Equal(2, factory.CreateCount);
Assert.All(
new[] { first, second },
plan =>
{
var parameter = Assert.Single(plan.ObservedParameters);
Assert.Equal("code", parameter.Name);
Assert.Equal(42, parameter.Value);
Assert.Equal(DbType.Int32, parameter.ExplicitDbType);
Assert.Equal(0, plan.SynchronousOpenCount);
Assert.Equal(0, plan.SynchronousExecuteCount);
Assert.True(plan.Disposed);
});
}
[Fact]
public async Task ExecuteAsync_ParameterizedSpec_RejectsProviderMarkerBeforeOpeningConnection()
{
var plan = ConnectionPlan.Success(new DataTable());
var factory = new FakeConnectionFactory(plan);
var executor = CreateExecutor(factory);
var query = new DataQuerySpec(
"SELECT value FROM sample WHERE code = :code",
[new DataQueryParameter("code", 7)]);
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => executor.ExecuteAsync(DataSourceKind.MariaDb, "Rejected", query));
Assert.Equal(0, factory.CreateCount);
Assert.DoesNotContain("code", exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("7", exception.Message, StringComparison.Ordinal);
}
[Fact]
public async Task ExecuteAsync_Success_PreservesSchemaRowsAndDbNull()
{
@@ -199,6 +421,30 @@ public sealed class ResilientDataQueryExecutorTests
Assert.True(plan.Disposed);
}
[Theory]
[InlineData("SELECT value INTO OUTFILE '/tmp/sample' FROM sample")]
[InlineData("SELECT 1; REPLACE INTO sample VALUES (1)")]
[InlineData("SELECT 1; SET @value = 1")]
[InlineData("WITH value AS (SELECT 1) DELETE FROM sample")]
public async Task ExecuteAsync_AmbiguousOrStateChangingLegacyRead_DoesNotRetry(
string query)
{
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", query));
Assert.True(exception.IsTransient);
Assert.Equal(1, factory.CreateCount);
Assert.True(plan.Disposed);
}
private static ResilientDataQueryExecutor CreateExecutor(
FakeConnectionFactory factory,
bool transient = false,