using Microsoft.AspNetCore.Server.Kestrel.Core; using ZB.MOM.WW.ScadaBridge.Host; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// The site node's Kestrel configuration, asserted against the code that actually ships (the other /// Host startup tests re-declare their own ConfigureKestrel lambda, which can never catch a /// regression in Program.cs's). /// public class SiteKestrelSetupTests { [Fact] public void SiteListener_EnablesHttp2Keepalive() { // Arch-review 2026-08-19 P4-3. Kestrel's default KeepAlivePingDelay is TimeSpan.MaxValue — // no server-initiated pings — so a half-open HTTP/2 connection is never detected on a // stream that is legitimately silent. The LocalDb sync stream is exactly that: with the // dialling node crashed or partitioned, this node's handler stays parked on a dead stream // holding the single-stream guard, refusing the reconnect that would heal the pair. var options = new KestrelServerOptions(); SiteKestrelSetup.Configure(options, grpcPort: 8083, metricsPort: 8084); Assert.NotEqual(TimeSpan.MaxValue, options.Limits.Http2.KeepAlivePingDelay); Assert.Equal(SiteKestrelSetup.Http2KeepAlivePingDelay, options.Limits.Http2.KeepAlivePingDelay); Assert.Equal(SiteKestrelSetup.Http2KeepAlivePingTimeout, options.Limits.Http2.KeepAlivePingTimeout); // Detection has to be quick enough to matter, and a ping must not be its own timeout. Assert.True(SiteKestrelSetup.Http2KeepAlivePingTimeout < SiteKestrelSetup.Http2KeepAlivePingDelay); Assert.True( SiteKestrelSetup.Http2KeepAlivePingDelay + SiteKestrelSetup.Http2KeepAlivePingTimeout <= TimeSpan.FromMinutes(2)); } [Fact] public void SiteListener_KeepsGrpcHttp2Only_AndMetricsHttp1Capable() { // The keepalive change must not disturb the protocol split: h2c-only on the gRPC port so // Grpc.AspNetCore can serve it without TLS ALPN, HTTP/1.1-capable on the metrics port so a // standard Prometheus scraper can reach /metrics. var options = new KestrelServerOptions(); SiteKestrelSetup.Configure(options, grpcPort: 8083, metricsPort: 8084); var endpoints = options.GetType() .GetProperty("CodeBackedListenOptions", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public)! .GetValue(options) as System.Collections.IEnumerable; Assert.NotNull(endpoints); var protocols = endpoints!.Cast().Select(o => o.Protocols).ToList(); Assert.Equal(2, protocols.Count); Assert.Equal(HttpProtocols.Http2, protocols[0]); Assert.Equal(HttpProtocols.Http1AndHttp2, protocols[1]); } }