using MxGateway.Client.Cli; using MxGateway.Contracts.Proto; namespace MxGateway.Client.Tests; public sealed class MxGatewayClientCliTests { [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=1", output.ToString()); Assert.Contains("worker-protocol=1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } [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\":1", output.ToString()); Assert.Equal(string.Empty, error.ToString()); } [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()); } [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()); } [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()); } [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); } private sealed class FakeCliClient : IMxGatewayCliClient { public Queue InvokeReplies { get; } = new(); public List InvokeRequests { get; } = []; public List CloseSessionRequests { get; } = []; public List Events { get; } = []; 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; } } } }