using Google.Protobuf.WellKnownTypes; using MxGateway.Client.Cli; using MxGateway.Contracts.Proto; using MxGateway.Contracts.Proto.Galaxy; namespace MxGateway.Client.Tests; /// Tests for the CLI command interface. public sealed class MxGatewayClientCliTests { /// Verifies that the version command prints compiled protocol versions. [Fact] public void Run_Version_PrintsCompiledProtocolVersions() { using var output = new StringWriter(); using var error = new StringWriter(); var exitCode = MxGatewayClientCli.Run(["version"], output, error); Assert.Equal(0, exitCode); Assert.Contains("gateway-protocol=3", output.ToString()); Assert.Contains("worker-protocol=1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that the version command with --json flag prints JSON protocol versions. [Fact] public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions() { using var output = new StringWriter(); using var error = new StringWriter(); int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error); Assert.Equal(0, exitCode); Assert.Contains("\"gatewayProtocolVersion\":3", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that the write command builds a write request and prints JSON reply. [Fact] public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.InvokeReplies.Enqueue(new MxCommandReply { SessionId = "session-fixture", Kind = MxCommandKind.Write, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "write", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--session-id", "session-fixture", "--server-handle", "12", "--item-handle", "34", "--type", "int32", "--value", "123", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); MxCommandRequest request = Assert.Single(fakeClient.InvokeRequests); Assert.Equal(MxCommandKind.Write, request.Command.Kind); Assert.Equal(123, request.Command.Write.Value.Int32Value); Assert.Contains("MX_COMMAND_KIND_WRITE", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that error output redacts sensitive API key values. [Fact] public async Task RunAsync_ErrorOutput_RedactsApiKey() { using var output = new StringWriter(); using var error = new StringWriter(); int exitCode = await MxGatewayClientCli.RunAsync( [ "open-session", "--endpoint", "http://localhost:5000", "--api-key", "secret-api-key", ], output, error, _ => throw new InvalidOperationException("boom secret-api-key")); Assert.Equal(1, exitCode); Assert.DoesNotContain("secret-api-key", error.ToString()); Assert.Contains("[redacted]", error.ToString()); } /// Verifies that stream-events with max-events limit stops output in non-JSON format. [Fact] public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.Events.Add(new MxEvent { SessionId = "session-fixture", Family = MxEventFamily.OnDataChange, WorkerSequence = 1, }); fakeClient.Events.Add(new MxEvent { SessionId = "session-fixture", Family = MxEventFamily.OnWriteComplete, WorkerSequence = 2, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "stream-events", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--session-id", "session-fixture", "--max-events", "1", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Contains("workerSequence", output.ToString()); Assert.DoesNotContain("ON_WRITE_COMPLETE", output.ToString()); } /// Verifies that smoke command closes opened session when a command fails. [Fact] public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new() { InvokeFailure = new InvalidOperationException("register failed"), }; int exitCode = await MxGatewayClientCli.RunAsync( [ "smoke", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--item", "Area001.Pump001.Speed", "--json", ], output, error, _ => fakeClient); Assert.Equal(1, exitCode); CloseSessionRequest closeRequest = Assert.Single(fakeClient.CloseSessionRequests); Assert.Equal("session-fixture", closeRequest.SessionId); } /// Verifies that galaxy-test-connection command prints JSON reply. [Fact] public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new() { GalaxyTestConnectionReply = new TestConnectionReply { Ok = true }, }; int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-test-connection", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Single(fakeClient.GalaxyTestConnectionRequests); Assert.Contains("\"ok\": true", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that galaxy-discover command prints hierarchy summary. [Fact] public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply { NextPageToken = "7:1", TotalObjectCount = 2, Objects = { new GalaxyObject { GobjectId = 7, TagName = "DelmiaReceiver_001", ContainedName = "DelmiaReceiver", ParentGobjectId = 1, Attributes = { new GalaxyAttribute { AttributeName = "DownloadPath", FullTagReference = "DelmiaReceiver_001.DownloadPath", }, }, }, }, }); fakeClient.GalaxyDiscoverHierarchyReplies.Enqueue(new DiscoverHierarchyReply { TotalObjectCount = 2, Objects = { new GalaxyObject { GobjectId = 8, TagName = "DelmiaReceiver_002", ContainedName = "DelmiaReceiver", ParentGobjectId = 1, }, }, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-discover", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); Assert.Equal(2, fakeClient.GalaxyDiscoverHierarchyRequests.Count); Assert.Equal(5000, fakeClient.GalaxyDiscoverHierarchyRequests[0].PageSize); Assert.Equal("", fakeClient.GalaxyDiscoverHierarchyRequests[0].PageToken); Assert.Equal("7:1", fakeClient.GalaxyDiscoverHierarchyRequests[1].PageToken); string text = output.ToString(); Assert.Contains("objects=2", text); Assert.Contains("DelmiaReceiver_001", text); Assert.Contains("DelmiaReceiver_002", text); Assert.Contains("attributes=1", text); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that galaxy-watch command prints text output for deploy events. [Fact] public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); DateTime deploy = new(2026, 4, 28, 14, 30, 0, DateTimeKind.Utc); fakeClient.GalaxyDeployEvents.Add(new DeployEvent { Sequence = 1, ObservedAt = Timestamp.FromDateTime(deploy), TimeOfLastDeploy = Timestamp.FromDateTime(deploy), TimeOfLastDeployPresent = true, ObjectCount = 5, AttributeCount = 17, }); fakeClient.GalaxyDeployEvents.Add(new DeployEvent { Sequence = 2, ObservedAt = Timestamp.FromDateTime(deploy.AddSeconds(30)), TimeOfLastDeploy = Timestamp.FromDateTime(deploy.AddSeconds(30)), TimeOfLastDeployPresent = true, ObjectCount = 6, AttributeCount = 18, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-watch", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--last-seen-deploy-time", "2026-04-28T14:00:00Z", "--max-events", "2", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); WatchDeployEventsRequest request = Assert.Single(fakeClient.GalaxyWatchDeployEventsRequests); Assert.NotNull(request.LastSeenDeployTime); string text = output.ToString(); Assert.Contains("sequence=1", text); Assert.Contains("sequence=2", text); Assert.Contains("objects=5", text); Assert.Contains("attributes=18", text); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that galaxy-watch with --json emits one JSON object per event. [Fact] public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.GalaxyDeployEvents.Add(new DeployEvent { Sequence = 42, ObjectCount = 99, AttributeCount = 1024, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "galaxy-watch", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--max-events", "1", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); string text = output.ToString(); Assert.Contains("\"sequence\": \"42\"", text); Assert.Contains("\"objectCount\": 99", text); } /// Fake CLI client for testing. private sealed class FakeCliClient : IMxGatewayCliClient { /// Queue of invoke replies to return. public Queue InvokeReplies { get; } = new(); /// List of received invoke requests. public List InvokeRequests { get; } = []; /// List of received close session requests. public List CloseSessionRequests { get; } = []; /// List of events to yield when streaming. public List Events { get; } = []; /// Exception to throw on invoke, if any. public Exception? InvokeFailure { get; init; } /// public ValueTask DisposeAsync() { return ValueTask.CompletedTask; } /// public Task OpenSessionAsync( OpenSessionRequest request, CancellationToken cancellationToken) { return Task.FromResult(new OpenSessionReply { SessionId = "session-fixture", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, GatewayProtocolVersion = 1, WorkerProtocolVersion = 1, }); } /// public Task CloseSessionAsync( CloseSessionRequest request, CancellationToken cancellationToken) { CloseSessionRequests.Add(request); return Task.FromResult(new CloseSessionReply { SessionId = request.SessionId, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, FinalState = SessionState.Closed, }); } /// public Task InvokeAsync( MxCommandRequest request, CancellationToken cancellationToken) { InvokeRequests.Add(request); if (InvokeFailure is not null) { throw InvokeFailure; } return Task.FromResult(InvokeReplies.Dequeue()); } /// public async IAsyncEnumerable StreamEventsAsync( StreamEventsRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { foreach (MxEvent gatewayEvent in Events) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); yield return gatewayEvent; } } /// Galaxy test connection reply to return. public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true }; /// Galaxy get last deploy time reply to return. public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false }; /// Galaxy discover hierarchy reply to return. public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new(); public Queue GalaxyDiscoverHierarchyReplies { get; } = new(); /// List of received galaxy test connection requests. public List GalaxyTestConnectionRequests { get; } = []; /// List of received galaxy get last deploy time requests. public List GalaxyGetLastDeployTimeRequests { get; } = []; /// List of received galaxy discover hierarchy requests. public List GalaxyDiscoverHierarchyRequests { get; } = []; /// public Task GalaxyTestConnectionAsync( TestConnectionRequest request, CancellationToken cancellationToken) { GalaxyTestConnectionRequests.Add(request); return Task.FromResult(GalaxyTestConnectionReply); } /// public Task GalaxyGetLastDeployTimeAsync( GetLastDeployTimeRequest request, CancellationToken cancellationToken) { GalaxyGetLastDeployTimeRequests.Add(request); return Task.FromResult(GalaxyGetLastDeployTimeReply); } /// public Task GalaxyDiscoverHierarchyAsync( DiscoverHierarchyRequest request, CancellationToken cancellationToken) { GalaxyDiscoverHierarchyRequests.Add(request); return Task.FromResult( GalaxyDiscoverHierarchyReplies.TryDequeue(out DiscoverHierarchyReply? reply) ? reply : GalaxyDiscoverHierarchyReply); } /// List of received galaxy watch deploy events requests. public List GalaxyWatchDeployEventsRequests { get; } = []; /// List of deploy events to yield when watching. public List GalaxyDeployEvents { get; } = []; /// public async IAsyncEnumerable GalaxyWatchDeployEventsAsync( WatchDeployEventsRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { GalaxyWatchDeployEventsRequests.Add(request); foreach (DeployEvent deployEvent in GalaxyDeployEvents) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); yield return deployEvent; } } } }