fix(grpc): central Kestrel gRPC listener was dropping the :5000 HTTP surface (T1A.2 regression)
Caught on the Phase 1A rig proof: central-a logged only "Now listening on: http://[::]:8083" and nothing else. Central UI, the Management + Inbound API, and the /health/* endpoints Traefik + IActiveNodeGate depend on were all gone, with no startup error — the node booted, joined the cluster and served gRPC fine. Cause: calling options.ListenAnyIP in ConfigureKestrel puts Kestrel into explicit-endpoints mode, which SUPPRESSES the URLs from ASPNETCORE_URLS/--urls entirely — it is not additive, contrary to the comment T1A.2 shipped. Central's whole HTTP/1 surface lives on that URL (http://+:5000 on the rig, a different port in prod), so binding only the gRPC port silently deleted it. The site branch has the same shape but no ASPNETCORE_URLS surface to lose — it binds every port it needs explicitly. Fix: parse the port(s) from the configured URLs and re-declare them (Http1AndHttp2) alongside the gRPC port (Http2) in the one ConfigureKestrel call. New Program.ParseHttpBindPorts + CentralHttpBindPortsTests (14 cases: wildcard/ipv6/ hostname hosts, multi-URL, de-dupe, scheme-default, null/blank, unparseable-skipped). Why no test caught it originally: unit/E2E tests use TestServer, which never binds real Kestrel. Only a live node exposes the missing listener — which is exactly what the rig proof is for.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins <see cref="Program.ParseHttpBindPorts"/>, which re-declares central's HTTP surface
|
||||
/// after the gRPC listener switches Kestrel into explicit-endpoints mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This exists because the regression it guards is invisible to every other test: a central
|
||||
/// node that binds only the gRPC port boots clean, joins the cluster and serves gRPC, while
|
||||
/// the Central UI, Management/Inbound API and <c>/health/*</c> are silently dead. TestServer
|
||||
/// never binds real Kestrel, so the parser is the one seam that can be asserted in-process.
|
||||
/// The rig caught the original defect; this keeps it caught.
|
||||
/// </remarks>
|
||||
public class CentralHttpBindPortsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("http://+:5000", new[] { 5000 })]
|
||||
[InlineData("http://0.0.0.0:5000", new[] { 5000 })]
|
||||
[InlineData("http://[::]:5000", new[] { 5000 })]
|
||||
[InlineData("http://localhost:5000", new[] { 5000 })]
|
||||
[InlineData("http://*:8085", new[] { 8085 })]
|
||||
[InlineData("http://+:5000;http://+:5001", new[] { 5000, 5001 })]
|
||||
[InlineData("http://+:5000 ; http://+:5001", new[] { 5000, 5001 })]
|
||||
[InlineData("http://+:5000;http://+:5000", new[] { 5000 })] // de-duped
|
||||
public void ParsesPortsFromServerUrls(string urls, int[] expected)
|
||||
{
|
||||
Assert.Equal(expected, Program.ParseHttpBindPorts(urls));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://+:443", 443)] // scheme default when no explicit port
|
||||
[InlineData("http://+:80", 80)]
|
||||
public void HandlesSchemeDefaultAndExplicitPort(string url, int expected)
|
||||
{
|
||||
Assert.Equal(new[] { expected }, Program.ParseHttpBindPorts(url));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void EmptyOrNull_YieldsNoPorts(string? urls)
|
||||
{
|
||||
Assert.Empty(Program.ParseHttpBindPorts(urls));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnparseableEntry_IsSkipped_NotThrown()
|
||||
{
|
||||
// A malformed URL must not take the node down at boot; the good one still binds.
|
||||
var ports = Program.ParseHttpBindPorts("not-a-url;http://+:5000");
|
||||
Assert.Equal(new[] { 5000 }, ports);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user