Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/HttpsEndpointChannelTests.cs
T
Joseph Doherty 9138f3edf8 fix(grpc): control-plane PSK credentials work on https endpoints (ScadaBridge#64)
WithSiteCredentials always composed the per-site PSK over
ChannelCredentials.Insecure. Grpc.Net.Client refuses that on an https
address ("insecure channel credentials and can't use a HttpClient with a
'https' scheme"), so every https control-plane endpoint threw at channel
construction - in SitePairChannelProvider the throw was caught and the
site address logged as malformed, i.e. a TLS site was silently
undialable. The old comment claiming TLS needed no change here was wrong.

WithSiteCredentials now takes the endpoint: https composes over
ChannelCredentials.SecureSsl (PSK retained inside TLS, no insecure
opt-in); anything else, including callers that omit it, keeps the
previous composition unchanged. SitePairChannelProvider,
SiteStreamGrpcClient and CentralChannelProvider pass their endpoint. The
three AuditLog pull clients are deliberately left for after #42.

Tests: real Kestrel HTTP/2-over-TLS (ALPN) with the real
ControlPlaneAuthInterceptor - correct PSK accepted, wrong PSK still
PermissionDenied, untrusted server cert fails with an
AuthenticationException cause, the pre-fix composition throws on https,
http composition unchanged; plus each production builder accepts https
(mutation check: disabling the TLS branch turns those red).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 17:06:01 -05:00

89 lines
3.5 KiB
C#

using Grpc.Net.Client;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc;
/// <summary>
/// Every production builder of a PSK-credentialed control-plane channel must accept an
/// <c>https://</c> endpoint (ScadaBridge#64). Before the endpoint was threaded into
/// <see cref="ControlPlaneCredentials.WithSiteCredentials"/>, each composed its credentials over
/// insecure channel credentials and Grpc.Net.Client threw at construction for any https address —
/// in <see cref="SitePairChannelProvider"/> that throw was caught and the site address logged as
/// malformed, so a TLS site was silently undialable.
/// </summary>
public class HttpsEndpointChannelTests
{
private const string Site = "site-a";
/// <summary>Verifies central's per-site pair provider builds a channel for an https site address.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SitePairChannelProvider_builds_an_https_site_channel()
{
using var provider = new SitePairChannelProvider(
new FixedKeyProvider(),
Options.Create(new CommunicationOptions()),
NullLogger<SitePairChannelProvider>.Instance,
handlerFactory: null,
reachabilityProbe: (_, _) => Task.FromResult(false));
provider.UpdateSite(Site, "https://site-a-node-a:8083", nodeBEndpoint: null);
var target = await provider.ExecuteAsync(
Site, (channel, _) => Task.FromResult(channel.Target), CancellationToken.None);
Assert.Equal("site-a-node-a:8083", target);
}
/// <summary>Verifies a site's central channel provider builds https channels for both centrals.</summary>
[Fact]
public void CentralChannelProvider_builds_https_central_channels()
{
using var provider = new CentralChannelProvider(
new[] { "https://central-a:8083", "https://central-b:8083" },
new FixedKeyProvider(),
Site,
new CommunicationOptions(),
NullLogger.Instance);
}
/// <summary>Verifies the site stream client builds an https channel with credentials.</summary>
[Fact]
public void SiteStreamGrpcClient_builds_an_https_channel()
{
using var client = new SiteStreamGrpcClient(
"https://site-a-node-a:8083",
NullLogger<SiteStreamGrpcClient>.Instance,
new CommunicationOptions(),
new FixedKeyProvider(),
Site);
Assert.Equal("https://site-a-node-a:8083", client.Endpoint);
}
/// <summary>Verifies the scheme check behind the composition choice.</summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="expected">Whether it is TLS.</param>
[Theory]
[InlineData("https://host:8083", true)]
[InlineData("HTTPS://host:8083", true)]
[InlineData("http://host:8083", false)]
[InlineData(null, false)]
[InlineData("", false)]
[InlineData("not a url", false)]
public void IsTlsEndpoint_matches_only_absolute_https(string? endpoint, bool expected)
{
Assert.Equal(expected, ControlPlaneCredentials.IsTlsEndpoint(endpoint));
}
private sealed class FixedKeyProvider : ISitePskProvider
{
/// <inheritdoc />
public ValueTask<string> GetAsync(string siteId, CancellationToken ct) => new("k");
/// <inheritdoc />
public void Invalidate(string siteId) { }
}
}