using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.MxGateway.Client.Cli; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.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 stream-alarms with --max-events stops output and distinguishes payload cases. [Fact] public async Task RunAsync_StreamAlarms_WithMaxEventsStopsAndDistinguishesPayloadCases() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.AlarmFeedMessages.Add(new AlarmFeedMessage { ActiveAlarm = new ActiveAlarmSnapshot { AlarmFullReference = "Tank01.Level.HiHi" }, }); fakeClient.AlarmFeedMessages.Add(new AlarmFeedMessage { SnapshotComplete = true }); int exitCode = await MxGatewayClientCli.RunAsync( [ "stream-alarms", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--filter-prefix", "Tank01", "--max-events", "1", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); StreamAlarmsRequest request = Assert.Single(fakeClient.StreamAlarmsRequests); Assert.Equal("Tank01", request.AlarmFilterPrefix); string text = output.ToString(); Assert.Contains("active-alarm", text); Assert.Contains("Tank01.Level.HiHi", text); Assert.DoesNotContain("snapshot-complete", text); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that acknowledge-alarm builds a request and prints the JSON reply. [Fact] public async Task RunAsync_AcknowledgeAlarm_BuildsRequestAndPrintsJsonReply() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.AcknowledgeAlarmReplies.Enqueue(new AcknowledgeAlarmReply { CorrelationId = "ack-fixture", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "acknowledge-alarm", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--reference", "Tank01.Level.HiHi", "--comment", "ack from cli", "--operator", "operator1", "--json", ], output, error, _ => fakeClient); Assert.Equal(0, exitCode); AcknowledgeAlarmRequest request = Assert.Single(fakeClient.AcknowledgeAlarmRequests); Assert.Equal("Tank01.Level.HiHi", request.AlarmFullReference); Assert.Equal("ack from cli", request.Comment); Assert.Equal("operator1", request.OperatorUser); Assert.Contains("ack-fixture", output.ToString()); Assert.Equal(string.Empty, error.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); } /// Verifies that batch mode dispatches a single version command and emits the EOR sentinel. [Fact] public async Task RunAsync_Batch_DispatchesVersionAndWritesEndOfRecord() { using var output = new StringWriter(); using var error = new StringWriter(); using var input = new StringReader("version --json\n"); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: null, standardInput: input); Assert.Equal(0, exitCode); string text = output.ToString(); Assert.Contains("\"gatewayProtocolVersion\":3", text); Assert.Contains("__MXGW_BATCH_EOR__", text); // The EOR marker must come after the JSON output. int jsonIndex = text.IndexOf("\"gatewayProtocolVersion\"", StringComparison.Ordinal); int eorIndex = text.IndexOf("__MXGW_BATCH_EOR__", StringComparison.Ordinal); Assert.True(jsonIndex >= 0 && eorIndex > jsonIndex); Assert.Equal(string.Empty, error.ToString()); } /// Verifies that batch mode routes per-command errors to stdout as JSON between EOR markers. [Fact] public async Task RunAsync_Batch_WritesErrorsToStdoutAsJson() { using var output = new StringWriter(); using var error = new StringWriter(); // Unknown command should produce an error on the captured error stream, // which batch mode re-emits to stdout inside the same delimited block. using var input = new StringReader("nope-not-a-command\nversion\n"); int exitCode = await MxGatewayClientCli.RunAsync( ["batch"], output, error, clientFactory: null, standardInput: input); Assert.Equal(0, exitCode); string text = output.ToString(); // Two records → two EOR markers. int firstEor = text.IndexOf("__MXGW_BATCH_EOR__", StringComparison.Ordinal); int secondEor = text.IndexOf( "__MXGW_BATCH_EOR__", firstEor + 1, StringComparison.Ordinal); Assert.True(firstEor > 0); Assert.True(secondEor > firstEor); // The unknown-command error message must be on stdout (not on stderr). Assert.Contains("nope-not-a-command", text); Assert.DoesNotContain("nope-not-a-command", error.ToString()); // The follow-up `version` line should still succeed. Assert.Contains("gateway-protocol=", text); } /// /// Client.Dotnet-018: the README CLI examples for the alarm subcommands at /// `clients/dotnet/README.md` must drive cleanly through the production /// CLI argument parser. The previous text used non-existent flags /// (`--session-id`, `--max-messages`, `--alarm-reference`) that would /// fail with "Unknown command" / "Missing required option --reference". /// Each documented example is extracted from the README, parsed via the /// production , and asserted /// against exit code 0. /// /// The alarm subcommand to validate (e.g. "stream-alarms", "acknowledge-alarm"). [Theory] [InlineData("stream-alarms")] [InlineData("acknowledge-alarm")] public async Task RunAsync_ReadmeExamples_ForAlarmCommands_ParseSuccessfully(string command) { string readme = LocateClientReadme(); string[] commandLine = ExtractReadmeCommandLine(readme, command); // The documented examples do not include --api-key (the README assumes // the env var path documented elsewhere). Inject an API key via the // standard env var so CreateOptions succeeds and the parser fully // exercises the documented flag shape. string? previousKey = Environment.GetEnvironmentVariable("MXGATEWAY_API_KEY"); Environment.SetEnvironmentVariable("MXGATEWAY_API_KEY", "test-api-key"); try { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); fakeClient.AlarmFeedMessages.Add(new AlarmFeedMessage { ActiveAlarm = new ActiveAlarmSnapshot { AlarmFullReference = "fixture" }, }); fakeClient.AcknowledgeAlarmReplies.Enqueue(new AcknowledgeAlarmReply { CorrelationId = "ack-fixture", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, }); int exitCode = await MxGatewayClientCli.RunAsync( commandLine, output, error, _ => fakeClient); Assert.True( exitCode == 0, $"README example for '{command}' exited {exitCode}; stderr=<<{error}>>"); Assert.DoesNotContain("Unknown command", error.ToString()); Assert.DoesNotContain("Missing required option", error.ToString()); } finally { Environment.SetEnvironmentVariable("MXGATEWAY_API_KEY", previousKey); } } /// /// Client.Dotnet-019: `BenchReadBulkAsync` previously fell back to /// reply.ReturnValue.Int32Value when the register reply had no /// typed Register payload, silently driving the rest of the bench /// against a zero server handle. The fix must fail loudly with a /// descriptive . /// [Fact] public async Task RunAsync_BenchReadBulk_WhenRegisterReplyMissingTypedPayload_FailsLoudly() { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); // Successful protocol + MX status but no typed `Register` payload. // Before the Client.Dotnet-019 fix this silently became serverHandle=0 // and the bench proceeded through SubscribeBulk / warmup / steady-state // against an invalid handle, producing a misleading zero-result summary. fakeClient.InvokeReplies.Enqueue(new MxCommandReply { SessionId = "session-fixture", Kind = MxCommandKind.Register, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, }); int exitCode = await MxGatewayClientCli.RunAsync( [ "bench-read-bulk", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--duration-seconds", "1", "--warmup-seconds", "0", "--bulk-size", "1", ], output, error, _ => fakeClient); Assert.Equal(1, exitCode); // Descriptive message that names the missing typed payload. string err = error.ToString(); Assert.Contains("Register", err); // The bench must not produce any aggregate stats JSON. Assert.DoesNotContain("bench-read-bulk", output.ToString()); } /// /// Client.Dotnet-020: the steady-state loop in `BenchReadBulkAsync` had a /// bare `catch { failedCalls++; continue; }` that swallowed /// , so token-driven cancellation /// kept spinning until --duration-seconds elapsed. After the fix /// the bench must exit promptly when the supplied token cancels. /// [Fact] public async Task RunAsync_BenchReadBulk_WhenSteadyStateLoopReceivesCancellation_ExitsPromptly() { using var output = new StringWriter(); using var error = new StringWriter(); int invokeCount = 0; FakeCliClient fakeClient = new() { InvokeHandler = (request, ct) => { int n = Interlocked.Increment(ref invokeCount); // Reply 1 = Register (success with typed payload). if (request.Command.Kind == MxCommandKind.Register) { return Task.FromResult(new MxCommandReply { SessionId = "session-fixture", Kind = MxCommandKind.Register, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Register = new RegisterReply { ServerHandle = 1 }, }); } // Reply 2 = SubscribeBulk (success). if (request.Command.Kind == MxCommandKind.SubscribeBulk) { var subscribeReply = new MxCommandReply { SessionId = "session-fixture", Kind = MxCommandKind.SubscribeBulk, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, SubscribeBulk = new BulkSubscribeReply(), }; return Task.FromResult(subscribeReply); } // ReadBulk reply 1 = success (so the steady-state loop enters // and starts iterating). Reply 2+ = simulated cancellation. if (request.Command.Kind == MxCommandKind.ReadBulk && n <= 3) { return Task.FromResult(new MxCommandReply { SessionId = "session-fixture", Kind = MxCommandKind.ReadBulk, ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, ReadBulk = new BulkReadReply(), }); } // From here on every ReadBulk throws OCE — the steady-state // loop must exit promptly rather than spinning until // --duration-seconds elapses. throw new OperationCanceledException(); }, }; var sw = System.Diagnostics.Stopwatch.StartNew(); await Assert.ThrowsAsync(async () => await MxGatewayClientCli.RunAsync( [ "bench-read-bulk", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--duration-seconds", "30", "--warmup-seconds", "0", "--bulk-size", "1", ], output, error, _ => fakeClient)); sw.Stop(); // Without the fix the loop swallows OCE and continues until the 30 s // steady-state deadline expires. With the fix it exits as soon as OCE // surfaces. Generous 10 s ceiling to keep the test stable under load. Assert.True( sw.Elapsed < TimeSpan.FromSeconds(10), $"Bench did not exit promptly on cancellation; took {sw.Elapsed}."); } /// /// Client.Dotnet-021: both `ReadBulkAsync` and `BenchReadBulkAsync` cast /// the user-supplied --timeout-ms to without /// bounds checking, so a negative value (e.g. -1) silently wraps /// to ~49.7 days. The fix must reject negatives with a clear error. /// /// The bulk-read subcommand to validate (e.g. "read-bulk", "bench-read-bulk"). [Theory] [InlineData("read-bulk")] [InlineData("bench-read-bulk")] public async Task RunAsync_TimeoutMs_NegativeValue_RejectsWithClearError(string command) { using var output = new StringWriter(); using var error = new StringWriter(); FakeCliClient fakeClient = new(); string[] args = command is "read-bulk" ? [ "read-bulk", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--session-id", "session-fixture", "--server-handle", "1", "--items", "Area001.Pump001.Speed", "--timeout-ms", "-1", ] : [ "bench-read-bulk", "--endpoint", "http://localhost:5000", "--api-key", "test-api-key", "--duration-seconds", "1", "--warmup-seconds", "0", "--bulk-size", "1", "--timeout-ms", "-1", ]; int exitCode = await MxGatewayClientCli.RunAsync( args, output, error, _ => fakeClient); Assert.NotEqual(0, exitCode); string err = error.ToString(); Assert.Contains("timeout-ms", err); Assert.Contains("non-negative", err); } /// /// Locates the .NET client README by walking up from the test assembly's /// base directory until clients/dotnet/README.md is found. Keeps /// the regression test independent of the current working directory. /// private static string LocateClientReadme() { string? directory = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(directory)) { string candidate = Path.Combine(directory, "clients", "dotnet", "README.md"); if (File.Exists(candidate)) { return candidate; } directory = Path.GetDirectoryName(directory); } throw new FileNotFoundException("clients/dotnet/README.md not found above test assembly base directory."); } /// /// Extracts the documented CLI invocation for the requested subcommand /// from the README, returning only the arguments after the /// mxgw-dotnet-equivalent prefix so they can be passed straight /// to . /// private static string[] ExtractReadmeCommandLine(string readmePath, string command) { string[] lines = File.ReadAllLines(readmePath); // Look for the documented `dotnet run ... -- ...` line. foreach (string line in lines) { int dashes = line.IndexOf("-- " + command, StringComparison.Ordinal); if (dashes < 0) { continue; } string after = line[(dashes + 3)..].Trim(); // Tokenize by whitespace, respecting "..." quoted segments. return TokenizeCommandLine(after); } throw new InvalidOperationException( $"README at '{readmePath}' has no documented example for subcommand '{command}'."); } /// /// Splits a single command-line string into argv tokens, honouring /// double-quoted segments so paths with embedded spaces survive intact. /// private static string[] TokenizeCommandLine(string input) { var tokens = new List(); var current = new System.Text.StringBuilder(); bool inQuotes = false; foreach (char ch in input) { if (ch == '"') { inQuotes = !inQuotes; continue; } if (!inQuotes && char.IsWhiteSpace(ch)) { if (current.Length > 0) { tokens.Add(current.ToString()); current.Clear(); } continue; } current.Append(ch); } if (current.Length > 0) { tokens.Add(current.ToString()); } return tokens.ToArray(); } /// 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; } /// Optional per-call handler that overrides queue-based behaviour. public Func>? InvokeHandler { 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; } if (InvokeHandler is not null) { return InvokeHandler(request, cancellationToken); } 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; } } /// Queue of acknowledge-alarm replies to return. public Queue AcknowledgeAlarmReplies { get; } = new(); /// List of received acknowledge-alarm requests. public List AcknowledgeAlarmRequests { get; } = []; /// List of received stream-alarms requests. public List StreamAlarmsRequests { get; } = []; /// List of alarm feed messages to yield when streaming alarms. public List AlarmFeedMessages { get; } = []; /// public Task AcknowledgeAlarmAsync( AcknowledgeAlarmRequest request, CancellationToken cancellationToken) { AcknowledgeAlarmRequests.Add(request); return Task.FromResult(AcknowledgeAlarmReplies.Dequeue()); } /// public async IAsyncEnumerable StreamAlarmsAsync( StreamAlarmsRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { StreamAlarmsRequests.Add(request); foreach (AlarmFeedMessage feedMessage in AlarmFeedMessages) { cancellationToken.ThrowIfCancellationRequested(); await Task.Yield(); yield return feedMessage; } } /// 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(); /// Queue of galaxy discover hierarchy replies to return. 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; } } } }