310 lines
11 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|