feat: initialize existing development playout PCs
This commit is contained in:
@@ -178,6 +178,127 @@ public sealed class LegacyIniDatabaseOptionsLoaderTests
|
||||
Assert.Equal("selected.maria.invalid", runtime.Options.MariaDb.Host);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runtime_DevelopmentLiveUsesOnlyExactLocalAppDataIni()
|
||||
{
|
||||
using var localApplicationData = TemporaryDirectory.Create();
|
||||
var resourceDirectory = Path.Combine(
|
||||
localApplicationData.Path,
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"Res");
|
||||
Directory.CreateDirectory(resourceDirectory);
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
|
||||
TemporaryLegacyIni.ValidUtf8Bytes());
|
||||
|
||||
const string environmentName = "MBN_STOCK_ORACLE_HOST";
|
||||
var originalEnvironment = Environment.GetEnvironmentVariable(environmentName);
|
||||
DatabaseRuntime runtime;
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable(
|
||||
environmentName,
|
||||
"stale-environment.invalid");
|
||||
runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
|
||||
localApplicationData.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(
|
||||
environmentName,
|
||||
originalEnvironment);
|
||||
}
|
||||
|
||||
Assert.Equal("oracle.dev.invalid", runtime.Options.Oracle.Host);
|
||||
Assert.Equal("maria.dev.invalid", runtime.Options.MariaDb.Host);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runtime_DevelopmentLiveFailsClosedWhenBothProtectedProfilesAreMissing()
|
||||
{
|
||||
using var localApplicationData = TemporaryDirectory.Create();
|
||||
Directory.CreateDirectory(Path.Combine(
|
||||
localApplicationData.Path,
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"Config"));
|
||||
|
||||
Assert.Throws<DatabaseConfigurationException>(
|
||||
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
|
||||
localApplicationData.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runtime_DevelopmentLiveFallsBackToProtectedLocalAppDataJson()
|
||||
{
|
||||
using var localApplicationData = TemporaryDirectory.Create();
|
||||
var configurationDirectory = Path.Combine(
|
||||
localApplicationData.Path,
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"Config");
|
||||
Directory.CreateDirectory(configurationDirectory);
|
||||
File.WriteAllText(
|
||||
Path.Combine(configurationDirectory, "database.local.json"),
|
||||
"""
|
||||
{
|
||||
"oracle": {
|
||||
"host": "json.oracle.invalid",
|
||||
"port": 1521,
|
||||
"sid": "DEVORA",
|
||||
"serviceName": null,
|
||||
"userName": "oracle-user",
|
||||
"password": "oracle-password"
|
||||
},
|
||||
"mariaDb": {
|
||||
"host": "json.maria.invalid",
|
||||
"port": 3306,
|
||||
"database": "dev_stock",
|
||||
"userName": "maria-user",
|
||||
"password": "maria-password",
|
||||
"tlsMode": "Required"
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var runtime = DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
|
||||
localApplicationData.Path);
|
||||
|
||||
Assert.Equal("json.oracle.invalid", runtime.Options.Oracle.Host);
|
||||
Assert.Equal("json.maria.invalid", runtime.Options.MariaDb.Host);
|
||||
Assert.Equal(MariaDbTlsMode.Required, runtime.Options.MariaDb.TlsMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runtime_DevelopmentLiveRejectsNonOrdinaryIniTarget()
|
||||
{
|
||||
using var localApplicationData = TemporaryDirectory.Create();
|
||||
var iniDirectory = Path.Combine(
|
||||
localApplicationData.Path,
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"Res",
|
||||
"MmoneyCoder.ini");
|
||||
Directory.CreateDirectory(iniDirectory);
|
||||
|
||||
Assert.Throws<DatabaseConfigurationException>(
|
||||
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
|
||||
localApplicationData.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runtime_DevelopmentLiveRejectsNonOrdinaryJsonTarget()
|
||||
{
|
||||
using var localApplicationData = TemporaryDirectory.Create();
|
||||
var jsonDirectory = Path.Combine(
|
||||
localApplicationData.Path,
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"Config",
|
||||
"database.local.json");
|
||||
Directory.CreateDirectory(jsonDirectory);
|
||||
|
||||
Assert.Throws<DatabaseConfigurationException>(
|
||||
() => DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
|
||||
localApplicationData.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_OptionalRealLegacyIni_IsConfiguredWithoutPrintingValues()
|
||||
{
|
||||
|
||||
@@ -98,6 +98,113 @@ public sealed class LegacyDevelopmentLiveStartupContractTests
|
||||
Assert.Contains("args.Arguments", app, StringComparison.Ordinal);
|
||||
Assert.Contains("Environment.GetCommandLineArgs()", app, StringComparison.Ordinal);
|
||||
Assert.Contains("ResolveLaunchArguments(", app, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"IsDevelopmentLiveLaunchRequested = string.Equals(",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"IsDevelopmentLiveLaunch = developmentLive.IsApplied;",
|
||||
app,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliedDevelopmentLiveUsesOnlyProtectedStartupInputs()
|
||||
{
|
||||
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(
|
||||
"? LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(",
|
||||
window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"? null\n : _appliedOperatorSettings.ResourceDirectory;",
|
||||
window.ReplaceLineEndings("\n"),
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"DatabaseRuntime.CreateDevelopmentLiveLocalOverride()",
|
||||
window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"? PlayoutOptionsLoader.LoadDevelopmentLive(",
|
||||
playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"IsSourceOnlyBuild ||\n _isDevelopmentLiveLaunchRequested",
|
||||
playout.ReplaceLineEndings("\n"),
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DevelopmentLiveDatabaseFailureForcesDryRunAndSkipsAutomaticConnect()
|
||||
{
|
||||
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 blockAssignment = window.IndexOf(
|
||||
"_developmentLiveStartupBlocked = true;",
|
||||
StringComparison.Ordinal);
|
||||
var successAssignment = window.IndexOf(
|
||||
"databaseInitializationSucceeded = true;",
|
||||
StringComparison.Ordinal);
|
||||
var runtimeReset = window.IndexOf(
|
||||
"_databaseRuntime = null;",
|
||||
successAssignment,
|
||||
StringComparison.Ordinal);
|
||||
var pagePlanReset = window.IndexOf(
|
||||
"fixedPagePlanProvider = null;",
|
||||
successAssignment,
|
||||
StringComparison.Ordinal);
|
||||
var playoutInitialization = window.IndexOf(
|
||||
"InitializePlayoutRuntime();",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(successAssignment >= 0);
|
||||
Assert.True(runtimeReset > successAssignment);
|
||||
Assert.True(pagePlanReset > successAssignment);
|
||||
Assert.True(blockAssignment > runtimeReset);
|
||||
Assert.True(playoutInitialization > blockAssignment);
|
||||
Assert.Contains(
|
||||
"var databaseInitializationSucceeded = false;",
|
||||
window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"if (_isDevelopmentLiveLaunch && !databaseInitializationSucceeded)\n" +
|
||||
" {\n" +
|
||||
" // Development Live is one fail-closed startup transaction.",
|
||||
window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_isDevelopmentLiveLaunch &&\n" +
|
||||
" !_developmentLiveStartupBlocked\n" +
|
||||
" ? PlayoutOptionsLoader.LoadDevelopmentLive(",
|
||||
playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"_isDevelopmentLiveLaunchRequested ||\n" +
|
||||
" _developmentLiveStartupBlocked",
|
||||
playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"if (!_developmentLiveStartupBlocked)\n" +
|
||||
" {\n" +
|
||||
" await ConnectPlayoutAsync(_lifetimeCancellation.Token);",
|
||||
window,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -333,7 +333,10 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
element.Attribute("Name")?.Value,
|
||||
"ValidateLegacyRuntimeAssets",
|
||||
StringComparison.Ordinal));
|
||||
var sourceOnlyMessage = Assert.Single(validationTarget.Elements("Message"));
|
||||
var sourceOnlyMessage = Assert.Single(
|
||||
validationTarget.Elements("Message"),
|
||||
message => (message.Attribute("Condition")?.Value ?? string.Empty)
|
||||
.Contains("$(LegacyRuntimeAssetsMode)", StringComparison.Ordinal));
|
||||
Assert.Contains(
|
||||
"$(LegacyRuntimeAssetsMode)",
|
||||
sourceOnlyMessage.Attribute("Condition")?.Value,
|
||||
@@ -342,6 +345,21 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
"source-only DryRun",
|
||||
sourceOnlyMessage.Attribute("Text")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Initialize-ExistingDevelopmentPc.ps1",
|
||||
sourceOnlyMessage.Attribute("Text")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var fullRuntimeMessage = Assert.Single(
|
||||
validationTarget.Elements("Message"),
|
||||
message => (message.Attribute("Text")?.Value ?? string.Empty)
|
||||
.Contains(
|
||||
"Verified full legacy runtime assets are enabled",
|
||||
StringComparison.Ordinal));
|
||||
Assert.Contains(
|
||||
"Verified full legacy runtime assets are enabled",
|
||||
fullRuntimeMessage.Attribute("Text")?.Value,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var runtimeValidationErrors = validationTarget.Elements("Error")
|
||||
.Where(error => (error.Attribute("Condition")?.Value ?? string.Empty)
|
||||
@@ -353,6 +371,11 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
"$(LegacyRuntimeAssetsValidationEnabled)",
|
||||
error.Attribute("Condition")?.Value,
|
||||
StringComparison.Ordinal));
|
||||
Assert.Contains(
|
||||
runtimeValidationErrors,
|
||||
error => (error.Attribute("Text")?.Value ?? string.Empty).Contains(
|
||||
"Initialize-ExistingDevelopmentPc.ps1",
|
||||
StringComparison.Ordinal));
|
||||
|
||||
string[] externalContentIncludes =
|
||||
[
|
||||
@@ -602,6 +625,377 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExistingDevelopmentPcInitializerUsesLocalAssetsWithoutPerformingLiveActions()
|
||||
{
|
||||
var initializer = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"scripts",
|
||||
"Initialize-ExistingDevelopmentPc.ps1"));
|
||||
|
||||
Assert.Contains("[string] $LegacyRuntimeSourceRoot", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[switch] $NoFolderPicker", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
@"..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Test-DirectCutsAndResRootCandidate",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-Path $siblingCandidate",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"System.Windows.Forms.FolderBrowserDialog",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$selectedLeaf.Equals('Cuts'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$selectedLeaf.Equals('Res'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"single common parent of Cuts and Res",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"must be on a ready local fixed drive",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"must be outside and must not overlap this repository",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Assert-NoReparsePointInTree", initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("'New-LegacyRuntimeBundle.ps1'", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("'Initialize-LegacyRuntimeBundle.ps1'", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$existingRuntime.ManifestSha256.Equals(",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Assert-InstalledRuntimeMatchesManifest",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Installed runtime bundle contains an unexpected file",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[switch] $ReplaceRuntimeBinding", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Set-RuntimeBindingAtomically", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[IO.File]::Replace($temporary, $destination, $backup)",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
var shadowInstall = initializer.IndexOf(
|
||||
"-RepositoryRoot $shadowRepositoryRoot",
|
||||
StringComparison.Ordinal);
|
||||
var bindingReplace = initializer.IndexOf(
|
||||
"Set-RuntimeBindingAtomically",
|
||||
shadowInstall,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(shadowInstall >= 0);
|
||||
Assert.True(bindingReplace > shadowInstall);
|
||||
|
||||
Assert.Contains("[string] $DatabaseIniPath", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[IO.Path]::IsPathRooted($DatabaseIniPath)", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[switch] $SkipDatabaseProfile", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[switch] $ReplaceDatabaseProfile", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(@"Res\MmoneyCoder.ini", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("$maximumDatabaseIniLength = 1MB", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Copy-DatabaseProfileAtomically", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[IO.File]::Replace($temporaryPath, $Destination, $backupPath)",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("SetAccessRuleProtection($true, $false)", initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("'Inspect-K3DRegistration.ps1'", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Get-VerifiedK3DRegistration -Reports $registrationReports",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[string] $registration.RegistryHive -cne 'HKLM'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[string] $registration.RegistryView -cne 'Registry64'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[string] $registration.CurrentUserOverrides -cne 'None'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"[string] $classReport.ThreadingModel -cne 'Apartment'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Activator.CreateInstance", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Connect(", initializer, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("TAKE IN", initializer, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("TAKE OUT", initializer, StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("[switch] $ConfigureDevelopmentLive", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[switch] $ReplaceLiveConfig", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-ConfigureDevelopmentLive requires the default Debug x64 build verification.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Independent native and interop SHA-256 approvals are both required.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Get-FileHash -LiteralPath $nativePath -Algorithm SHA256",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Get-FileHash -LiteralPath $interopPath -Algorithm SHA256",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$PlayoutHost -cne '127.0.0.1'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Existing Development Live files differ from the exact requested",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("& dotnet build $ProjectPath -c Debug '-p:Platform=x64'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"'-getProperty:LegacyRuntimeAssetsEnabled'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"unexpectedly resolved as SourceOnly",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Debug x64 payload count mismatch", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Credential-bearing MmoneyCoder.ini entered the Debug build output.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var runtimeWrite = initializer.LastIndexOf(
|
||||
"Set-RuntimeBindingAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var databaseWrite = initializer.LastIndexOf(
|
||||
"Copy-DatabaseProfileAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var build = initializer.LastIndexOf(
|
||||
"Invoke-DebugBuildAndVerify",
|
||||
StringComparison.Ordinal);
|
||||
var cleanup = initializer.LastIndexOf(
|
||||
"-Path $rollbackRoot",
|
||||
StringComparison.Ordinal);
|
||||
var liveWrite = initializer.LastIndexOf(
|
||||
"& $liveInitializerPath @liveArguments",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(runtimeWrite >= 0);
|
||||
Assert.True(databaseWrite > runtimeWrite);
|
||||
Assert.True(build > databaseWrite);
|
||||
Assert.True(cleanup > build);
|
||||
Assert.True(liveWrite > cleanup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExistingDevelopmentPcInitializerRollsBackAndReissuesLiveFailClosed()
|
||||
{
|
||||
var initializer = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"scripts",
|
||||
"Initialize-ExistingDevelopmentPc.ps1"));
|
||||
|
||||
Assert.Contains("function New-FileRollbackSnapshot", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("function Restore-FileRollbackSnapshot", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("OriginalBytes = [IO.File]::ReadAllBytes($backupPath)",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("AccessSddl = $accessSddl", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$Description rollback backup failed its immediate ACL/hash verification.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Previous database profile backup failed immediate ACL/hash verification.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$Description rollback did not restore the exact bytes and ACL.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Set-UserOnlyDirectoryAcl -Path $rollbackRoot",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var databaseMutation = initializer.LastIndexOf(
|
||||
"$databaseMutationStarted = $true",
|
||||
StringComparison.Ordinal);
|
||||
var databaseWrite = initializer.LastIndexOf(
|
||||
"Copy-DatabaseProfileAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var databaseRollback = initializer.LastIndexOf(
|
||||
"-Snapshot $databaseSnapshot",
|
||||
StringComparison.Ordinal);
|
||||
var runtimeMutation = initializer.LastIndexOf(
|
||||
"$runtimeBindingMutationStarted = $true",
|
||||
StringComparison.Ordinal);
|
||||
var runtimeWrite = initializer.LastIndexOf(
|
||||
"Set-RuntimeBindingAtomically",
|
||||
StringComparison.Ordinal);
|
||||
var runtimeRollback = initializer.LastIndexOf(
|
||||
"-Snapshot $propsSnapshot",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(databaseMutation >= 0 && databaseMutation < databaseWrite);
|
||||
Assert.True(runtimeMutation >= 0 && runtimeMutation < runtimeWrite);
|
||||
Assert.True(databaseRollback > databaseWrite);
|
||||
Assert.True(runtimeRollback > runtimeWrite);
|
||||
Assert.Contains("Original failure:", initializer, StringComparison.Ordinal);
|
||||
Assert.Contains("Secondary recovery failure(s):", initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("function Assert-ExactObjectProperties", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-Description 'Runtime bundle creation result'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-Description 'Runtime bundle initialization result'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-Description 'K3D registration inspection result'",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Runtime bundle creation result paths escaped or changed the exact output contract.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Runtime bundle initialization result paths or hashes violate containment.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("function Get-VerifiedRuntimeManifest", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"duplicate/extra/case/type/format changes are rejected.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"function Assert-DebugRuntimePayloadMatchesManifest",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Debug x64 runtime payload differs from the manifest:",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Debug x64 runtime output contains an unexpected file:",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Debug x64 runtime output does not match the exact manifest file set.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"-Root $targetRoot",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("function Test-ExactUtf8Text", initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"case changes, type coercion, reordered properties",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Get-JsonContractText", initializer,
|
||||
StringComparison.Ordinal);
|
||||
var livePreflightStart = initializer.IndexOf(
|
||||
"function Get-DevelopmentLivePreflight",
|
||||
StringComparison.Ordinal);
|
||||
var livePreflightEnd = initializer.IndexOf(
|
||||
"function Copy-DatabaseProfileAtomically",
|
||||
livePreflightStart,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(livePreflightStart >= 0 && livePreflightEnd > livePreflightStart);
|
||||
var livePreflightFunction = initializer[livePreflightStart..livePreflightEnd];
|
||||
Assert.DoesNotContain("NeedsWrite = $false", livePreflightFunction,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("DevelopmentLiveReused = $false", initializer,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var firstPersistentLiveInvalidation = initializer.IndexOf(
|
||||
"# Invalidate the previous one-launch authorization before database/runtime",
|
||||
StringComparison.Ordinal);
|
||||
var setupDirectoryCreation = initializer.IndexOf(
|
||||
"[IO.Directory]::CreateDirectory($setupParent)",
|
||||
StringComparison.Ordinal);
|
||||
var buildVerification = initializer.LastIndexOf(
|
||||
"Invoke-DebugBuildAndVerify",
|
||||
StringComparison.Ordinal);
|
||||
var setupCleanup = initializer.LastIndexOf(
|
||||
"-Path $rollbackRoot",
|
||||
StringComparison.Ordinal);
|
||||
var liveIssue = initializer.LastIndexOf(
|
||||
"$liveResults = @(& $liveInitializerPath @liveArguments)",
|
||||
StringComparison.Ordinal);
|
||||
var liveJsonVerification = initializer.LastIndexOf(
|
||||
"-ExpectedJson ([string] $livePreflight.AuthorizationJson)",
|
||||
StringComparison.Ordinal);
|
||||
var failClosedLiveRemoval = initializer.LastIndexOf(
|
||||
"Remove-DevelopmentLiveAuthorizationFailClosed",
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(firstPersistentLiveInvalidation >= 0);
|
||||
Assert.True(firstPersistentLiveInvalidation < setupDirectoryCreation);
|
||||
Assert.True(buildVerification < setupCleanup);
|
||||
Assert.True(setupCleanup < liveIssue);
|
||||
Assert.True(liveIssue < liveJsonVerification);
|
||||
Assert.True(liveJsonVerification < failClosedLiveRemoval);
|
||||
Assert.Contains(
|
||||
"Development Live initializer returned unexpected success-stream output.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"$Description failed its exact JSON/ACL verification.",
|
||||
initializer,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepositoryDoesNotContainCopiedLegacyRuntimeTrees()
|
||||
{
|
||||
|
||||
@@ -189,7 +189,7 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentFailure_RestoresEveryPreviousProcessValue()
|
||||
public void EnvironmentFailure_DisarmsEveryPreviousProcessValue()
|
||||
{
|
||||
var platform = FakePlatform.Valid();
|
||||
platform.Environment[
|
||||
@@ -209,20 +209,32 @@ public sealed class DevelopmentLiveLaunchBootstrapTests
|
||||
|
||||
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
|
||||
Assert.Equal("development-live-environment-failed", result.Code);
|
||||
Assert.Equal(
|
||||
"old-native",
|
||||
platform.Environment[
|
||||
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable]);
|
||||
Assert.Equal(
|
||||
"old-interop",
|
||||
platform.Environment[
|
||||
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable]);
|
||||
Assert.Equal(
|
||||
"DryRun",
|
||||
platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable]);
|
||||
Assert.Equal(
|
||||
"old-authorization",
|
||||
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable]);
|
||||
Assert.Empty(platform.Environment);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectedExactDebugRequest_DisarmsInheritedLiveGates()
|
||||
{
|
||||
var platform = new FakePlatform(Encoding.UTF8.GetBytes("{}"));
|
||||
platform.Environment[
|
||||
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] =
|
||||
new string('A', 64);
|
||||
platform.Environment[
|
||||
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] =
|
||||
new string('B', 64);
|
||||
platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] =
|
||||
DevelopmentLiveLaunchBootstrap.RequiredMode;
|
||||
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] =
|
||||
PlayoutOptionsLoader.RequiredLiveAuthorizationValue;
|
||||
|
||||
var result = DevelopmentLiveLaunchBootstrap.TryApply(
|
||||
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
|
||||
isDebugBuild: true,
|
||||
"authorization.json",
|
||||
platform);
|
||||
|
||||
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
|
||||
Assert.Empty(platform.Environment);
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidAuthorizationJson() => new()
|
||||
|
||||
@@ -88,6 +88,210 @@ public sealed class PlayoutOptionsLoaderTests
|
||||
Assert.Empty(options.TestSceneAllowlist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_RejectedDevelopmentLiveRequestCannotInheritLiveEnvironment()
|
||||
{
|
||||
using var environment = ClearedEnvironment()
|
||||
.Set("MBN_STOCK_PLAYOUT_MODE", "Live")
|
||||
.Set("MBN_STOCK_PLAYOUT_HOST", "127.0.0.1")
|
||||
.Set(
|
||||
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
|
||||
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
|
||||
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
|
||||
|
||||
var options = PlayoutOptionsLoader.Load(
|
||||
file.Path,
|
||||
operatorSceneDirectory: "C:\\operator-scenes",
|
||||
operatorBackgroundDirectory: "C:\\operator-backgrounds",
|
||||
forceSafeDryRun: true);
|
||||
var validated = ValidatedPlayoutOptions.Create(options);
|
||||
|
||||
Assert.Equal(PlayoutMode.DryRun, validated.Mode);
|
||||
Assert.False(validated.TrustedLiveOutputEnabled);
|
||||
Assert.Null(validated.SceneDirectory);
|
||||
Assert.Empty(validated.TestSceneAllowlist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadDevelopmentLive_IgnoresEnvironmentAndPinsExecutableCuts()
|
||||
{
|
||||
using var environment = ClearedEnvironment()
|
||||
.Set("MBN_STOCK_PLAYOUT_MODE", "Disabled")
|
||||
.Set("MBN_STOCK_PLAYOUT_HOST", "stale-remote-host.invalid")
|
||||
.Set("MBN_STOCK_PLAYOUT_PORT", "32001")
|
||||
.Set("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", "99")
|
||||
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\stale-scenes")
|
||||
.Set(
|
||||
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
|
||||
"C:\\stale-backgrounds")
|
||||
.Set("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "true")
|
||||
.Set("MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS", "9");
|
||||
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
|
||||
var executableRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
Guid.NewGuid().ToString("N"));
|
||||
var cuts = Path.Combine(executableRoot, "Cuts");
|
||||
Directory.CreateDirectory(cuts);
|
||||
try
|
||||
{
|
||||
var options = PlayoutOptionsLoader.LoadDevelopmentLive(
|
||||
file.Path,
|
||||
executableRoot);
|
||||
|
||||
Assert.True(options.DevelopmentLiveBootstrapApplied);
|
||||
Assert.Equal(PlayoutMode.Live, options.Mode);
|
||||
Assert.Equal("127.0.0.1", options.Host);
|
||||
Assert.Equal(31001, options.Port);
|
||||
Assert.Equal(7, options.OutputChannel);
|
||||
Assert.Equal(Path.GetFullPath(cuts), options.SceneDirectory);
|
||||
Assert.Equal(
|
||||
Path.GetFullPath(cuts),
|
||||
options.DevelopmentLiveExecutableSceneDirectory);
|
||||
Assert.Null(options.LegacyBackgroundDirectory);
|
||||
Assert.False(options.ReconnectEnabled);
|
||||
Assert.Equal(0, options.MaximumReconnectAttempts);
|
||||
|
||||
var validated = ValidatedPlayoutOptions.Create(options);
|
||||
Assert.Equal("127.0.0.1", validated.Host);
|
||||
Assert.Equal(Path.GetFullPath(cuts), validated.SceneDirectory);
|
||||
Assert.False(validated.ReconnectEnabled);
|
||||
Assert.Equal(0, validated.MaximumReconnectAttempts);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(executableRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadDevelopmentLive_RejectsBaseProfileThatCanReconnect()
|
||||
{
|
||||
using var environment = ClearedEnvironment();
|
||||
using var file = TemporaryJsonFile.Create(
|
||||
DevelopmentLiveBaseJson().Replace(
|
||||
"\"reconnectEnabled\": false",
|
||||
"\"reconnectEnabled\": true",
|
||||
StringComparison.Ordinal));
|
||||
|
||||
var exception = Assert.Throws<PlayoutConfigurationException>(
|
||||
() => PlayoutOptionsLoader.LoadDevelopmentLive(file.Path, "C:\\runtime"));
|
||||
|
||||
Assert.Contains(
|
||||
"local configuration is invalid",
|
||||
exception.Message,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DevelopmentLiveRequiresLiteralLoopbackAtFinalBoundary()
|
||||
{
|
||||
using var environment = ClearedEnvironment();
|
||||
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
|
||||
var executableRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(Path.Combine(executableRoot, "Cuts"));
|
||||
try
|
||||
{
|
||||
var options = PlayoutOptionsLoader.LoadDevelopmentLive(
|
||||
file.Path,
|
||||
executableRoot);
|
||||
options.Host = "playout.dev.invalid";
|
||||
|
||||
var exception = Assert.Throws<PlayoutConfigurationException>(
|
||||
() => ValidatedPlayoutOptions.Create(options));
|
||||
|
||||
Assert.Contains(
|
||||
nameof(PlayoutOptions.Host),
|
||||
exception.Message,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(executableRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadDevelopmentLive_RejectsNonCanonicalJsonAndAllowlistExpansion()
|
||||
{
|
||||
using var environment = ClearedEnvironment();
|
||||
var canonical = DevelopmentLiveBaseJson();
|
||||
var closingBrace = canonical.LastIndexOf('}');
|
||||
Assert.True(closingBrace > 0);
|
||||
string[] invalidConfigurations =
|
||||
[
|
||||
canonical.Replace(
|
||||
"\"mode\": \"DryRun\"",
|
||||
"\"Mode\": \"DryRun\"",
|
||||
StringComparison.Ordinal),
|
||||
canonical.Replace(
|
||||
"\"mode\": \"DryRun\"",
|
||||
"\"mode\": \"DryRun\", \"mode\": \"DryRun\"",
|
||||
StringComparison.Ordinal),
|
||||
canonical.Replace(
|
||||
"\"port\": 31001",
|
||||
"\"port\": \"31001\"",
|
||||
StringComparison.Ordinal),
|
||||
canonical[..closingBrace] + ",\n}",
|
||||
canonical.Replace("{", "{/* comment */", StringComparison.Ordinal),
|
||||
canonical[..closingBrace] + ",\n \"unexpected\": true\n}",
|
||||
canonical.Replace(
|
||||
"\"N5001\"",
|
||||
"\"N5001\", \"9999\"",
|
||||
StringComparison.Ordinal)
|
||||
];
|
||||
|
||||
foreach (var json in invalidConfigurations)
|
||||
{
|
||||
using var file = TemporaryJsonFile.Create(json);
|
||||
|
||||
var exception = Assert.Throws<PlayoutConfigurationException>(
|
||||
() => PlayoutOptionsLoader.LoadDevelopmentLive(
|
||||
file.Path,
|
||||
"C:\\runtime"));
|
||||
|
||||
Assert.Equal(
|
||||
"Development Live local configuration is invalid.",
|
||||
exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DevelopmentLiveRejectsSceneRootReplacementAfterLoading()
|
||||
{
|
||||
using var environment = ClearedEnvironment();
|
||||
using var file = TemporaryJsonFile.Create(DevelopmentLiveBaseJson());
|
||||
var executableRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
Guid.NewGuid().ToString("N"));
|
||||
var replacementRoot = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(Path.Combine(executableRoot, "Cuts"));
|
||||
Directory.CreateDirectory(replacementRoot);
|
||||
try
|
||||
{
|
||||
var options = PlayoutOptionsLoader.LoadDevelopmentLive(
|
||||
file.Path,
|
||||
executableRoot);
|
||||
options.SceneDirectory = replacementRoot;
|
||||
|
||||
var exception = Assert.Throws<PlayoutConfigurationException>(
|
||||
() => ValidatedPlayoutOptions.Create(options));
|
||||
|
||||
Assert.Contains(
|
||||
"protected startup contract",
|
||||
exception.Message,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(executableRoot, recursive: true);
|
||||
Directory.Delete(replacementRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyExecutableAssetDefaults_UsesCutsBesideExecutable()
|
||||
{
|
||||
@@ -335,4 +539,43 @@ public sealed class PlayoutOptionsLoaderTests
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string DevelopmentLiveBaseJson() =>
|
||||
"""
|
||||
{
|
||||
"mode": "DryRun",
|
||||
"host": "127.0.0.1",
|
||||
"port": 31001,
|
||||
"tcpMode": 1,
|
||||
"clientPort": 0,
|
||||
"sceneDirectory": null,
|
||||
"outputChannel": 7,
|
||||
"layoutIndex": 10,
|
||||
"legacySceneFadeDuration": 6,
|
||||
"legacySceneBackgroundKind": "None",
|
||||
"legacySceneBackgroundAssetPath": null,
|
||||
"legacySceneBackgroundVideoLoopCount": 2004,
|
||||
"legacySceneBackgroundVideoLoopInfinite": true,
|
||||
"legacyBackgroundDirectory": null,
|
||||
"testProcessWindowTitlePattern": null,
|
||||
"testSceneAllowlist": [
|
||||
"5001", "5006", "5011", "5016", "50160", "5023", "5024", "5025",
|
||||
"5026", "5029", "5032", "5037", "5068", "5070", "5072", "5074",
|
||||
"5076", "5077", "5078", "5079", "5080", "5081", "5082", "5083",
|
||||
"5084", "5085", "5086", "50860", "5087", "5088", "6001", "6067",
|
||||
"8001", "8002", "8003", "8018", "8032", "8035", "8040", "8046",
|
||||
"8051", "8056", "8061", "8067", "N5001"
|
||||
],
|
||||
"trustedLiveOutputEnabled": true,
|
||||
"queueCapacity": 64,
|
||||
"connectTimeoutMilliseconds": 5000,
|
||||
"operationTimeoutMilliseconds": 5000,
|
||||
"disconnectTimeoutMilliseconds": 3000,
|
||||
"processPollIntervalMilliseconds": 1000,
|
||||
"reconnectDelayMilliseconds": 1000,
|
||||
"maximumReconnectAttempts": 0,
|
||||
"reconnectEnabled": false,
|
||||
"maximumAutomaticRefreshesPerTakeIn": 0
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@@ -258,6 +258,21 @@ public sealed class PlayoutSafetyValidationTests : IDisposable
|
||||
Assert.Contains(nameof(PlayoutOptions.Host), exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_OrdinaryLiveModeRetainsConfiguredRemoteEndpointBehavior()
|
||||
{
|
||||
var options = ValidTestOptions();
|
||||
options.Mode = PlayoutMode.Live;
|
||||
options.Host = "playout.dev.invalid";
|
||||
options.OutputChannel = null;
|
||||
options.TestProcessWindowTitlePattern = null;
|
||||
options.TrustedLiveOutputEnabled = true;
|
||||
|
||||
var validated = ValidatedPlayoutOptions.Create(options);
|
||||
|
||||
Assert.Equal("playout.dev.invalid", validated.Host);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(".*")]
|
||||
[InlineData("PGM")]
|
||||
|
||||
Reference in New Issue
Block a user