feat: complete legacy UI and playout parity migration
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#nullable enable
|
||||
|
||||
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
|
||||
|
||||
public sealed class DevelopmentDatabaseSmokeScenarioTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SuccessfulScenario_InvokesEachOperationExactlyOnceInOrder()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var scenario = CreateScenario(calls);
|
||||
|
||||
var result = await scenario.RunAsync();
|
||||
|
||||
Assert.True(result.Passed);
|
||||
Assert.True(result.CleanupAttempted);
|
||||
Assert.True(result.CleanupVerified);
|
||||
Assert.Equal(
|
||||
["preflight", "create", "create-readback", "save", "save-readback", "cleanup", "absent"],
|
||||
calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownCreate_DoesNotRetryCleanupOrContinue()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var scenario = CreateScenario(
|
||||
calls,
|
||||
create: _ =>
|
||||
{
|
||||
calls.Add("create");
|
||||
return Task.FromException(new UnknownTestException());
|
||||
});
|
||||
|
||||
var result = await scenario.RunAsync();
|
||||
|
||||
Assert.False(result.Passed);
|
||||
Assert.True(result.OutcomeUnknown);
|
||||
Assert.Equal(nameof(UnknownTestException), result.FailureClass);
|
||||
Assert.True(result.ManualCleanupMayBeRequired);
|
||||
Assert.False(result.CleanupAttempted);
|
||||
Assert.Equal(["preflight", "create"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownSave_DoesNotIssueReadbackOrCleanupMutation()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var scenario = CreateScenario(
|
||||
calls,
|
||||
save: _ =>
|
||||
{
|
||||
calls.Add("save");
|
||||
return Task.FromException(new UnknownTestException());
|
||||
});
|
||||
|
||||
var result = await scenario.RunAsync();
|
||||
|
||||
Assert.False(result.Passed);
|
||||
Assert.True(result.OutcomeUnknown);
|
||||
Assert.True(result.ManualCleanupMayBeRequired);
|
||||
Assert.False(result.CleanupAttempted);
|
||||
Assert.Equal(["preflight", "create", "create-readback", "save"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KnownReadbackFailure_StillCleansKnownCreatedRecordInFinally()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var scenario = CreateScenario(
|
||||
calls,
|
||||
verifyCreate: _ =>
|
||||
{
|
||||
calls.Add("create-readback");
|
||||
return Task.FromException(new InvalidOperationException("bad readback"));
|
||||
});
|
||||
|
||||
var result = await scenario.RunAsync();
|
||||
|
||||
Assert.False(result.Passed);
|
||||
Assert.False(result.OutcomeUnknown);
|
||||
Assert.Equal(nameof(InvalidOperationException), result.FailureClass);
|
||||
Assert.Equal(DevelopmentDatabaseSmokeStage.CreateReadback, result.FailureStage);
|
||||
Assert.True(result.CleanupAttempted);
|
||||
Assert.True(result.CleanupVerified);
|
||||
Assert.Equal(
|
||||
["preflight", "create", "create-readback", "cleanup", "absent"],
|
||||
calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownCleanup_IsNotRetriedAndSkipsAbsenceRead()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var scenario = CreateScenario(
|
||||
calls,
|
||||
cleanup: _ =>
|
||||
{
|
||||
calls.Add("cleanup");
|
||||
return Task.FromException(new UnknownTestException());
|
||||
});
|
||||
|
||||
var result = await scenario.RunAsync();
|
||||
|
||||
Assert.False(result.Passed);
|
||||
Assert.True(result.OutcomeUnknown);
|
||||
Assert.Equal(DevelopmentDatabaseSmokeStage.Cleanup, result.FailureStage);
|
||||
Assert.True(result.CleanupAttempted);
|
||||
Assert.False(result.CleanupVerified);
|
||||
Assert.Equal(
|
||||
["preflight", "create", "create-readback", "save", "save-readback", "cleanup"],
|
||||
calls);
|
||||
}
|
||||
|
||||
private static DevelopmentDatabaseSmokeScenario CreateScenario(
|
||||
List<string> calls,
|
||||
Func<CancellationToken, Task>? create = null,
|
||||
Func<CancellationToken, Task>? verifyCreate = null,
|
||||
Func<CancellationToken, Task>? save = null,
|
||||
Func<CancellationToken, Task>? cleanup = null) =>
|
||||
new(
|
||||
"fake production workflow",
|
||||
Record(calls, "preflight"),
|
||||
create ?? Record(calls, "create"),
|
||||
verifyCreate ?? Record(calls, "create-readback"),
|
||||
save ?? Record(calls, "save"),
|
||||
Record(calls, "save-readback"),
|
||||
cleanup ?? Record(calls, "cleanup"),
|
||||
Record(calls, "absent"),
|
||||
exception => exception is UnknownTestException);
|
||||
|
||||
private static Func<CancellationToken, Task> Record(
|
||||
ICollection<string> calls,
|
||||
string value) =>
|
||||
_ =>
|
||||
{
|
||||
calls.Add(value);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
private sealed class UnknownTestException : Exception;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Data;
|
||||
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
|
||||
|
||||
public sealed class ExistingChildRowGuardTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ZeroCount_UsesBoundProductionSourceSpecificPreflights()
|
||||
{
|
||||
var executor = new RecordingExecutor("0");
|
||||
var guard = new ExistingChildRowGuard(executor);
|
||||
|
||||
await guard.EnsureNamedPlaylistChildrenAbsentAsync("00000042", default);
|
||||
await guard.EnsureThemeChildrenAbsentAsync(ThemeMarket.Krx, "00000043", default);
|
||||
await guard.EnsureThemeChildrenAbsentAsync(ThemeMarket.Nxt, "00000044", default);
|
||||
await guard.EnsureExpertChildrenAbsentAsync("0045", default);
|
||||
|
||||
Assert.Equal(4, executor.Calls.Count);
|
||||
Assert.Equal(
|
||||
[DataSourceKind.Oracle, DataSourceKind.Oracle, DataSourceKind.MariaDb, DataSourceKind.Oracle],
|
||||
executor.Calls.Select(call => call.Source));
|
||||
Assert.All(executor.Calls, call =>
|
||||
{
|
||||
var parameter = Assert.Single(call.Spec.Parameters);
|
||||
Assert.Equal("parent_code", parameter.Name);
|
||||
Assert.Equal(DbType.String, parameter.DbType);
|
||||
Assert.DoesNotContain((string)parameter.Value!, call.Spec.Sql, StringComparison.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExistingOrInvalidChildCount_FailsClosedBeforeMutation()
|
||||
{
|
||||
var guard = new ExistingChildRowGuard(new RecordingExecutor("1"));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
guard.EnsureNamedPlaylistChildrenAbsentAsync("00000042", default));
|
||||
}
|
||||
|
||||
private sealed class RecordingExecutor(string count) : IDataQueryExecutor
|
||||
{
|
||||
public List<QueryCall> Calls { get; } = [];
|
||||
|
||||
public DataTable Execute(DataSourceKind source, string tableName, string query) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
DataQuerySpec query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Calls.Add(new QueryCall(source, tableName, query));
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("ROW_COUNT", typeof(string));
|
||||
table.Rows.Add(count);
|
||||
return Task.FromResult(table);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record QueryCall(
|
||||
DataSourceKind Source,
|
||||
string QueryName,
|
||||
DataQuerySpec Spec);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AssemblyName>MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\tools\MBN_STOCK_WEBVIEW.DbWriteSmoke\MBN_STOCK_WEBVIEW.DbWriteSmoke.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,115 @@
|
||||
#nullable enable
|
||||
|
||||
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
|
||||
|
||||
public sealed class SmokeCommandLineTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DefaultInvocation_DoesNotCreatePlanOrTouchDatabase()
|
||||
{
|
||||
var planCalls = 0;
|
||||
using var output = new StringWriter();
|
||||
|
||||
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
|
||||
[],
|
||||
_ =>
|
||||
{
|
||||
++planCalls;
|
||||
throw new InvalidOperationException("must not load configuration");
|
||||
},
|
||||
output);
|
||||
|
||||
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.NotAcknowledgedExitCode, exitCode);
|
||||
Assert.Equal(0, planCalls);
|
||||
Assert.Contains("no database call", output.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExactAcknowledgement_RunsPlanAndRedactsFailureDetails()
|
||||
{
|
||||
var secret = "SECRET_CREDENTIAL_OR_VALUE";
|
||||
var planCalls = 0;
|
||||
var scenario = Scenario(
|
||||
create: _ => Task.FromException(new UnknownTestException(secret)));
|
||||
using var output = new StringWriter();
|
||||
|
||||
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
|
||||
[SmokeCommandLine.WriteAcknowledgement],
|
||||
_ =>
|
||||
{
|
||||
++planCalls;
|
||||
return [scenario];
|
||||
},
|
||||
output);
|
||||
|
||||
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.OutcomeUnknownExitCode, exitCode);
|
||||
Assert.Equal(1, planCalls);
|
||||
Assert.DoesNotContain(secret, output.ToString(), StringComparison.Ordinal);
|
||||
Assert.Contains("QUARANTINED", output.ToString(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("--i-understand-this-writes-development-db")]
|
||||
[InlineData("--I-UNDERSTAND-THIS-WRITES-DEVELOPMENT-DATABASE")]
|
||||
[InlineData("--i-understand-this-writes-development-database --extra")]
|
||||
public async Task NearMissAcknowledgement_IsRejectedWithoutPlan(string commandLine)
|
||||
{
|
||||
var planCalls = 0;
|
||||
using var output = new StringWriter();
|
||||
|
||||
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
|
||||
commandLine.Split(' ', StringSplitOptions.RemoveEmptyEntries),
|
||||
_ =>
|
||||
{
|
||||
++planCalls;
|
||||
return [];
|
||||
},
|
||||
output);
|
||||
|
||||
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.NotAcknowledgedExitCode, exitCode);
|
||||
Assert.Equal(0, planCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OutcomeUnknown_StopsAllLaterScenarios()
|
||||
{
|
||||
var laterCalls = 0;
|
||||
var unknown = Scenario(
|
||||
create: _ => Task.FromException(new UnknownTestException("hidden")));
|
||||
var later = Scenario(
|
||||
preflight: _ =>
|
||||
{
|
||||
++laterCalls;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
using var output = new StringWriter();
|
||||
|
||||
var exitCode = await DevelopmentDatabaseWriteSmokeApp.RunAsync(
|
||||
[SmokeCommandLine.WriteAcknowledgement],
|
||||
_ => [unknown, later],
|
||||
output);
|
||||
|
||||
Assert.Equal(DevelopmentDatabaseWriteSmokeApp.OutcomeUnknownExitCode, exitCode);
|
||||
Assert.Equal(0, laterCalls);
|
||||
}
|
||||
|
||||
private static DevelopmentDatabaseSmokeScenario Scenario(
|
||||
Func<CancellationToken, Task>? preflight = null,
|
||||
Func<CancellationToken, Task>? create = null) =>
|
||||
new(
|
||||
"test scenario",
|
||||
preflight ?? Completed,
|
||||
create ?? Completed,
|
||||
Completed,
|
||||
save: null,
|
||||
verifySave: null,
|
||||
Completed,
|
||||
Completed,
|
||||
exception => exception is UnknownTestException);
|
||||
|
||||
private static Task Completed(CancellationToken _) => Task.CompletedTask;
|
||||
|
||||
private sealed class UnknownTestException(string message) : Exception(message);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#nullable enable
|
||||
|
||||
using MBN_STOCK_WEBVIEW.DbWriteSmoke;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.DbWriteSmoke.Tests;
|
||||
|
||||
public sealed class SmokeRunIdentityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ProducesCollisionResistantSafeBoundedPrefix()
|
||||
{
|
||||
var identity = SmokeRunIdentity.Create(
|
||||
DateTimeOffset.Parse("2026-07-15T10:11:12+09:00"),
|
||||
Guid.Parse("01234567-89ab-cdef-0123-456789abcdef"));
|
||||
|
||||
var value = identity.For("G_PROFIT");
|
||||
|
||||
Assert.Equal(
|
||||
"MBW2607150123456789O",
|
||||
value);
|
||||
Assert.Equal(20, value.Length);
|
||||
Assert.Matches("^[A-Z0-9]+$", value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_RejectsEmptyNonceAndUnsafeSuffix()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
SmokeRunIdentity.Create(DateTimeOffset.UtcNow, Guid.Empty));
|
||||
var identity = SmokeRunIdentity.Create(DateTimeOffset.UtcNow, Guid.NewGuid());
|
||||
Assert.Throws<ArgumentException>(() => identity.For("bad-value"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user