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_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", 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.Empty(options.TestSceneAllowlist); Assert.False(options.TrustedLiveOutputEnabled); Assert.True(options.ReconnectEnabled); } [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_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_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 } """); 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("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(["C:\\test-scenes\\allowed.t2s"], options.TestSceneAllowlist); Assert.True(options.TrustedLiveOutputEnabled); } [Fact] public void Load_InvalidJson_ReturnsOnlySafeConfigurationMessage() { using var environment = ClearedEnvironment(); using var file = TemporaryJsonFile.Create("{ invalid-json: true }"); var exception = Assert.Throws( () => PlayoutOptionsLoader.Load(file.Path)); Assert.Equal("송출 설정 파일 형식이 올바르지 않습니다.", exception.Message); Assert.DoesNotContain(file.Path, exception.Message, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("invalid-json", exception.Message, StringComparison.OrdinalIgnoreCase); } [Theory] [InlineData("MBN_STOCK_PLAYOUT_MODE", "not-a-mode")] [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( () => 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; } }