using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Client.Tests; /// /// Drives the public client API against — a real /// gRPC server on loopback — so the transport, protobuf serialization, call metadata, /// and gRPC status mapping are all exercised. Every other test in this project /// substitutes and therefore proves nothing about /// what actually crosses the wire. /// public sealed class MxGatewayClientWireTests { private const string ApiKey = "mxgw_wiretest_secret"; /// /// Verifies the full session happy path decodes real wire bytes end to end. /// /// A task that represents the asynchronous operation. [Fact] public async Task SessionRoundTrip_OverRealTransport_DecodesEveryReplyField() { await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync(); await using MxGatewayClient client = server.CreateClient(ApiKey); MxGatewaySession session = await client.OpenSessionAsync( new OpenSessionRequest { ClientSessionName = "wire-test" }); Assert.Equal(WireFakeGatewayServer.FakeGatewayService.SessionId, session.SessionId); Assert.Equal("fake-backend", session.OpenSessionReply.BackendName); Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId); Assert.Equal(3u, session.OpenSessionReply.GatewayProtocolVersion); Assert.Equal(["events", "invoke"], session.OpenSessionReply.Capabilities); int serverHandle = await session.RegisterAsync("wire-test-client"); Assert.Equal(WireFakeGatewayServer.FakeGatewayService.ServerHandle, serverHandle); MxCommandRequest? invoke = server.Service.InvokeRequest; Assert.NotNull(invoke); Assert.Equal(MxCommandKind.Register, invoke.Command.Kind); Assert.Equal("wire-test-client", invoke.Command.Register.ClientName); List events = await CollectAsync(client.StreamEventsAsync( new StreamEventsRequest { SessionId = session.SessionId })); MxEvent single = Assert.Single(events); Assert.Equal(MxEventFamily.OnDataChange, single.Family); Assert.Equal(WireFakeGatewayServer.FakeGatewayService.ServerHandle, single.ServerHandle); Assert.Equal(WireFakeGatewayServer.FakeGatewayService.ItemHandle, single.ItemHandle); Assert.Equal(17, single.Value.Int32Value); Assert.Equal(192, single.Quality); Assert.Equal(9ul, single.WorkerSequence); Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, single.BodyCase); CloseSessionReply closeReply = await session.CloseAsync(); Assert.Equal(SessionState.Closed, closeReply.FinalState); Assert.Equal( WireFakeGatewayServer.FakeGatewayService.SessionId, server.Service.CloseSessionRequest?.SessionId); } /// /// Verifies the API key reaches the server as a bearer header on unary and /// streaming calls alike. A transport fake can only assert what the client passes; /// this asserts what the server receives. /// /// A task that represents the asynchronous operation. [Fact] public async Task ApiKey_ReachesTheServerAsBearerMetadata_OnEveryRpc() { await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync(); await using MxGatewayClient client = server.CreateClient(ApiKey); MxGatewaySession session = await client.OpenSessionAsync( new OpenSessionRequest { ClientSessionName = "wire-test" }); await session.RegisterAsync("wire-test-client"); await CollectAsync(client.StreamEventsAsync( new StreamEventsRequest { SessionId = session.SessionId })); await session.CloseAsync(); string expected = $"Bearer {ApiKey}"; Assert.Equal( new Dictionary { ["OpenSession"] = expected, ["Invoke"] = expected, ["StreamEvents"] = expected, ["CloseSession"] = expected, }, server.Service.AuthorizationByMethod); } /// /// Verifies the gateway's replay-gap sentinel survives serialization and is /// surfaced as a typed, non-terminal stream item. /// /// A task that represents the asynchronous operation. [Fact] public async Task ReplayGapSentinel_SurvivesTheWire_AsTypedStreamItem() { await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync(service => service.ReplayGap = new ReplayGap { RequestedAfterSequence = 3, OldestAvailableSequence = 8, }); await using MxGatewayClient client = server.CreateClient(ApiKey); MxGatewaySession session = await client.OpenSessionAsync( new OpenSessionRequest { ClientSessionName = "wire-test" }); List items = []; IAsyncEnumerable stream = client.StreamEventsAsync(new StreamEventsRequest { SessionId = session.SessionId, AfterWorkerSequence = 3, }); await foreach (MxEventStreamItem item in stream.AsStreamItemsAsync()) { items.Add(item); } Assert.Equal(2, items.Count); Assert.True(items[0].IsReplayGap); Assert.Equal(3ul, items[0].ReplayGap!.RequestedAfterSequence); Assert.Equal(8ul, items[0].ReplayGap!.OldestAvailableSequence); Assert.False(items[1].IsReplayGap); Assert.Equal(MxEventFamily.OnDataChange, items[1].Event.Family); Assert.Equal(3ul, server.Service.StreamEventsRequest?.AfterWorkerSequence); } /// /// Verifies a genuine PERMISSION_DENIED status maps to the typed client /// exception rather than a bare RpcException. /// /// A task that represents the asynchronous operation. [Fact] public async Task PermissionDeniedStatus_MapsToAuthorizationException() { await using WireFakeGatewayServer server = await WireFakeGatewayServer.StartAsync( service => service.DenyInvoke = true); await using MxGatewayClient client = server.CreateClient(ApiKey); MxGatewaySession session = await client.OpenSessionAsync( new OpenSessionRequest { ClientSessionName = "wire-test" }); await Assert.ThrowsAsync( () => session.RegisterAsync("wire-test-client")); } private static async Task> CollectAsync(IAsyncEnumerable stream) { List events = []; await foreach (MxEvent gatewayEvent in stream) { events.Add(gatewayEvent); } return events; } }