using System.Text.Json; namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests; public sealed class LegacyDevelopmentLiveStartupContractTests { private const string ParityProjectGuid = "{0F72B3C8-8A81-4D1E-8E57-0C0E9A9960C1}"; private const string PrototypeProjectGuid = "{E6F5F1A0-D0FD-4E6B-A76D-1E8E3DAE8606}"; private const string ExactArgument = "--development-live"; private static readonly string RepositoryRoot = FindRepositoryRoot(); private static readonly string ParityRoot = Path.Combine( RepositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp"); [Fact] public void VisualStudioExposesOneLivePackageProfileWithoutDryRun() { using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine( ParityRoot, "Properties", "launchSettings.json"))); var profile = Assert.Single(document.RootElement.GetProperty("profiles") .EnumerateObject()); Assert.Contains("Live", profile.Name, StringComparison.Ordinal); Assert.DoesNotContain("DryRun", profile.Name, StringComparison.Ordinal); Assert.Equal("MsixPackage", profile.Value.GetProperty("commandName").GetString()); var arguments = profile.Value.TryGetProperty("commandLineArgs", out var value) ? value.GetString() : null; Assert.True( string.IsNullOrEmpty(arguments) || string.Equals(arguments, ExactArgument, StringComparison.Ordinal), "The sole Live package profile may use only the normal launch or exact Live argument."); } [Fact] public void SharedSolutionLaunchStartsOnlyTheLegacyParityApp() { using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine( RepositoryRoot, "MBN_STOCK_WEBVIEW.slnLaunch"))); var profile = Assert.Single(document.RootElement.EnumerateArray()); Assert.Equal("Legacy Parity App (VS F5)", profile.GetProperty("Name").GetString()); var project = Assert.Single(profile.GetProperty("Projects").EnumerateArray()); Assert.Equal( "src\\MBN_STOCK_WEBVIEW.LegacyParityApp\\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj", project.GetProperty("Path").GetString()); Assert.Equal("Start", project.GetProperty("Action").GetString()); } [Fact] public void NormalDebugDeployTargetsParityInsteadOfThePrototype() { var solution = File.ReadAllText(Path.Combine( RepositoryRoot, "MBN_STOCK_WEBVIEW.sln")); Assert.Contains( $"{ParityProjectGuid}.Debug|x64.Deploy.0 = Debug|x64", solution, StringComparison.Ordinal); Assert.DoesNotContain( $"{PrototypeProjectGuid}.Debug|x64.Deploy.0 = Debug|x64", solution, StringComparison.Ordinal); } [Fact] public void AppAppliesRuntimeGateBeforeConstructingAnyRuntimeWindow() { var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs")) .ReplaceLineEndings("\n"); var bootstrap = app.IndexOf( "DevelopmentLiveLaunchBootstrap.TryApply(", StringComparison.Ordinal); var sourceOnlyDirective = app.IndexOf( "#if SOURCE_ONLY_RUNTIME", StringComparison.Ordinal); var fullRuntimeDirective = app.IndexOf( "#else", sourceOnlyDirective, StringComparison.Ordinal); var preflight = app.IndexOf( "ResolveDevelopmentLiveConfigurationRecoveryMessage(developmentLive)", fullRuntimeDirective, StringComparison.Ordinal); var constructMainWindow = app.IndexOf( "? new MainWindow()", preflight, StringComparison.Ordinal); var constructRecoveryWindow = app.IndexOf( ": new FirstRunSetupWindow(", constructMainWindow, StringComparison.Ordinal); var runtimeEndDirective = app.IndexOf("#endif", constructRecoveryWindow, StringComparison.Ordinal); Assert.True(bootstrap >= 0); Assert.True(sourceOnlyDirective >= 0); Assert.True(fullRuntimeDirective > sourceOnlyDirective); Assert.True(bootstrap > fullRuntimeDirective); Assert.True(preflight > bootstrap); Assert.True(constructMainWindow > preflight); Assert.True(constructRecoveryWindow > constructMainWindow); Assert.True(runtimeEndDirective > constructRecoveryWindow); Assert.Contains("args.Arguments", app, StringComparison.Ordinal); Assert.Contains("Environment.GetCommandLineArgs()", app, StringComparison.Ordinal); Assert.Contains("ResolveLaunchArguments(", app, StringComparison.Ordinal); Assert.Contains( "IsDevelopmentLiveLaunchRequested = developmentLive.IsRequested;", app, StringComparison.Ordinal); Assert.Contains( "IsDevelopmentLiveLaunch =\n" + " developmentLive.IsApplied &&\n" + " configurationRecoveryMessage is null;", app, StringComparison.Ordinal); Assert.DoesNotContain("#if DEBUG", app, StringComparison.Ordinal); } [Fact] public void InvalidProtectedLiveProfileRoutesToExplicitRecoveryWithoutConstructingMainWindow() { var app = File.ReadAllText(Path.Combine(ParityRoot, "App.xaml.cs")) .ReplaceLineEndings("\n"); Assert.Contains( "_ = PlayoutOptionsLoader.LoadDevelopmentLive();", app, StringComparison.Ordinal); Assert.Contains( "catch (PlayoutConfigurationException)", app, StringComparison.Ordinal); Assert.Contains( "\"development-live-configuration-replacement-required\"", app, StringComparison.Ordinal); Assert.Contains( "버튼을 누르기 전에는 기존 Live 설정을 변경하지 않습니다.", app, StringComparison.Ordinal); Assert.Contains( "FirstRunSetupWindow.IsVerifiedSetupAvailable()", app, StringComparison.Ordinal); Assert.Contains( "liveConfigReplacementAvailable: recoverySetupAvailable", app, StringComparison.Ordinal); Assert.Contains( "현재 빌드에는 검증된 Git 소스 경로가 포함되어 있지 않아 자동 교체를 실행할 수 없습니다.", app, StringComparison.Ordinal); var preflight = app.IndexOf( "ResolveDevelopmentLiveConfigurationRecoveryMessage(developmentLive)", StringComparison.Ordinal); var windowChoice = app.IndexOf( "_window = configurationRecoveryMessage is null", preflight, StringComparison.Ordinal); var mainWindow = app.IndexOf( "? new MainWindow()", windowChoice, StringComparison.Ordinal); var recoveryWindow = app.IndexOf( ": new FirstRunSetupWindow(", mainWindow, StringComparison.Ordinal); Assert.True(preflight >= 0); Assert.True(windowChoice > preflight); Assert.True(mainWindow > windowChoice); Assert.True(recoveryWindow > mainWindow); } [Fact] public void AppliedDevelopmentLiveUsesSelectedRuntimePathsAndSelectedResDatabaseAuthority() { var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs")); var playout = File.ReadAllText(Path.Combine( ParityRoot, "MainWindow.Playout.cs")); Assert.Contains( "_isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch;", window, StringComparison.Ordinal); Assert.Contains( "_isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested;", window, StringComparison.Ordinal); Assert.Contains( "var selectedResourceDirectory =\n" + " _appliedOperatorSettings.ResourceDirectory;", window.ReplaceLineEndings("\n"), StringComparison.Ordinal); Assert.Contains( "LegacyRuntimeStockCutMenuLoader.ResolveFromResourceDirectory(", window, StringComparison.Ordinal); Assert.Contains( "var selectedLegacyDatabaseIni =\n" + " selectedResourceDirectory is null\n" + " ? null", window.ReplaceLineEndings("\n"), StringComparison.Ordinal); Assert.Contains( "? DatabaseRuntime.CreateDevelopmentLiveLocalOverride(\n" + " selectedResourceDirectory)", window.ReplaceLineEndings("\n"), StringComparison.Ordinal); Assert.Contains( "? PlayoutOptionsLoader.LoadDevelopmentLive(", playout, StringComparison.Ordinal); Assert.Contains( "operatorSceneDirectory:\n" + " _appliedOperatorSettings.SceneDirectory", playout.ReplaceLineEndings("\n"), StringComparison.Ordinal); Assert.DoesNotContain("forceSafeDryRun", playout, StringComparison.Ordinal); Assert.Contains( "if (_playoutOptions.Mode != PlayoutMode.Live)", playout, StringComparison.Ordinal); } [Fact] public void InvalidDevelopmentLiveDatabaseConfigurationBlocksEngineAndAutomaticConnect() { var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs")) .ReplaceLineEndings("\n"); var playout = File.ReadAllText(Path.Combine( ParityRoot, "MainWindow.Playout.cs")) .ReplaceLineEndings("\n"); var databaseFailureCondition = window.IndexOf( "_isDevelopmentLiveLaunch &&\n" + " exception is DatabaseConfigurationException;", StringComparison.Ordinal); var startupBlock = window.IndexOf( "_developmentLiveStartupBlocked = true;", databaseFailureCondition, StringComparison.Ordinal); var safeMessage = window.IndexOf( "\"Development Live 데이터베이스 설정을 확인할 수 없습니다.\"", startupBlock, StringComparison.Ordinal); var playoutInitialization = window.IndexOf( "InitializePlayoutRuntime();", safeMessage, StringComparison.Ordinal); var automaticConnectGuard = window.IndexOf( "if (!_developmentLiveStartupBlocked)", playoutInitialization, StringComparison.Ordinal); var automaticConnect = window.IndexOf( "await ConnectPlayoutAsync(_lifetimeCancellation.Token);", automaticConnectGuard, StringComparison.Ordinal); var earlyBlock = playout.IndexOf( "if (_developmentLiveStartupBlocked)", StringComparison.Ordinal); var earlyReturn = playout.IndexOf( "return;", earlyBlock, StringComparison.Ordinal); var createEngine = playout.IndexOf( "_playoutEngine = PlayoutEngineFactory.Create(", StringComparison.Ordinal); Assert.True(databaseFailureCondition >= 0); Assert.True(startupBlock > databaseFailureCondition); Assert.True(safeMessage > startupBlock); Assert.True(playoutInitialization > safeMessage); Assert.True(automaticConnectGuard > playoutInitialization); Assert.True(automaticConnect > automaticConnectGuard); Assert.True(earlyBlock >= 0); Assert.True(earlyReturn > earlyBlock); Assert.True(createEngine > earlyReturn); } [Fact] public void BootstrapOrPlayoutInvariantFailureBlocksEngineAndAutomaticConnect() { var window = File.ReadAllText(Path.Combine(ParityRoot, "MainWindow.xaml.cs")) .ReplaceLineEndings("\n"); var playout = File.ReadAllText(Path.Combine( ParityRoot, "MainWindow.Playout.cs")) .ReplaceLineEndings("\n"); var initialBlock = window.IndexOf( "_developmentLiveStartupBlocked =\n" + " _isDevelopmentLiveLaunchRequested &&\n" + " !_isDevelopmentLiveLaunch;", StringComparison.Ordinal); var earlyBlock = playout.IndexOf( "if (_developmentLiveStartupBlocked)", StringComparison.Ordinal); var earlyReturn = playout.IndexOf("return;", earlyBlock, StringComparison.Ordinal); var loadLive = playout.IndexOf( "PlayoutOptionsLoader.LoadDevelopmentLive(", StringComparison.Ordinal); var requireLive = playout.IndexOf( "if (_playoutOptions.Mode != PlayoutMode.Live)", StringComparison.Ordinal); var createEngine = playout.IndexOf( "_playoutEngine = PlayoutEngineFactory.Create(", StringComparison.Ordinal); var blockOnInitializationFailure = playout.IndexOf( "_developmentLiveStartupBlocked = true;", createEngine, StringComparison.Ordinal); Assert.True(initialBlock >= 0); Assert.True(earlyBlock >= 0); Assert.True(earlyReturn > earlyBlock); Assert.True(loadLive > earlyReturn); Assert.True(requireLive > loadLive); Assert.True(createEngine > requireLive); Assert.True(blockOnInitializationFailure > createEngine); Assert.DoesNotContain("forceSafeDryRun", playout, StringComparison.Ordinal); Assert.Contains( "if (!_developmentLiveStartupBlocked)\n" + " {\n" + " await ConnectPlayoutAsync(_lifetimeCancellation.Token);", window, StringComparison.Ordinal); } [Fact] public void TrackedExampleContainsOnlyNonUsableHashPlaceholders() { var example = File.ReadAllText(Path.Combine( RepositoryRoot, "Config", "playout.development-live.example.json")); using var document = JsonDocument.Parse(example); var root = document.RootElement; Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32()); Assert.Equal("Live", root.GetProperty("mode").GetString()); Assert.StartsWith("<64-HEX-", root.GetProperty("nativeSha256").GetString()); Assert.StartsWith("<64-HEX-", root.GetProperty("interopSha256").GetString()); Assert.DoesNotContain(":\\", example, StringComparison.Ordinal); } [Fact] public void RealDevelopmentAuthorizationFileIsIgnoredEverywhere() { var ignore = File.ReadAllText(Path.Combine(RepositoryRoot, ".gitignore")); Assert.Contains("Config/playout.development-live.local.json", ignore, StringComparison.Ordinal); Assert.Contains("**/playout.development-live.local.json", ignore, StringComparison.Ordinal); } 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."); } }