Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.Playout.Tests/DevelopmentLiveLaunchBootstrapTests.cs

310 lines
11 KiB
C#

using System.Security;
using System.Text;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
public sealed class DevelopmentLiveLaunchBootstrapTests
{
private const string NativeSha256 =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private const string InteropSha256 =
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
[Theory]
[InlineData("--development-live", "ignored", "--development-live")]
[InlineData("unexpected", "--development-live", "unexpected")]
[InlineData(" ", "--development-live", " ")]
public void ResolveLaunchArguments_PreservesNonEmptyActivationArguments(
string activationArguments,
string processArgument,
string expected)
{
Assert.Equal(
expected,
DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
activationArguments,
["app.exe", processArgument]));
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void ResolveLaunchArguments_UsesOneProcessArgumentWhenActivationIsEmpty(
string? activationArguments)
{
Assert.Equal(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
activationArguments,
["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument]));
}
[Fact]
public void ResolveLaunchArguments_RejectsMissingOrAdditionalProcessArguments()
{
Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments("", ["app.exe"]));
Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
"",
["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, "extra"]));
}
[Fact]
public void DefaultPath_IsASeparateLocalApplicationDataFile()
{
var expected = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MBN_STOCK_WEBVIEW",
"Config",
"playout.development-live.local.json");
Assert.Equal(expected, DevelopmentLiveLaunchBootstrap.DefaultPath);
Assert.NotEqual(PlayoutOptionsLoader.DefaultPath,
DevelopmentLiveLaunchBootstrap.DefaultPath);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("--development-live ")]
[InlineData(" --development-live")]
[InlineData("--DEVELOPMENT-LIVE")]
[InlineData("--development-live --another-option")]
public void NonExactArgument_SkipsWithoutReadingOrMutating(string? arguments)
{
var platform = FakePlatform.Valid();
var result = DevelopmentLiveLaunchBootstrap.TryApply(
arguments,
isDebugBuild: true,
"ignored.json",
platform);
Assert.Equal(DevelopmentLiveBootstrapStatus.Skipped, result.Status);
Assert.Equal("development-live-not-requested", result.Code);
Assert.Equal(0, platform.ReadCount);
Assert.Empty(platform.Environment);
}
[Fact]
public void ReleaseBuild_SkipsExactArgumentWithoutReadingOrMutating()
{
var platform = FakePlatform.Valid();
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: false,
"ignored.json",
platform);
Assert.Equal(DevelopmentLiveBootstrapStatus.Skipped, result.Status);
Assert.Equal("development-live-debug-only", result.Code);
Assert.Equal(0, platform.ReadCount);
Assert.Empty(platform.Environment);
}
[Fact]
public void DebugExactArgumentAndStrictFile_ApplyAllProcessGates()
{
var platform = FakePlatform.Valid();
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: true,
"authorization.json",
platform);
Assert.True(result.IsApplied);
Assert.Equal(DevelopmentLiveBootstrapStatus.Applied, result.Status);
Assert.Equal("development-live-applied", result.Code);
Assert.Equal(1, platform.ReadCount);
Assert.Equal(
NativeSha256.ToUpperInvariant(),
platform.Environment[
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable]);
Assert.Equal(
InteropSha256,
platform.Environment[
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable]);
Assert.Equal(
DevelopmentLiveLaunchBootstrap.RequiredMode,
platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable]);
Assert.Equal(
PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable]);
}
[Theory]
[MemberData(nameof(InvalidAuthorizationJson))]
public void InvalidOrNonCanonicalAuthorization_IsRejectedWithoutEnvironmentMutation(
string json)
{
var platform = new FakePlatform(Encoding.UTF8.GetBytes(json));
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: true,
"authorization.json",
platform);
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
Assert.Equal("development-live-authorization-invalid", result.Code);
Assert.Empty(platform.Environment);
}
[Fact]
public void OversizedAuthorization_IsRejectedBeforeParsing()
{
var platform = new FakePlatform(
new byte[DevelopmentLiveLaunchBootstrap.MaximumAuthorizationFileBytes + 1]);
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: true,
"authorization.json",
platform);
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
Assert.Equal("development-live-authorization-invalid", result.Code);
Assert.Empty(platform.Environment);
}
[Fact]
public void PermissionFailure_IsFailClosedAndDoesNotLeakDetails()
{
var platform = FakePlatform.Valid();
platform.ReadException = new UnauthorizedAccessException("sensitive local path");
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: true,
"sensitive local path",
platform);
Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
Assert.Equal("development-live-authorization-unavailable", result.Code);
Assert.DoesNotContain("sensitive", result.Code, StringComparison.OrdinalIgnoreCase);
Assert.Empty(platform.Environment);
}
[Fact]
public void EnvironmentFailure_RestoresEveryPreviousProcessValue()
{
var platform = FakePlatform.Valid();
platform.Environment[
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] = "old-native";
platform.Environment[
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] = "old-interop";
platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] = "DryRun";
platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] =
"old-authorization";
platform.FailOnceOnSetName = DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable;
var result = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
isDebugBuild: true,
"authorization.json",
platform);
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]);
}
public static TheoryData<string> InvalidAuthorizationJson() => new()
{
"{}",
ValidJson().Replace("\"schemaVersion\": 1", "\"schemaVersion\": 2"),
ValidJson().Replace("\"mode\": \"Live\"", "\"mode\": \"live\""),
ValidJson().Replace(
PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
"I_AUTHORIZE_SOMETHING_ELSE"),
ValidJson().Replace(NativeSha256, new string('A', 63)),
ValidJson().Replace(InteropSha256, new string('G', 64)),
ValidJson().Replace(
"\"interopSha256\"",
"\"unexpected\": true, \"interopSha256\""),
ValidJson().Replace(
"\"mode\": \"Live\"",
"\"mode\": \"Live\", \"mode\": \"Live\""),
ValidJson().Replace("\"mode\"", "\"Mode\""),
ValidJson().TrimEnd('}') + ",}",
ValidJson().Replace("{", "{/* comment */", StringComparison.Ordinal),
"[]",
"null"
};
private static string ValidJson() =>
$$"""
{
"schemaVersion": 1,
"mode": "Live",
"authorization": "{{PlayoutOptionsLoader.RequiredLiveAuthorizationValue}}",
"nativeSha256": "{{NativeSha256}}",
"interopSha256": "{{InteropSha256}}"
}
""";
private sealed class FakePlatform(byte[] authorizationBytes) :
IDevelopmentLiveBootstrapPlatform
{
public Dictionary<string, string?> Environment { get; } =
new(StringComparer.Ordinal);
public Exception? ReadException { get; set; }
public string? FailOnceOnSetName { get; set; }
public int ReadCount { get; private set; }
public static FakePlatform Valid() =>
new(Encoding.UTF8.GetBytes(ValidJson()));
public byte[] ReadAuthorizationFile(string path, int maximumBytes)
{
ReadCount++;
if (ReadException is not null)
{
throw ReadException;
}
return authorizationBytes;
}
public string? GetProcessEnvironmentVariable(string name) =>
Environment.GetValueOrDefault(name);
public void SetProcessEnvironmentVariable(string name, string? value)
{
if (string.Equals(name, FailOnceOnSetName, StringComparison.Ordinal))
{
FailOnceOnSetName = null;
throw new SecurityException("simulated environment denial");
}
if (value is null)
{
Environment.Remove(name);
}
else
{
Environment[name] = value;
}
}
}
}