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

748 lines
29 KiB
C#

using System.Security.Cryptography;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Playout.Safety;
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
public sealed class PlayoutLaunchAuthorizationTests
{
private const string Capability =
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
[Fact]
public void Missing_gate_environment_preserves_the_standard_launch_profile()
{
using var environment = ClearGateEnvironment();
var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment();
Assert.False(authorization.IsGateA);
Assert.True(authorization.AllowsDatabaseWrites);
Assert.True(authorization.AllowsCompositionMutation);
Assert.False(authorization.TryClaimGateAPrepare(Capability));
}
[Fact]
public void Non_gate_capture_preserves_existing_live_authorization_semantics()
{
using var environment = ClearGateEnvironment()
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
using var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment();
Assert.False(authorization.IsGateA);
Assert.True(authorization.IsAuthorizedForThisLaunch);
Assert.Equal(
PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
Environment.GetEnvironmentVariable(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
}
[Fact]
public void Gate_capture_consumes_static_live_bearer_and_second_capture_fails_closed()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
using var environment = GateEnvironment(
Capability,
"41308",
"638878000000000000",
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture))
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
using var first = PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath));
PlayoutLaunchAuthorization? second = null;
Assert.Throws<PlayoutConfigurationException>(() =>
second = PlayoutLaunchAuthorization.CaptureFromEnvironment());
Assert.True(first.IsGateA);
Assert.True(first.IsAuthorizedForThisLaunch);
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
Assert.Null(second);
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
}
[Fact]
public void Gate_environment_is_captured_once_and_cleared_before_browser_startup()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
using var environment = GateEnvironment(
Capability,
"41308",
"638878000000000000",
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));
using var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath));
Assert.True(authorization.IsGateA);
Assert.Equal(41308, authorization.ExpectedPgmProcessId);
Assert.Equal(638878000000000000, authorization.ExpectedPgmStartTimeUtcTicks);
Assert.Equal(expiry, authorization.ExpiresAtUtcTicks);
Assert.False(authorization.AllowsDatabaseWrites);
Assert.False(authorization.AllowsCompositionMutation);
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
environment.Set(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
new string('F', 64));
Assert.True(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimGateAPrepare(new string('F', 64)));
}
[Theory]
[InlineData("lowercase0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab")]
[InlineData("ABCDEF")]
[InlineData(null)]
public void Invalid_or_partial_gate_environment_fails_closed_and_is_cleared(
string? capability)
{
using var environment = ClearGateEnvironment()
.Set(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
capability)
.Set(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
"41308");
Assert.Throws<PlayoutConfigurationException>(
PlayoutLaunchAuthorization.CaptureFromEnvironment);
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
}
[Fact]
public void Malformed_first_capture_permanently_blocks_reinjection_and_clears_every_bearer()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
using var environment = ClearGateEnvironment()
.Set(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
"ABCDEF")
.Set(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
"41308")
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath)));
environment
.Set(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
Capability)
.Set(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
"41308")
.Set(
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable,
"638878000000000000")
.Set(
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable,
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture))
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
PlayoutLaunchAuthorization? second = null;
Assert.Throws<PlayoutConfigurationException>(() =>
second = PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath)));
Assert.Null(second);
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
}
[Fact]
public async Task Concurrent_gate_captures_have_one_success_and_all_later_calls_fail_closed()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
using var environment = GateEnvironment(
Capability,
"41308",
"638878000000000000",
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture))
.Set(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
using var start = new ManualResetEventSlim();
var tasks = Enumerable.Range(0, 8)
.Select(_ => Task.Run(() =>
{
start.Wait();
try
{
return (
Authorization: PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath)),
Error: (Exception?)null);
}
catch (Exception exception)
{
return (Authorization: (PlayoutLaunchAuthorization?)null, Error: exception);
}
}))
.ToArray();
start.Set();
var results = await Task.WhenAll(tasks);
var successful = Assert.Single(results, static result => result.Authorization is not null);
Assert.True(successful.Authorization!.IsGateA);
foreach (var failed in results.Where(static result => result.Authorization is null))
{
Assert.IsType<PlayoutConfigurationException>(failed.Error);
}
successful.Authorization.Dispose();
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable));
}
[Theory]
[InlineData(0L)]
[InlineData(-1L)]
[InlineData(9000000001L)]
public void Expired_or_overlong_gate_environment_fails_closed_and_clears_every_value(
long expiryOffsetTicks)
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + expiryOffsetTicks;
using var environment = GateEnvironment(
Capability,
"41308",
"638878000000000000",
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath)));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable));
Assert.Null(Environment.GetEnvironmentVariable(
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable));
}
[Fact]
public void Gate_scene_wrong_hash_fails_closed_and_does_not_leave_a_lease()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
using var environment = GateEnvironment(
Capability,
"41308",
"638878000000000000",
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
new string('A', 64)));
using var write = File.Open(
scenePath,
FileMode.Open,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete);
Assert.True(write.CanWrite);
}
[Fact]
public void Gate_scene_native_read_lease_blocks_write_and_delete_until_native_shutdown()
{
using var scenes = TemporarySceneDirectory.Create("5001.t2s");
var scenePath = Path.Combine(scenes.Path, "5001.t2s");
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(5).Ticks;
using var environment = GateEnvironment(
Capability,
"41308",
"638878000000000000",
expiry.ToString(System.Globalization.CultureInfo.InvariantCulture));
var authorization = PlayoutLaunchAuthorization.CaptureFromEnvironment(
clock,
scenePath,
SceneHash(scenePath));
using (var secondReader = File.Open(
scenePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read))
{
Assert.True(secondReader.CanRead);
}
Assert.ThrowsAny<IOException>(() => File.Open(
scenePath,
FileMode.Open,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete));
Assert.ThrowsAny<IOException>(() => File.Delete(scenePath));
authorization.CompleteGateAPrepare();
Assert.ThrowsAny<IOException>(() => File.Delete(scenePath));
authorization.Dispose();
File.Delete(scenePath);
Assert.False(File.Exists(scenePath));
}
[Fact]
public void Native_expiry_at_operator_claim_is_terminal_and_cannot_be_rearmed()
{
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
Capability,
timeProvider: clock,
expiresAtUtcTicks: expiry);
clock.Advance(TimeSpan.FromMinutes(1));
Assert.False(authorization.TryClaimGateAPrepare(Capability));
clock.SetUtcNow(clock.GetUtcNow().AddHours(-1));
Assert.False(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Connect,
cue: null));
}
[Fact]
public void Expiry_between_operator_claim_and_engine_prepare_blocks_vendor_boundary()
{
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
Capability,
timeProvider: clock,
expiresAtUtcTicks: expiry);
clock.Advance(TimeSpan.FromMinutes(1) - TimeSpan.FromTicks(1));
Assert.True(authorization.TryClaimGateAPrepare(Capability));
clock.Advance(TimeSpan.FromTicks(1));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
GateACue()));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
GateACue()));
}
[Fact]
public void Connect_just_before_expiry_does_not_extend_later_prepare_authority()
{
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
Capability,
timeProvider: clock,
expiresAtUtcTicks: expiry);
clock.Advance(TimeSpan.FromMinutes(1) - TimeSpan.FromTicks(1));
Assert.True(authorization.TryClaimEngineCommand(
PlayoutOperation.Connect,
cue: null));
clock.Advance(TimeSpan.FromTicks(1));
Assert.False(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
GateACue()));
}
[Fact]
public void Connect_claim_uses_monotonic_deadline_so_utc_rollback_cannot_extend_gate()
{
var clock = new ManualGateTimeProvider();
var expiry = clock.GetUtcNow().UtcTicks + TimeSpan.FromMinutes(1).Ticks;
using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(
Capability,
timeProvider: clock,
expiresAtUtcTicks: expiry);
clock.SetUtcNow(clock.GetUtcNow().AddHours(-1));
clock.AdvanceMonotonic(TimeSpan.FromMinutes(1));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Connect,
cue: null));
Assert.False(authorization.TryClaimGateAPrepare(Capability));
}
[Fact]
public void Capability_and_engine_prepare_are_each_one_shot_and_never_rearm()
{
var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
var cue = GateACue();
Assert.False(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
Assert.False(authorization.TryClaimGateAPrepare(new string('F', 64)));
Assert.True(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimGateAPrepare(Capability));
Assert.True(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
Assert.False(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
authorization.CompleteGateAPrepare();
Assert.False(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimEngineCommand(PlayoutOperation.Prepare, cue));
}
[Fact]
public void Wrong_prepare_target_consumes_the_engine_phase_and_blocks_correction()
{
var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
Assert.True(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
GateACue() with { SceneName = "5006", SceneFile = "5006.t2s" }));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
GateACue()));
}
[Fact]
public void GateA_rejects_operator_fade_and_background_changes_before_engine_dispatch()
{
var blockedCues = new[]
{
GateACue() with { FadeDuration = 5 },
GateACue() with { Mutations = [] },
GateACue() with
{
Mutations = [new PlayoutUseBackground(true)]
},
GateACue() with
{
Mutations =
[
new PlayoutUseBackground(false),
new PlayoutUseBackground(false)
]
},
GateACue() with
{
Mutations = [new PlayoutSetBackgroundTexture("background.png")]
},
GateACue() with
{
Mutations = [new PlayoutSetBackgroundVideo("background.mp4", 0, 1)]
}
};
foreach (var cue in blockedCues)
{
var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
Assert.True(authorization.TryClaimGateAPrepare(Capability));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
cue));
Assert.False(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
GateACue()));
}
}
[Fact]
public async Task Real_5001_provider_cue_with_exact_disabled_background_claims_native_prepare()
{
var provider = new RegisteredLegacySceneCueProvider(
new SinglePageDataSource(new LegacySceneDataPage(
"s5001",
new S5001DomesticStockSceneData(
LegacyDomesticEquityMarket.Kospi,
S5001DomesticStockMode.Current,
S5001NxtBadge.None,
"Samsung Electronics",
ScenePriceDirection.Up,
70_000,
500,
0.72m),
1)));
var page = await provider.CreatePageAsync(
new LegacyPlaylistEntry("gate-a-5001", "5001", FadeDuration: 6),
pageIndexZeroBased: 0);
using var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
Assert.Equal(new PlayoutUseBackground(false), page.Cue.Mutations![0]);
Assert.True(authorization.TryClaimGateAPrepare(Capability));
Assert.True(authorization.TryClaimEngineCommand(
PlayoutOperation.Prepare,
page.Cue));
}
[Fact]
public async Task Concurrent_operator_claims_have_exactly_one_winner()
{
var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
using var start = new ManualResetEventSlim();
var tasks = Enumerable.Range(0, 16)
.Select(_ => Task.Run(() =>
{
start.Wait();
return authorization.TryClaimGateAPrepare(Capability);
}))
.ToArray();
start.Set();
var results = await Task.WhenAll(tasks);
Assert.Single(results, static result => result);
}
[Fact]
public void GateA_options_require_the_exact_closed_profile()
{
var authorization = PlayoutLaunchAuthorization.CreateGateAForTests(Capability);
var valid = GateAOptions();
PlayoutEngineFactory.ValidateGateAOptions(valid, authorization);
foreach (var invalid in new[]
{
GateAOptions().WithMode(PlayoutMode.DryRun),
GateAOptions().WithAllowlist("5001", "5006"),
GateAOptions().WithReconnect(true, 0),
GateAOptions().WithReconnect(false, 1),
GateAOptions().WithRefresh(null),
GateAOptions().WithFade(5),
GateAOptions().WithBackground(LegacySceneBackgroundKind.Texture),
GateAOptions().WithSceneDirectory(Path.GetTempPath()),
GateAOptions().WithClientPort(1),
GateAOptions().WithOutputChannel(0)
})
{
Assert.Throws<PlayoutConfigurationException>(() =>
PlayoutEngineFactory.ValidateGateAOptions(invalid, authorization));
}
}
private static PlayoutCue GateACue() => new(
"5001.t2s",
"5001",
FadeDuration: 6,
Mutations: [new PlayoutUseBackground(false)]);
private static PlayoutOptions GateAOptions() => new()
{
Mode = PlayoutMode.Live,
Host = "127.0.0.1",
Port = 30001,
TcpMode = 1,
LayoutIndex = 10,
TestSceneAllowlist = ["5001"],
TrustedLiveOutputEnabled = true,
ReconnectEnabled = false,
MaximumReconnectAttempts = 0,
MaximumAutomaticRefreshesPerTakeIn = 0,
LegacySceneFadeDuration = 6,
LegacySceneBackgroundKind = LegacySceneBackgroundKind.None,
SceneDirectory = PlayoutLaunchAuthorization.GateAApprovedSceneDirectory
};
private static string SceneHash(string path) =>
Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
private sealed class SinglePageDataSource(LegacySceneDataPage page)
: ILegacySceneDataSource
{
public Task<LegacySceneDataPage> LoadPageAsync(
LegacyPlaylistEntry entry,
int pageIndexZeroBased,
CancellationToken cancellationToken = default) =>
Task.FromResult(page);
}
private static EnvironmentScope ClearGateEnvironment()
{
PlayoutLaunchAuthorization.ResetGateACaptureLatchForTests();
return new EnvironmentScope()
.Set(PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable, null)
.Set(PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable, null)
.Set(PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable, null)
.Set(PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable, null)
.Set(PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable, null);
}
private static EnvironmentScope GateEnvironment(
string capability,
string processId,
string startTimeUtcTicks,
string expiresAtUtcTicks) =>
ClearGateEnvironment()
.Set(PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable, capability)
.Set(PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable, processId)
.Set(PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable, startTimeUtcTicks)
.Set(PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable, expiresAtUtcTicks);
}
internal sealed class ManualGateTimeProvider : TimeProvider
{
private DateTimeOffset _utcNow = new(2026, 7, 18, 0, 0, 0, TimeSpan.Zero);
private long _timestamp;
public override long TimestampFrequency => TimeSpan.TicksPerSecond;
public override DateTimeOffset GetUtcNow() => _utcNow;
public override long GetTimestamp() => _timestamp;
public void Advance(TimeSpan duration)
{
_utcNow = _utcNow.Add(duration);
_timestamp += duration.Ticks;
}
public void AdvanceMonotonic(TimeSpan duration) =>
_timestamp += duration.Ticks;
public void SetUtcNow(DateTimeOffset value) => _utcNow = value;
}
internal static class GateAOptionTestExtensions
{
public static PlayoutOptions WithMode(this PlayoutOptions options, PlayoutMode mode)
{
options.Mode = mode;
return options;
}
public static PlayoutOptions WithAllowlist(
this PlayoutOptions options,
params string[] scenes)
{
options.TestSceneAllowlist = [.. scenes];
return options;
}
public static PlayoutOptions WithReconnect(
this PlayoutOptions options,
bool enabled,
int attempts)
{
options.ReconnectEnabled = enabled;
options.MaximumReconnectAttempts = attempts;
return options;
}
public static PlayoutOptions WithRefresh(this PlayoutOptions options, int? maximum)
{
options.MaximumAutomaticRefreshesPerTakeIn = maximum;
return options;
}
public static PlayoutOptions WithFade(this PlayoutOptions options, int fade)
{
options.LegacySceneFadeDuration = fade;
return options;
}
public static PlayoutOptions WithBackground(
this PlayoutOptions options,
LegacySceneBackgroundKind kind)
{
options.LegacySceneBackgroundKind = kind;
return options;
}
public static PlayoutOptions WithSceneDirectory(
this PlayoutOptions options,
string sceneDirectory)
{
options.SceneDirectory = sceneDirectory;
return options;
}
public static PlayoutOptions WithClientPort(
this PlayoutOptions options,
int clientPort)
{
options.ClientPort = clientPort;
return options;
}
public static PlayoutOptions WithOutputChannel(
this PlayoutOptions options,
int outputChannel)
{
options.OutputChannel = outputChannel;
return options;
}
}