339 lines
14 KiB
C#
339 lines
14 KiB
C#
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.Playout.Tests;
|
|
|
|
public sealed class PlayoutOptionsLoaderTests
|
|
{
|
|
private static readonly string[] EnvironmentNames =
|
|
[
|
|
"MBN_STOCK_PLAYOUT_MODE",
|
|
"MBN_STOCK_PLAYOUT_HOST",
|
|
"MBN_STOCK_PLAYOUT_PORT",
|
|
"MBN_STOCK_PLAYOUT_TCP_MODE",
|
|
"MBN_STOCK_PLAYOUT_CLIENT_PORT",
|
|
"MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL",
|
|
"MBN_STOCK_PLAYOUT_LAYOUT_INDEX",
|
|
"MBN_STOCK_PLAYOUT_LEGACY_FADE_DURATION",
|
|
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND",
|
|
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET",
|
|
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT",
|
|
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE",
|
|
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
|
|
"MBN_STOCK_PLAYOUT_SCENE_DIRECTORY",
|
|
"MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN",
|
|
"MBN_STOCK_PLAYOUT_QUEUE_CAPACITY",
|
|
"MBN_STOCK_PLAYOUT_CONNECT_TIMEOUT_MS",
|
|
"MBN_STOCK_PLAYOUT_OPERATION_TIMEOUT_MS",
|
|
"MBN_STOCK_PLAYOUT_DISCONNECT_TIMEOUT_MS",
|
|
"MBN_STOCK_PLAYOUT_PROCESS_POLL_INTERVAL_MS",
|
|
"MBN_STOCK_PLAYOUT_RECONNECT_DELAY_MS",
|
|
"MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS",
|
|
"MBN_STOCK_PLAYOUT_RECONNECT_ENABLED",
|
|
"MBN_STOCK_PLAYOUT_MAXIMUM_AUTOMATIC_REFRESHES_PER_TAKE_IN",
|
|
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
|
|
"MBN_STOCK_PLAYOUT_TEST_SCENE_ALLOWLIST",
|
|
"MBN_STOCK_PLAYOUT_TRUSTED_LIVE_OUTPUT_ENABLED"
|
|
];
|
|
|
|
[Fact]
|
|
public void Load_WhenFileIsMissing_UsesSafeDryRunDefaults()
|
|
{
|
|
using var environment = ClearedEnvironment();
|
|
using var file = TemporaryJsonFile.Missing();
|
|
|
|
var options = PlayoutOptionsLoader.Load(file.Path);
|
|
|
|
Assert.Equal(PlayoutMode.DryRun, options.Mode);
|
|
Assert.Equal("127.0.0.1", options.Host);
|
|
Assert.Equal(30001, options.Port);
|
|
Assert.Equal(1, options.TcpMode);
|
|
Assert.Equal(0, options.ClientPort);
|
|
Assert.Null(options.OutputChannel);
|
|
Assert.Null(options.SceneDirectory);
|
|
Assert.Equal(6, options.LegacySceneFadeDuration);
|
|
Assert.Equal(LegacySceneBackgroundKind.None, options.LegacySceneBackgroundKind);
|
|
Assert.Null(options.LegacySceneBackgroundAssetPath);
|
|
Assert.Equal(2004, options.LegacySceneBackgroundVideoLoopCount);
|
|
Assert.True(options.LegacySceneBackgroundVideoLoopInfinite);
|
|
Assert.Null(options.LegacyBackgroundDirectory);
|
|
Assert.Empty(options.TestSceneAllowlist);
|
|
Assert.False(options.TrustedLiveOutputEnabled);
|
|
Assert.True(options.ReconnectEnabled);
|
|
Assert.Null(options.MaximumAutomaticRefreshesPerTakeIn);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_SourceOnlyIgnoresLocalProfileOperatorRootsAndEnvironment()
|
|
{
|
|
using var environment = ClearedEnvironment()
|
|
.Set("MBN_STOCK_PLAYOUT_MODE", "Live")
|
|
.Set("MBN_STOCK_PLAYOUT_HOST", "configured-live-host")
|
|
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\configured-live-scenes")
|
|
.Set(
|
|
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
|
|
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
|
|
using var file = TemporaryJsonFile.Create("{ intentionally-invalid-json");
|
|
|
|
var options = PlayoutOptionsLoader.Load(
|
|
file.Path,
|
|
operatorSceneDirectory: "C:\\operator-scenes",
|
|
operatorBackgroundDirectory: "C:\\operator-backgrounds",
|
|
forceSafeDryRun: true);
|
|
|
|
Assert.Equal(PlayoutMode.DryRun, options.Mode);
|
|
Assert.Equal("127.0.0.1", options.Host);
|
|
Assert.Null(options.SceneDirectory);
|
|
Assert.Null(options.LegacyBackgroundDirectory);
|
|
Assert.False(options.TrustedLiveOutputEnabled);
|
|
Assert.Empty(options.TestSceneAllowlist);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyExecutableAssetDefaults_UsesCutsBesideExecutable()
|
|
{
|
|
var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
|
var cuts = Path.Combine(directory, "Cuts");
|
|
Directory.CreateDirectory(cuts);
|
|
try
|
|
{
|
|
var options = new PlayoutOptions();
|
|
|
|
PlayoutOptionsLoader.ApplyExecutableAssetDefaults(options, directory);
|
|
|
|
Assert.Equal(Path.GetFullPath(cuts), options.SceneDirectory);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyExecutableAssetDefaults_DoesNotReplaceExplicitSceneDirectory()
|
|
{
|
|
var options = new PlayoutOptions
|
|
{
|
|
SceneDirectory = "C:\\explicit-cuts"
|
|
};
|
|
|
|
PlayoutOptionsLoader.ApplyExecutableAssetDefaults(options, "C:\\runtime");
|
|
|
|
Assert.Equal("C:\\explicit-cuts", options.SceneDirectory);
|
|
}
|
|
|
|
[Fact]
|
|
public void ApplyOperatorAssetOverrides_ChangesOnlyNativeAssetRoots()
|
|
{
|
|
var options = new PlayoutOptions
|
|
{
|
|
Mode = PlayoutMode.Live,
|
|
Host = "protected-host",
|
|
SceneDirectory = "C:\\json-cuts",
|
|
LegacyBackgroundDirectory = "C:\\json-backgrounds",
|
|
TrustedLiveOutputEnabled = true,
|
|
TestSceneAllowlist = ["5001"]
|
|
};
|
|
|
|
PlayoutOptionsLoader.ApplyOperatorAssetOverrides(
|
|
options,
|
|
" C:\\operator-cuts ",
|
|
" C:\\operator-backgrounds ");
|
|
|
|
Assert.Equal("C:\\operator-cuts", options.SceneDirectory);
|
|
Assert.Equal("C:\\operator-backgrounds", options.LegacyBackgroundDirectory);
|
|
Assert.Equal(PlayoutMode.Live, options.Mode);
|
|
Assert.Equal("protected-host", options.Host);
|
|
Assert.True(options.TrustedLiveOutputEnabled);
|
|
Assert.Equal(["5001"], options.TestSceneAllowlist);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_EnvironmentAssetRootsOverrideNativeOperatorSelection()
|
|
{
|
|
using var environment = ClearedEnvironment()
|
|
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\environment-cuts")
|
|
.Set(
|
|
"MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY",
|
|
"C:\\environment-backgrounds");
|
|
using var file = TemporaryJsonFile.Missing();
|
|
|
|
var options = PlayoutOptionsLoader.Load(
|
|
file.Path,
|
|
"C:\\operator-cuts",
|
|
"C:\\operator-backgrounds");
|
|
|
|
Assert.Equal("C:\\environment-cuts", options.SceneDirectory);
|
|
Assert.Equal("C:\\environment-backgrounds", options.LegacyBackgroundDirectory);
|
|
Assert.Equal(PlayoutMode.DryRun, options.Mode);
|
|
Assert.False(options.TrustedLiveOutputEnabled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_EnvironmentOverridesJson_ExceptFileOnlySafetyGates()
|
|
{
|
|
using var environment = ClearedEnvironment()
|
|
.Set("MBN_STOCK_PLAYOUT_MODE", "DryRun")
|
|
.Set("MBN_STOCK_PLAYOUT_HOST", "env-host")
|
|
.Set("MBN_STOCK_PLAYOUT_PORT", "32001")
|
|
.Set("MBN_STOCK_PLAYOUT_TCP_MODE", "2")
|
|
.Set("MBN_STOCK_PLAYOUT_CLIENT_PORT", "32002")
|
|
.Set("MBN_STOCK_PLAYOUT_OUTPUT_CHANNEL", "12")
|
|
.Set("MBN_STOCK_PLAYOUT_LAYOUT_INDEX", "13")
|
|
.Set("MBN_STOCK_PLAYOUT_LEGACY_FADE_DURATION", "9")
|
|
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND", "Video")
|
|
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_ASSET", "Video\\studio.vrv")
|
|
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_COUNT", "2004")
|
|
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_VIDEO_LOOP_INFINITE", "false")
|
|
.Set("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_DIRECTORY", "C:\\env-backgrounds")
|
|
.Set("MBN_STOCK_PLAYOUT_SCENE_DIRECTORY", "C:\\env-scenes")
|
|
.Set("MBN_STOCK_PLAYOUT_TEST_WINDOW_TITLE_PATTERN", "ENV TEST")
|
|
.Set("MBN_STOCK_PLAYOUT_QUEUE_CAPACITY", "14")
|
|
.Set("MBN_STOCK_PLAYOUT_CONNECT_TIMEOUT_MS", "1501")
|
|
.Set("MBN_STOCK_PLAYOUT_OPERATION_TIMEOUT_MS", "1502")
|
|
.Set("MBN_STOCK_PLAYOUT_DISCONNECT_TIMEOUT_MS", "1503")
|
|
.Set("MBN_STOCK_PLAYOUT_PROCESS_POLL_INTERVAL_MS", "1504")
|
|
.Set("MBN_STOCK_PLAYOUT_RECONNECT_DELAY_MS", "1505")
|
|
.Set("MBN_STOCK_PLAYOUT_MAXIMUM_RECONNECT_ATTEMPTS", "4")
|
|
.Set("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "false")
|
|
.Set("MBN_STOCK_PLAYOUT_MAXIMUM_AUTOMATIC_REFRESHES_PER_TAKE_IN", "999")
|
|
.Set("MBN_STOCK_PLAYOUT_TEST_SCENE_ALLOWLIST", "C:\\env\\must-not-apply.t2s")
|
|
.Set("MBN_STOCK_PLAYOUT_TRUSTED_LIVE_OUTPUT_ENABLED", "false");
|
|
using var file = TemporaryJsonFile.Create(
|
|
"""
|
|
{
|
|
"mode": "Test",
|
|
"host": "json-host",
|
|
"port": 31001,
|
|
"tcpMode": 1,
|
|
"clientPort": 31002,
|
|
"outputChannel": 3,
|
|
"layoutIndex": 10,
|
|
"sceneDirectory": "C:\\json-scenes",
|
|
"testProcessWindowTitlePattern": "JSON TEST",
|
|
"testSceneAllowlist": ["C:\\test-scenes\\allowed.t2s"],
|
|
"trustedLiveOutputEnabled": true,
|
|
"queueCapacity": 64,
|
|
"connectTimeoutMilliseconds": 5000,
|
|
"operationTimeoutMilliseconds": 5000,
|
|
"disconnectTimeoutMilliseconds": 3000,
|
|
"processPollIntervalMilliseconds": 1000,
|
|
"reconnectDelayMilliseconds": 1000,
|
|
"maximumReconnectAttempts": 3,
|
|
"reconnectEnabled": true,
|
|
"maximumAutomaticRefreshesPerTakeIn": 1
|
|
}
|
|
""");
|
|
|
|
var options = PlayoutOptionsLoader.Load(file.Path);
|
|
|
|
Assert.Equal(PlayoutMode.DryRun, options.Mode);
|
|
Assert.Equal("env-host", options.Host);
|
|
Assert.Equal(32001, options.Port);
|
|
Assert.Equal(2, options.TcpMode);
|
|
Assert.Equal(32002, options.ClientPort);
|
|
Assert.Equal(12, options.OutputChannel);
|
|
Assert.Equal(13, options.LayoutIndex);
|
|
Assert.Equal(9, options.LegacySceneFadeDuration);
|
|
Assert.Equal(LegacySceneBackgroundKind.Video, options.LegacySceneBackgroundKind);
|
|
Assert.Equal("Video\\studio.vrv", options.LegacySceneBackgroundAssetPath);
|
|
Assert.Equal(2004, options.LegacySceneBackgroundVideoLoopCount);
|
|
Assert.False(options.LegacySceneBackgroundVideoLoopInfinite);
|
|
Assert.Equal("C:\\env-backgrounds", options.LegacyBackgroundDirectory);
|
|
Assert.Equal("C:\\env-scenes", options.SceneDirectory);
|
|
Assert.Equal("ENV TEST", options.TestProcessWindowTitlePattern);
|
|
Assert.Equal(14, options.QueueCapacity);
|
|
Assert.Equal(1501, options.ConnectTimeoutMilliseconds);
|
|
Assert.Equal(1502, options.OperationTimeoutMilliseconds);
|
|
Assert.Equal(1503, options.DisconnectTimeoutMilliseconds);
|
|
Assert.Equal(1504, options.ProcessPollIntervalMilliseconds);
|
|
Assert.Equal(1505, options.ReconnectDelayMilliseconds);
|
|
Assert.Equal(4, options.MaximumReconnectAttempts);
|
|
Assert.False(options.ReconnectEnabled);
|
|
Assert.Equal(1, options.MaximumAutomaticRefreshesPerTakeIn);
|
|
Assert.Equal(["C:\\test-scenes\\allowed.t2s"], options.TestSceneAllowlist);
|
|
Assert.True(options.TrustedLiveOutputEnabled);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadFileOnly_IgnoresEnvironmentForReproducibleExplicitDiagnostics()
|
|
{
|
|
using var environment = ClearedEnvironment()
|
|
.Set("MBN_STOCK_PLAYOUT_MODE", "Live")
|
|
.Set("MBN_STOCK_PLAYOUT_HOST", "sensitive-host");
|
|
using var file = TemporaryJsonFile.Create(
|
|
"""
|
|
{
|
|
"mode": "Test",
|
|
"host": "127.0.0.1"
|
|
}
|
|
""");
|
|
|
|
var options = PlayoutOptionsLoader.LoadFileOnly(file.Path);
|
|
|
|
Assert.Equal(PlayoutMode.Test, options.Mode);
|
|
Assert.Equal("127.0.0.1", options.Host);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_InvalidJson_ReturnsOnlySafeConfigurationMessage()
|
|
{
|
|
using var environment = ClearedEnvironment();
|
|
using var file = TemporaryJsonFile.Create("{ invalid-json: true }");
|
|
|
|
var exception = Assert.Throws<PlayoutConfigurationException>(
|
|
() => PlayoutOptionsLoader.Load(file.Path));
|
|
|
|
Assert.Equal("송출 설정 파일 형식이 올바르지 않습니다.", exception.Message);
|
|
Assert.DoesNotContain(file.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("invalid-json", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_StringAutomaticRefreshMaximumFailsClosed()
|
|
{
|
|
using var environment = ClearedEnvironment();
|
|
using var file = TemporaryJsonFile.Create(
|
|
"""
|
|
{
|
|
"maximumAutomaticRefreshesPerTakeIn": "1"
|
|
}
|
|
""");
|
|
|
|
var exception = Assert.Throws<PlayoutConfigurationException>(
|
|
() => PlayoutOptionsLoader.Load(file.Path));
|
|
|
|
Assert.DoesNotContain(file.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("\"1\"", exception.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("MBN_STOCK_PLAYOUT_MODE", "not-a-mode")]
|
|
[InlineData("MBN_STOCK_PLAYOUT_LEGACY_BACKGROUND_KIND", "not-a-background")]
|
|
[InlineData("MBN_STOCK_PLAYOUT_PORT", "not-a-number")]
|
|
[InlineData("MBN_STOCK_PLAYOUT_RECONNECT_ENABLED", "not-a-bool")]
|
|
public void Load_InvalidEnvironmentValue_IdentifiesVariableWithoutLeakingValue(
|
|
string name,
|
|
string invalidValue)
|
|
{
|
|
using var environment = ClearedEnvironment().Set(name, invalidValue);
|
|
using var file = TemporaryJsonFile.Missing();
|
|
|
|
var exception = Assert.Throws<PlayoutConfigurationException>(
|
|
() => PlayoutOptionsLoader.Load(file.Path));
|
|
|
|
Assert.Contains(name, exception.Message, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(invalidValue, exception.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static EnvironmentScope ClearedEnvironment()
|
|
{
|
|
var result = new EnvironmentScope();
|
|
foreach (var name in EnvironmentNames)
|
|
{
|
|
result.Set(name, null);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|