using MBN_STOCK_WEBVIEW.PlayoutSmoke; namespace MBN_STOCK_WEBVIEW.Playout.Tests; public sealed class SmokeCommandLineTests { [Theory] [InlineData("Tornado2 TEST", false)] [InlineData("TEST-1", false)] [InlineData("Tornado2", true)] [InlineData("PGM", true)] [InlineData("CONTEST", true)] [InlineData("", true)] [InlineData(null, true)] public void TornadoWindowTitleWithoutExplicitTestMarkerIsUnsafe( string? title, bool expected) { Assert.Equal(expected, SmokeCommandLine.IsUnsafeTornadoWindowTitle(title)); } [Fact] public void Parse_NoArguments_PreservesReadOnlyProbeDefault() { var parsed = SmokeCommandLine.Parse([]); Assert.True(parsed.IsValid); Assert.Equal(SmokeCommandKind.Probe, parsed.Invocation.Kind); } [Theory] [InlineData("--probe", "Probe")] [InlineData("--PROBE", "Probe")] [InlineData("--dry-run", "DryRun")] public void Parse_ExistingComFreeCommandsRemainAvailable( string argument, string expected) { var parsed = SmokeCommandLine.Parse([argument]); Assert.True(parsed.IsValid); Assert.Equal(expected, parsed.Invocation.Kind.ToString()); } [Fact] public void Parse_TestConnectRequiresExplicitAcknowledgementAndConfigArgument() { var withoutAcknowledgement = SmokeCommandLine.Parse( ["--test-connect", "--config", "C:\\safe\\playout.local.json"]); var complete = SmokeCommandLine.Parse( [ "--test-connect", SmokeCommandLine.AcknowledgementFlag, "--config", "C:\\safe\\playout.local.json" ]); Assert.False(withoutAcknowledgement.IsValid); Assert.Equal("missing-acknowledgement", withoutAcknowledgement.ErrorCode); Assert.True(complete.IsValid); Assert.Equal(SmokeCommandKind.TestConnect, complete.Invocation.Kind); } [Fact] public void Parse_TestSequenceRequiresBothSceneCodesAndBoundedObservation() { var missingObservation = SmokeCommandLine.Parse( [ "--test-sequence", SmokeCommandLine.AcknowledgementFlag, "--config", "C:\\safe\\playout.local.json", "--prepare", "5001", "--next", "5006" ]); var complete = SmokeCommandLine.Parse( [ "--test-sequence", "--prepare", "5001", "--observe-ms", "1000", SmokeCommandLine.AcknowledgementFlag, "--next", "5006", "--config", "C:\\safe\\playout.local.json" ]); Assert.False(missingObservation.IsValid); Assert.Equal("missing-required-option", missingObservation.ErrorCode); Assert.True(complete.IsValid); Assert.Equal(1000, complete.Invocation.ObservationMilliseconds); } [Theory] [InlineData("999")] [InlineData("30001")] [InlineData("1.5")] [InlineData("secret")] public void Parse_RejectsUnboundedOrNonIntegralObservation(string value) { var parsed = ValidSequenceArguments(observation: value); Assert.False(parsed.IsValid); Assert.Equal("invalid-observation-duration", parsed.ErrorCode); } [Theory] [InlineData("../5001")] [InlineData("5001.t2s")] [InlineData("장면")] [InlineData("SCENE A")] public void Parse_RejectsUnsafeSceneCodesWithoutReflectingThem(string sceneCode) { var parsed = ValidSequenceArguments(prepare: sceneCode); Assert.False(parsed.IsValid); Assert.Equal("unsafe-scene-code", parsed.ErrorCode); Assert.DoesNotContain(sceneCode, parsed.ErrorCode, StringComparison.OrdinalIgnoreCase); } [Fact] public void Parse_TestConnectRejectsMutationOptions() { var parsed = SmokeCommandLine.Parse( [ "--test-connect", SmokeCommandLine.AcknowledgementFlag, "--config", "C:\\safe\\playout.local.json", "--prepare", "5001" ]); Assert.False(parsed.IsValid); Assert.Equal("unknown-option", parsed.ErrorCode); } [Fact] public void Parse_TestSequenceRequiresTwoDistinctSceneCodes() { var parsed = SmokeCommandLine.Parse( [ "--test-sequence", SmokeCommandLine.AcknowledgementFlag, "--config", "C:\\safe\\playout.local.json", "--prepare", "5001", "--next", "5001", "--observe-ms", "1000" ]); Assert.False(parsed.IsValid); Assert.Equal("duplicate-scene-code", parsed.ErrorCode); } [Fact] public void Parse_TestPlanUsesTheSameSequenceInputsButNeverImpliesExecution() { var parsed = SmokeCommandLine.Parse( [ "--test-plan", SmokeCommandLine.AcknowledgementFlag, "--config", "C:\\safe\\playout.local.json", "--prepare", "5001", "--next", "5006", "--observe-ms", "5000" ]); Assert.True(parsed.IsValid); Assert.Equal(SmokeCommandKind.TestPlan, parsed.Invocation.Kind); } [Fact] public void Parse_PgmConnectDiagnosticRequiresDistinctAcknowledgementAndExactInputs() { string[] complete = [ "--pgm-connect-diagnostic", SmokeCommandLine.PgmConnectAcknowledgementFlag, "--host", "127.0.0.1", "--port", "30001", "--expected-pgm-window-title", "PGM" ]; var missingAcknowledgement = SmokeCommandLine.Parse(complete[0..1] .Concat(complete[2..]) .ToArray()); var wrongAcknowledgement = SmokeCommandLine.Parse( complete.Select(value => value == SmokeCommandLine.PgmConnectAcknowledgementFlag ? SmokeCommandLine.AcknowledgementFlag : value).ToArray()); var parsed = SmokeCommandLine.Parse(complete); Assert.False(missingAcknowledgement.IsValid); Assert.Equal("missing-acknowledgement", missingAcknowledgement.ErrorCode); Assert.False(wrongAcknowledgement.IsValid); Assert.Equal("unknown-option", wrongAcknowledgement.ErrorCode); Assert.True(parsed.IsValid); Assert.Equal(SmokeCommandKind.PgmConnectDiagnostic, parsed.Invocation.Kind); Assert.Equal("127.0.0.1", parsed.Invocation.Host); Assert.Equal(30001, parsed.Invocation.Port); Assert.Equal("PGM", parsed.Invocation.ExpectedPgmWindowTitle); Assert.Null(parsed.Invocation.ConfigPath); } [Theory] [InlineData("--config")] [InlineData("--prepare")] [InlineData("--next")] [InlineData("--observe-ms")] public void Parse_PgmConnectDiagnosticRejectsEveryConfigOrRenderOption(string option) { var parsed = SmokeCommandLine.Parse( [ "--pgm-connect-diagnostic", SmokeCommandLine.PgmConnectAcknowledgementFlag, "--host", "127.0.0.1", "--port", "30001", "--expected-pgm-window-title", "PGM", option, "5001" ]); Assert.False(parsed.IsValid); Assert.Equal("unknown-option", parsed.ErrorCode); } [Theory] [InlineData("0")] [InlineData("65536")] [InlineData("secret")] public void Parse_PgmConnectDiagnosticRejectsInvalidPort(string port) { var parsed = SmokeCommandLine.Parse( [ "--pgm-connect-diagnostic", SmokeCommandLine.PgmConnectAcknowledgementFlag, "--host", "127.0.0.1", "--port", port, "--expected-pgm-window-title", "PGM" ]); Assert.False(parsed.IsValid); Assert.Equal("invalid-port", parsed.ErrorCode); } [Fact] public void Parse_PgmCutsSequenceRequiresItsOwnLongAcknowledgementAndFixedInputs() { var complete = ValidPgmCutsArguments(); var missingAcknowledgement = SmokeCommandLine.Parse( complete.Where(value => value != SmokeCommandLine.PgmCutsSequenceAcknowledgementFlag) .ToArray()); var wrongAcknowledgement = SmokeCommandLine.Parse( complete.Select(value => value == SmokeCommandLine.PgmCutsSequenceAcknowledgementFlag ? SmokeCommandLine.AcknowledgementFlag : value).ToArray()); var parsed = SmokeCommandLine.Parse(complete); var explicitObservation = SmokeCommandLine.Parse( complete.Concat(["--observe-ms", "5000"]).ToArray()); Assert.False(missingAcknowledgement.IsValid); Assert.Equal("missing-acknowledgement", missingAcknowledgement.ErrorCode); Assert.False(wrongAcknowledgement.IsValid); Assert.Equal("unknown-option", wrongAcknowledgement.ErrorCode); Assert.True(parsed.IsValid); Assert.True(explicitObservation.IsValid); Assert.Equal(SmokeCommandKind.PgmCutsSequence, parsed.Invocation.Kind); Assert.Equal(5_000, parsed.Invocation.ObservationMilliseconds); Assert.Equal("C:\\Cuts", parsed.Invocation.SceneRoot); Assert.Null(parsed.Invocation.ConfigPath); Assert.Null(parsed.Invocation.PrepareSceneCode); Assert.Null(parsed.Invocation.NextSceneCode); } [Theory] [InlineData("--config")] [InlineData("--prepare")] [InlineData("--next")] public void Parse_PgmCutsSequenceRejectsConfigAndCallerSelectedScenes(string option) { var parsed = SmokeCommandLine.Parse( ValidPgmCutsArguments().Concat([option, "sensitive-value"]).ToArray()); Assert.False(parsed.IsValid); Assert.Equal("unknown-option", parsed.ErrorCode); } [Theory] [InlineData("4999")] [InlineData("5001")] [InlineData("secret")] public void Parse_PgmCutsSequenceOnlyAcceptsExplicitFiveSecondObservation(string value) { var parsed = SmokeCommandLine.Parse( ValidPgmCutsArguments().Concat(["--observe-ms", value]).ToArray()); Assert.False(parsed.IsValid); Assert.Equal("invalid-observation-duration", parsed.ErrorCode); } [Theory] [InlineData("localhost", "C:\\Cuts")] [InlineData("192.0.2.1", "C:\\Cuts")] [InlineData("127.0.0.1", "relative-cuts")] public void Parse_PgmCutsSequenceRequiresLiteralLoopbackAndAbsoluteSceneRoot( string host, string sceneRoot) { var arguments = ValidPgmCutsArguments(); arguments[Array.IndexOf(arguments, "127.0.0.1")] = host; arguments[Array.IndexOf(arguments, "C:\\Cuts")] = sceneRoot; var parsed = SmokeCommandLine.Parse(arguments); Assert.False(parsed.IsValid); Assert.Equal("missing-or-invalid-required-option", parsed.ErrorCode); } private static SmokeCommandParseResult ValidSequenceArguments( string prepare = "5001", string observation = "1000") => SmokeCommandLine.Parse( [ "--test-sequence", SmokeCommandLine.AcknowledgementFlag, "--config", "C:\\safe\\playout.local.json", "--prepare", prepare, "--next", "5006", "--observe-ms", observation ]); private static string[] ValidPgmCutsArguments() => [ "--pgm-cuts-sequence", SmokeCommandLine.PgmCutsSequenceAcknowledgementFlag, "--host", "127.0.0.1", "--port", "30001", "--expected-pgm-window-title", "Tornado2 PGM", "--scene-root", "C:\\Cuts" ]; }