using Microsoft.Extensions.Configuration;
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.
///
[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",
AllowUntrustedServerCertificate = true,
};
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);
}
[Theory] // #693: the ServerHistorian:GatewayAtTimeFromRaw kill switch reaches the data source (default on)
[InlineData(null, true)]
[InlineData(true, true)]
[InlineData(false, false)]
public void Factory_passes_the_AtTime_kill_switch(bool? configured, bool expected)
{
var opts = configured is { } c
? new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y", GatewayAtTimeFromRaw = c }
: new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" };
using var services = new ServiceCollection().BuildServiceProvider();
var dataSource = Assert.IsType(GatewayHistorian.CreateDataSource(opts, services));
dataSource.AtTimeFromRaw.ShouldBe(expected);
}
[Theory] // #694 R2: ServerHistorian:MaxTieClusterOverfetch bounds the crowding re-reads (default 65536)
[InlineData(null, 65536)]
[InlineData(1000, 1000)]
public void Factory_passes_the_tie_cluster_limit_to_the_crowding_re_reads(int? configured, int expected)
{
var opts = configured is { } c
? new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y", MaxTieClusterOverfetch = c }
: new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" };
using var services = new ServiceCollection().BuildServiceProvider();
var dataSource = Assert.IsType(GatewayHistorian.CreateDataSource(opts, services));
dataSource.MaxPreWindowRows.ShouldBe(expected);
}
[Theory] // ServerHistorian:Gateway:EventDrainBudget binds from configuration and reaches the data source
[InlineData(null, GatewayHistorianSettings.DefaultEventDrainBudget)]
[InlineData("7", 7)]
[InlineData("0", 0)]
public void Factory_passes_the_bound_event_drain_budget(string? configured, int expected)
{
var values = new Dictionary
{
["ServerHistorian:Enabled"] = "true",
["ServerHistorian:Endpoint"] = "https://localhost:5222",
["ServerHistorian:ApiKey"] = "histgw_x_y",
};
if (configured is not null) values["ServerHistorian:Gateway:EventDrainBudget"] = configured;
var opts = new Microsoft.Extensions.Configuration.ConfigurationBuilder().AddInMemoryCollection(values).Build()
.GetSection(ServerHistorianOptions.SectionName).Get()!;
using var services = new ServiceCollection().BuildServiceProvider();
var dataSource = Assert.IsType(GatewayHistorian.CreateDataSource(opts, services));
dataSource.EventDrainBudget.ShouldBe(expected);
}
}