Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyGateAAppIntegrationContractTests.cs

362 lines
16 KiB
C#

namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyGateAAppIntegrationContractTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string AppRoot = Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp");
private static readonly string Window = Read(AppRoot, "MainWindow.xaml.cs");
private static readonly string Playout = Read(AppRoot, "MainWindow.Playout.cs");
private static readonly string IntentParser = Read(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyBridge",
"LegacyPlayoutUiIntents.cs");
private static readonly string Factory = Read(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.Playout",
"PlayoutEngineFactory.cs");
private static readonly string LaunchAuthorization = Read(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.Playout",
"Safety",
"PlayoutLaunchAuthorization.cs");
private static readonly string InfrastructureRoot = Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.Infrastructure",
"Execution");
[Fact]
public void Launch_authorization_is_captured_before_WebView_creation_and_forwarded_unchanged()
{
var constructor = Slice(
Window,
" public MainWindow()",
" private async void OnRootLoaded(");
var capture = RequiredIndex(
constructor,
"PlayoutLaunchAuthorization.CaptureFromEnvironment()");
var initializeComponent = RequiredIndex(constructor, "InitializeComponent();");
Assert.True(capture < initializeComponent);
Assert.Contains(
"_playoutLaunchAuthorization =\n" +
" PlayoutLaunchAuthorization.CaptureFromEnvironment();",
Normalize(constructor),
StringComparison.Ordinal);
var initialization = Normalize(Slice(
Playout,
" private void InitializePlayoutRuntime()",
" private async Task ConnectPlayoutAsync("));
Assert.Contains(
"_playoutEngine = PlayoutEngineFactory.Create(\n" +
" _playoutOptions,\n" +
" _playoutLaunchAuthorization);",
initialization,
StringComparison.Ordinal);
var factoryCreate = Slice(
Normalize(Factory),
" public static IPlayoutEngine Create(\n" +
" PlayoutOptions options,\n" +
" PlayoutLaunchAuthorization launchAuthorization)",
" internal static void ValidateGateAOptions(");
Assert.Contains("dispatcher,\n launchAuthorization,", factoryCreate,
StringComparison.Ordinal);
Assert.Contains("launchAuthorization: launchAuthorization", factoryCreate,
StringComparison.Ordinal);
}
[Fact]
public void Gate_A_has_a_dedicated_capability_intent_and_exact_Samsung_5001_prepare_handler()
{
Assert.Contains(
"public sealed record LegacyGateAPrepareIntent(string Capability)",
IntentParser,
StringComparison.Ordinal);
Assert.Contains("\"gate-a-prepare\" => ParseGateAPrepare(payload)", IntentParser,
StringComparison.Ordinal);
var parser = Slice(
IntentParser,
" private static LegacyUiIntent? ParseGateAPrepare(JsonElement payload)",
" private static LegacyUiIntent? ParseFadeDuration(JsonElement payload)");
Assert.Contains("payload.EnumerateObject().Count() != 1", parser,
StringComparison.Ordinal);
Assert.Contains("capability.Length != 64", parser, StringComparison.Ordinal);
Assert.Contains("not (>= 'A' and <= 'F')", parser, StringComparison.Ordinal);
var dispatch = Normalize(Slice(
Window,
" private async Task<LegacyOperatorSnapshot> HandleIntentAsync(",
" private async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsForDispatchAsync("));
Assert.Contains(
"LegacyGateAPrepareIntent prepare =>\n" +
" await ExecuteGateAPrepareAsync(\n" +
" prepare.Capability,",
dispatch,
StringComparison.Ordinal);
var handler = Slice(
Playout,
" private async Task<LegacyOperatorSnapshot> ExecuteGateAPrepareAsync(",
" private bool TryCreateExactGateAPrepareRequest(");
Assert.Contains("TryClaimGateAPrepare(capability)", handler,
StringComparison.Ordinal);
Assert.Contains("TryCreateExactGateAPrepareRequest(out var request)", handler,
StringComparison.Ordinal);
Assert.Contains("gateAPrepareClaimed: true", handler, StringComparison.Ordinal);
Assert.Contains("gateAPrepareRequest: request", handler, StringComparison.Ordinal);
Assert.Contains("CompleteGateAPrepare()", handler, StringComparison.Ordinal);
var exact = Slice(
Playout,
" private bool TryCreateExactGateAPrepareRequest(",
" private async Task<LegacyOperatorSnapshot> SetFadeDurationAsync(");
foreach (var required in new[]
{
"snapshot.Playlist.Count != 1",
"composition.FadeDuration != 6",
"composition.BackgroundKind != LegacySceneBackgroundKind.None",
"row.MarketText, \"코스피\"",
"row.StockName, \"삼성전자\"",
"row.PageText, \"1/1\"",
"row.StockCode, \"005930\"",
"_controller.CreatePlayoutRequest(6)",
"candidate.SelectedIndexZeroBased != 0",
"entry.CutCode, \"5001\"",
"entry.FadeDuration != 6",
"entry.PageNavigation?.IsEnabled == true",
"selection.GroupCode, \"코스피\"",
"selection.Subject, \"삼성전자\"",
"selection.DataCode, \"005930\""
})
{
Assert.Contains(required, exact, StringComparison.Ordinal);
}
}
[Fact]
public void Ordinary_playout_intents_are_rejected_in_Gate_A_before_workflow_access()
{
var execute = Slice(
Playout,
" private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(",
" private async Task<LegacyOperatorSnapshot> ExecuteGateAPrepareAsync(");
var workflowRead = RequiredIndex(execute, "var workflow = _playoutWorkflow;");
var preWorkflowGuard = execute[..workflowRead];
Assert.Contains("_playoutLaunchAuthorization.IsGateA", preWorkflowGuard,
StringComparison.Ordinal);
Assert.Contains("!gateAPrepareClaimed", preWorkflowGuard, StringComparison.Ordinal);
Assert.Contains("command != LegacyOperatorPlayoutCommand.Prepare", preWorkflowGuard,
StringComparison.Ordinal);
Assert.Contains("gateAPrepareRequest is null", preWorkflowGuard,
StringComparison.Ordinal);
Assert.Contains("return _controller.ReportError(", preWorkflowGuard,
StringComparison.Ordinal);
Assert.DoesNotContain("_playoutWorkflow", preWorkflowGuard,
StringComparison.Ordinal);
Assert.DoesNotContain("CreatePlayoutRequest", preWorkflowGuard,
StringComparison.Ordinal);
Assert.DoesNotContain("_playoutCommandGate.WaitAsync", preWorkflowGuard,
StringComparison.Ordinal);
var dispatch = Slice(
Window,
" private async Task<LegacyOperatorSnapshot> HandleIntentAsync(",
" private async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsForDispatchAsync(");
Assert.Contains("LegacyExecutePlayoutIntent playout =>", dispatch,
StringComparison.Ordinal);
Assert.Contains("ExecuteOperatorPlayoutAsync(\n playout.Command,",
Normalize(dispatch), StringComparison.Ordinal);
}
[Fact]
public void All_database_mutation_executors_receive_the_same_launch_denial_policy()
{
var constructor = Slice(
Window,
" public MainWindow()",
" private async void OnRootLoaded(");
Assert.Contains(
"new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization)",
constructor,
StringComparison.Ordinal);
Assert.True(
CountOccurrences(
constructor,
"mutationAuthorization: databaseMutationAuthorization") == 3,
"The same launch authorization must be injected into all three DB mutation executors.");
var adapter = Slice(
Window,
" private sealed class LaunchDatabaseMutationAuthorization",
" private void PostState(");
Assert.Contains(": IDatabaseMutationAuthorization", adapter,
StringComparison.Ordinal);
Assert.Contains("return _authorization.AllowsDatabaseWrites;", adapter,
StringComparison.Ordinal);
Assert.Contains("_authorization = authorization ??", adapter,
StringComparison.Ordinal);
foreach (var executorFile in new[]
{
"OracleManualFinancialMutationExecutor.cs",
"OracleNamedPlaylistMutationExecutor.cs",
"OperatorCatalogMutationExecutor.cs"
})
{
var executor = File.ReadAllText(Path.Combine(InfrastructureRoot, executorFile));
var demand = RequiredIndex(executor, "DatabaseMutationAuthorizationGuard.Demand(");
var open = RequiredIndex(executor, "await connection.OpenAsync(");
Assert.True(demand < open, $"{executorFile} must deny before opening a DB connection.");
Assert.Contains("_mutationAuthorization", executor, StringComparison.Ordinal);
Assert.Contains("IDatabaseMutationAuthorization mutationAuthorization", executor,
StringComparison.Ordinal);
}
}
[Fact]
public void Gate_A_blocks_persistent_write_intents_before_dispatch()
{
var process = Normalize(Slice(
Window,
" private async Task ProcessIntentAsync(LegacyUiIntent intent)",
" private static bool IsNamedPlaylistModalIntent("));
var guard = RequiredIndex(
process,
"_playoutLaunchAuthorization.IsGateA &&\n IsPersistentWriteIntent(intent)");
var dispatchStarted = RequiredIndex(process, "dispatchStarted = true;");
var dispatch = RequiredIndex(process, "HandleIntentAsync(intent");
Assert.True(guard < dispatchStarted);
Assert.True(guard < dispatch);
var guardEnd = process.IndexOf("return;", guard, StringComparison.Ordinal);
Assert.True(guardEnd > guard);
Assert.Contains("ReportIntentFailure(", process[guard..guardEnd],
StringComparison.Ordinal);
var persistent = Slice(
Playout,
" private static bool IsPersistentWriteIntent(LegacyUiIntent intent)",
" private static bool IsFixedSectionBatchMutationIntent(");
foreach (var intent in new[]
{
"LegacyDeleteUc4SelectedThemeIntent",
"LegacyDeleteUc6SelectedExpertIntent",
"LegacySaveOperatorCatalogIntent",
"LegacyConfirmNativeDialogIntent",
"LegacyAddComparisonPairIntent",
"LegacySaveManualFinancialIntent",
"LegacySaveManualNetSellIntent",
"LegacySaveManualViIntent",
"LegacyImportManualListsIntent",
"LegacyCreateNamedPlaylistIntent",
"LegacySaveCurrentNamedPlaylistIntent",
"LegacyDeleteSelectedNamedPlaylistIntent"
})
{
Assert.Contains(intent, persistent, StringComparison.Ordinal);
}
}
[Fact]
public void Gate_A_blocks_fade_and_background_mutation_before_any_command_gate()
{
Assert.Contains("public bool AllowsCompositionMutation => !IsGateA;",
LaunchAuthorization, StringComparison.Ordinal);
var fade = Slice(
Playout,
" private async Task<LegacyOperatorSnapshot> SetFadeDurationAsync(",
" private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh(");
AssertCompositionMutationGuard(
fade,
"Gate A 검증 회차에서는 Fade 설정을 변경할 수 없습니다.");
var toggle = Slice(
Playout,
" private async Task<LegacyOperatorSnapshot> ToggleBackgroundAsync(",
" private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(");
AssertCompositionMutationGuard(
toggle,
"Gate A 검증 회차에서는 배경 설정을 변경할 수 없습니다.");
var choose = Slice(
Playout,
" private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(",
" private bool CanChangeComposition()");
AssertCompositionMutationGuard(
choose,
"Gate A 검증 회차에서는 배경 파일을 선택할 수 없습니다.");
}
private static void AssertCompositionMutationGuard(string method, string message)
{
var authorization = RequiredIndex(
method,
"if (!_playoutLaunchAuthorization.AllowsCompositionMutation)");
var commandGate = RequiredIndex(method, "_playoutCommandGate.WaitAsync");
Assert.True(authorization < commandGate);
Assert.Contains(message, method[authorization..commandGate],
StringComparison.Ordinal);
Assert.Contains("return _controller.ReportError(", method[authorization..commandGate],
StringComparison.Ordinal);
}
private static int CountOccurrences(string source, string marker)
{
var count = 0;
var index = 0;
while ((index = source.IndexOf(marker, index, StringComparison.Ordinal)) >= 0)
{
count++;
index += marker.Length;
}
return count;
}
private static string Slice(string source, string startMarker, string endMarker)
{
var start = RequiredIndex(source, startMarker);
var end = RequiredIndex(source, endMarker, start + startMarker.Length);
return source[start..end];
}
private static int RequiredIndex(string source, string marker, int startIndex = 0)
{
var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal);
Assert.True(index >= 0, $"Required source marker was not found: {marker}");
return index;
}
private static string Normalize(string source) =>
source.Replace("\r\n", "\n", StringComparison.Ordinal);
private static string Read(params string[] parts) =>
File.ReadAllText(Path.Combine(parts));
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
{
return current.FullName;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Repository root was not found.");
}
}