using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests; /// /// Read-cutover seam tests (T10). Both assert offline construction only — the package client builds /// its GrpcChannel lazily, so neither the adapter ctor nor the factory dials the gateway. A /// bogus/unreachable endpoint must therefore construct without throwing or performing network I/O. /// public sealed class HistorianGatewayClientAdapterTests { [Fact] public void Adapter_constructs_from_options_without_dialing() { // Constructing the channel must not perform network I/O (lazy connect). var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" }; using var adapter = HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance); Assert.NotNull(adapter); } /// /// A plaintext h2c gateway (an http:// endpoint with UseTls=false) must /// construct, using nothing but documented defaults. /// /// /// /// Found by the LocalDb Phase 2 live gate, on the third consecutive crash-loop of the rig. /// docs/Historian.md and CLAUDE.md both document http:// as a supported /// transport ("scheme selects transport: https = TLS, http = h2c"), but it was in fact /// unreachable: this factory forwarded the TLS-only options unconditionally, and /// defaults to /// , so it always sent RequireCertificateValidation=true — /// which the client rejects outright when UseTls=false /// ("RequireCertificateValidation is a TLS-only option and requires UseTls=true"). /// /// /// So every h2c deployment crashed at startup, and the only way to avoid it was to /// set AllowUntrustedServerCertificate=true — asserting a certificate posture for a /// connection that has no certificate at all. /// /// [Fact] public void Adapter_constructs_for_plaintext_h2c_endpoint() { var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "http://localhost:5222", ApiKey = "histgw_x_y", UseTls = false, }; using var adapter = HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance); adapter.ShouldNotBeNull(); } /// /// A pinned CA path is likewise TLS-only, and must not leak into an h2c client — the same /// rejection, reached by a different field. /// [Fact] public void Adapter_ignores_tls_only_settings_on_a_plaintext_endpoint() { var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "http://localhost:5222", ApiKey = "histgw_x_y", UseTls = false, CaCertificatePath = "/etc/ssl/certs/leftover-from-a-tls-config.pem", }; using var adapter = HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance); adapter.ShouldNotBeNull(); } /// /// archreview 06/S-11 — an enabled historian with an empty Endpoint must fail with a /// named, config-key-carrying (defense-in-depth for any /// caller that bypasses the Host's ServerHistorianOptionsValidator), not the raw /// that new Uri("") throws deep in the factory. Current /// code throws → this is the failing-first (RED) repro. /// [Fact] public void Create_with_empty_endpoint_throws_named_config_error() { var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "", ApiKey = "histgw_x_y" }; var ex = Should.Throw( () => HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance)); ex.Message.ShouldContain("ServerHistorian:Endpoint"); } /// /// archreview 06/S-11 companion — a non-empty but unparseable / non-absolute Endpoint must /// also fail with the same named , not a raw /// . RED on current code. /// [Fact] public void Create_with_malformed_endpoint_throws_named_config_error() { var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "not a uri", ApiKey = "histgw_x_y" }; var ex = Should.Throw( () => HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance)); ex.Message.ShouldContain("ServerHistorian:Endpoint"); } [Fact] public void Factory_builds_GatewayHistorianDataSource() { var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" }; using var services = new ServiceCollection().BuildServiceProvider(); var dataSource = GatewayHistorian.CreateDataSource(opts, services); Assert.IsType(dataSource); } }