From 8471db4b1dbd7c4e67d206f7c1d519a96d05c2ac Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 20 Aug 2026 10:14:55 -0400 Subject: [PATCH] test(host): no-PSK mesh calls report the interceptor's verdict, not a cookie 401/redirect Extends MeshGrpcAuthStackTests (T5): a no-bearer or wrong-bearer call to DeploymentArtifactService.Fetch, TelemetryStreamService.Subscribe and NodeCommandService.Execute fails with the INTERCEPTOR's detail string ("Config-serve authentication failed.", "Telemetry stream authentication failed.", "Node command authentication failed."), asserted alongside the status code rather than in place of it - the vacuity trap the plan calls out (ConfigServeAuthInterceptor throws Unauthenticated, and Grpc.Net.Client can map an HTTP 401 to the same status, so status alone would not discriminate a broken build). Verified red-then-green against the T1 (pre-AllowAnonymous) mapper: all 6 new theory cases fail there with StatusCode.Internal ("HTTP/2 server reset the stream, PROTOCOL_ERROR") instead of Unauthenticated/PermissionDenied - the cookie challenge breaking the h2c stream before any gRPC trailers are written, never a clean auth denial. That is the exact signature the live gate (T10) should grep for; recorded in the class remarks. --- .../MeshGrpcAuthStackTests.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MeshGrpcAuthStackTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MeshGrpcAuthStackTests.cs index d55a27d3..33c27bfa 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MeshGrpcAuthStackTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MeshGrpcAuthStackTests.cs @@ -19,6 +19,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh; using ZB.MOM.WW.OtOpcUa.Commons.Protos.DeploymentArtifact.V1; using ZB.MOM.WW.OtOpcUa.Commons.Protos.MeshTransport.V1; +using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; @@ -255,6 +256,88 @@ public sealed class MeshGrpcAuthStackTests : IAsyncLifetime () => call.ResponseStream.MoveNext(TestContext.Current.CancellationToken)); } + // --------------------------------------------------------------------------------------- + // T5 - status fidelity: a no-PSK / wrong-PSK call reports the INTERCEPTOR's verdict, never a + // cookie challenge. This is the vacuity trap the plan calls out: ConfigServeAuthInterceptor + // throws StatusCode.Unauthenticated, and Grpc.Net.Client also maps an HTTP 401 to + // Unauthenticated - so "the call failed with Unauthenticated" is true on a broken build AND a + // fixed one. The interceptor's DETAIL string is the only thing that discriminates them. + // + // Observed cookie-challenge signature (captured red-then-green against this same fixture, see + // MeshGrpcAuthStackTests' positive tests): the challenge does not surface as a clean + // Unauthenticated/PermissionDenied at all. Grpc.Net.Client reports + // RpcException(StatusCode.Internal, "Error starting gRPC call. HttpRequestException: The HTTP/2 + // server reset the stream. HTTP/2 error code 'PROTOCOL_ERROR' (0x1)..."), because the cookie + // handler's redirect/401 breaks the prior-knowledge h2c stream before any gRPC trailers are + // written. So a status-only assertion here would ALSO be vacuous in the other direction: a + // regressed build fails with StatusCode.Internal, never Unauthenticated/PermissionDenied, and a + // caller checking only "did it throw" cannot tell a blocked call from a refused one. Recorded + // here per the plan (T5 step 2) - this is the exact string the live gate (T10) greps for. + // --------------------------------------------------------------------------------------- + + [Theory] + [InlineData(null)] + [InlineData("Bearer the-wrong-key")] + public async Task NoOrWrongPsk_ArtifactFetch_FailsWithTheInterceptorsDetail(string? authHeader) + { + var client = new DeploymentArtifactService.DeploymentArtifactServiceClient(Channel()); + var headers = new Metadata(); + if (authHeader is not null) + headers.Add("authorization", authHeader); + + using var call = client.Fetch( + new FetchRequest { DeploymentId = Guid.NewGuid().ToString() }, + headers, + cancellationToken: TestContext.Current.CancellationToken); + + var ex = await Should.ThrowAsync( + () => call.ResponseStream.MoveNext(TestContext.Current.CancellationToken)); + + ex.StatusCode.ShouldBe(StatusCode.Unauthenticated); + ex.Status.Detail.ShouldBe("Config-serve authentication failed."); + } + + [Theory] + [InlineData(null)] + [InlineData("Bearer the-wrong-key")] + public async Task NoOrWrongPsk_TelemetrySubscribe_FailsWithTheInterceptorsDetail(string? authHeader) + { + var client = new TelemetryStreamService.TelemetryStreamServiceClient(Channel()); + var headers = new Metadata(); + if (authHeader is not null) + headers.Add("authorization", authHeader); + + using var call = client.Subscribe( + new TelemetryStreamRequest { CorrelationId = "r3-01-t5" }, + headers, + cancellationToken: TestContext.Current.CancellationToken); + + var ex = await Should.ThrowAsync( + () => call.ResponseStream.MoveNext(TestContext.Current.CancellationToken)); + + ex.StatusCode.ShouldBe(StatusCode.PermissionDenied); + ex.Status.Detail.ShouldBe("Telemetry stream authentication failed."); + } + + [Theory] + [InlineData(null)] + [InlineData("Bearer the-wrong-key")] + public async Task NoOrWrongPsk_NodeCommandExecute_FailsWithTheInterceptorsDetail(string? authHeader) + { + var command = new RestartDriver("C1", "driver-1", "alice", Guid.NewGuid()); + var client = new NodeCommandService.NodeCommandServiceClient(Channel()); + var headers = new Metadata(); + if (authHeader is not null) + headers.Add("authorization", authHeader); + + var ex = await Should.ThrowAsync(() => client.ExecuteAsync( + MeshTransportDtoMapper.ToEnvelope(command), headers, + cancellationToken: TestContext.Current.CancellationToken).ResponseAsync); + + ex.StatusCode.ShouldBe(StatusCode.PermissionDenied); + ex.Status.Detail.ShouldBe("Node command authentication failed."); + } + // --------------------------------------------------------------------------------------- // harness // ---------------------------------------------------------------------------------------