using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Runtime; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian; /// /// Verifies the config-gated AddServerHistorian registration: when the /// ServerHistorian section is absent or disabled the /// default seeded by AddOtOpcUaRuntime survives (the factory is never invoked); when it is /// enabled the factory's returned wins /// (last-registration-wins over the TryAddSingleton Null default). /// public sealed class AddServerHistorianTests { /// A trivial read source the factory hands back when enabled; never actually invoked. private sealed class FakeHistorianDataSource : IHistorianDataSource { private static readonly HistoryReadResult EmptyRead = new(Array.Empty(), null); private static readonly HistoricalEventsResult EmptyEvents = new(Array.Empty(), null); public Task ReadRawAsync( string fullReference, DateTime startUtc, DateTime endUtc, uint maxValuesPerNode, CancellationToken cancellationToken) => Task.FromResult(EmptyRead); public Task ReadProcessedAsync( string fullReference, DateTime startUtc, DateTime endUtc, TimeSpan interval, HistoryAggregateType aggregate, CancellationToken cancellationToken) => Task.FromResult(EmptyRead); public Task ReadAtTimeAsync( string fullReference, IReadOnlyList timestampsUtc, CancellationToken cancellationToken) => Task.FromResult(EmptyRead); public Task ReadEventsAsync( string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken cancellationToken) => Task.FromResult(EmptyEvents); public HistorianHealthSnapshot GetHealthSnapshot() => new( TotalQueries: 0, TotalSuccesses: 0, TotalFailures: 0, ConsecutiveFailures: 0, LastSuccessTime: null, LastFailureTime: null, LastError: null, ProcessConnectionOpen: false, EventConnectionOpen: false, ActiveProcessNode: null, ActiveEventNode: null, Nodes: Array.Empty()); public void Dispose() { // Stateless fake; nothing to release. } } private static IConfiguration ConfigFrom(Dictionary values) => new ConfigurationBuilder().AddInMemoryCollection(values).Build(); [Fact] public void Section_absent_keeps_null_source_and_factory_not_invoked() { var factoryInvoked = false; var services = new ServiceCollection(); services.AddOtOpcUaRuntime(); var config = ConfigFrom(new Dictionary()); services.AddServerHistorian(config, (_, _) => { factoryInvoked = true; return new FakeHistorianDataSource(); }); using var provider = services.BuildServiceProvider(); provider.GetRequiredService().ShouldBeSameAs(NullHistorianDataSource.Instance); factoryInvoked.ShouldBeFalse(); } [Fact] public void Section_disabled_keeps_null_source_and_factory_not_invoked() { var factoryInvoked = false; var services = new ServiceCollection(); services.AddOtOpcUaRuntime(); var config = ConfigFrom(new Dictionary { ["ServerHistorian:Enabled"] = "false", }); services.AddServerHistorian(config, (_, _) => { factoryInvoked = true; return new FakeHistorianDataSource(); }); using var provider = services.BuildServiceProvider(); provider.GetRequiredService().ShouldBeSameAs(NullHistorianDataSource.Instance); factoryInvoked.ShouldBeFalse(); } [Fact] public void Section_enabled_registers_factory_source() { var services = new ServiceCollection(); services.AddOtOpcUaRuntime(); var config = ConfigFrom(new Dictionary { ["ServerHistorian:Enabled"] = "true", ["ServerHistorian:Endpoint"] = "https://historian.example.com:5222", ["ServerHistorian:ApiKey"] = "histgw_x_y", }); services.AddServerHistorian(config, (_, _) => new FakeHistorianDataSource()); using var provider = services.BuildServiceProvider(); var resolved = provider.GetRequiredService(); resolved.ShouldBeOfType(); resolved.ShouldNotBeSameAs(NullHistorianDataSource.Instance); } [Fact] public void Section_enabled_passes_bound_options_to_factory() { ServerHistorianOptions? seen = null; var services = new ServiceCollection(); services.AddOtOpcUaRuntime(); var config = ConfigFrom(new Dictionary { ["ServerHistorian:Enabled"] = "true", ["ServerHistorian:Endpoint"] = "https://historian.example.com:5222", ["ServerHistorian:ApiKey"] = "histgw_x_y", ["ServerHistorian:UseTls"] = "true", ["ServerHistorian:CaCertificatePath"] = "/etc/ssl/gateway-ca.pem", }); services.AddServerHistorian(config, (opts, _) => { seen = opts; return new FakeHistorianDataSource(); }); using var provider = services.BuildServiceProvider(); _ = provider.GetRequiredService(); seen.ShouldNotBeNull(); seen.Endpoint.ShouldBe("https://historian.example.com:5222"); seen.ApiKey.ShouldBe("histgw_x_y"); seen.UseTls.ShouldBeTrue(); seen.CaCertificatePath.ShouldBe("/etc/ssl/gateway-ca.pem"); } [Fact] public void Section_binds_endpoint_apikey_tls_fields() { var config = ConfigFrom(new Dictionary { ["ServerHistorian:Endpoint"] = "https://historian.example.com:5222", ["ServerHistorian:ApiKey"] = "histgw_x_y", ["ServerHistorian:UseTls"] = "true", ["ServerHistorian:CaCertificatePath"] = "/etc/ssl/gateway-ca.pem", }); var opts = config.GetSection(ServerHistorianOptions.SectionName).Get(); opts.ShouldNotBeNull(); opts.Endpoint.ShouldBe("https://historian.example.com:5222"); opts.ApiKey.ShouldBe("histgw_x_y"); opts.UseTls.ShouldBeTrue(); opts.CaCertificatePath.ShouldBe("/etc/ssl/gateway-ca.pem"); } }