using CliFx.Exceptions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands; using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes; namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests; /// /// Regression tests for Client.CLI-003: numeric command options must validate their ranges /// and report a clean operator-facing error instead of silently passing bad values to the SDK. /// public class CommandRangeValidationTests { /// Verifies that BrowseCommand rejects negative depth values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task BrowseCommand_NegativeDepth_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new BrowseCommand(factory) { Url = "opc.tcp://localhost:4840", Depth = -1 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--depth"); } /// Verifies that BrowseCommand rejects zero depth values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task BrowseCommand_ZeroDepth_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new BrowseCommand(factory) { Url = "opc.tcp://localhost:4840", Depth = 0 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--depth"); } /// Verifies that SubscribeCommand rejects zero interval values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task SubscribeCommand_ZeroInterval_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new SubscribeCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=N", Interval = 0 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--interval"); } /// Verifies that SubscribeCommand rejects negative interval values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task SubscribeCommand_NegativeInterval_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new SubscribeCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=N", Interval = -50 }; using var console = TestConsoleHelper.CreateConsole(); await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); } /// Verifies that SubscribeCommand in recursive mode rejects zero max depth with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task SubscribeCommand_RecursiveZeroMaxDepth_ThrowsCommandException() { // --max-depth only matters when --recursive is set; validation is scoped to that combination. var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new SubscribeCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=N", Recursive = true, MaxDepth = 0 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--max-depth"); } /// Verifies that SubscribeCommand rejects negative duration values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task SubscribeCommand_NegativeDuration_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new SubscribeCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=N", DurationSeconds = -5 }; using var console = TestConsoleHelper.CreateConsole(); await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); } /// Verifies that AlarmsCommand rejects zero interval values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task AlarmsCommand_ZeroInterval_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new AlarmsCommand(factory) { Url = "opc.tcp://localhost:4840", Interval = 0 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--interval"); } /// Verifies that HistoryReadCommand rejects negative max values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task HistoryReadCommand_NegativeMax_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new HistoryReadCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=N", MaxValues = -10 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--max"); } /// Verifies that HistoryReadCommand rejects zero interval values with a command exception. /// A task that represents the asynchronous test. [Fact] public async Task HistoryReadCommand_ZeroInterval_ThrowsCommandException() { var fakeService = new FakeOpcUaClientService(); var factory = new FakeOpcUaClientServiceFactory(fakeService); var command = new HistoryReadCommand(factory) { Url = "opc.tcp://localhost:4840", NodeId = "ns=2;s=N", Aggregate = "Average", IntervalMs = 0 }; using var console = TestConsoleHelper.CreateConsole(); var ex = await Should.ThrowAsync(async () => await command.ExecuteAsync(console)); ex.Message.ShouldContain("--interval"); } }