using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests { [Trait("Category", "Unit")] public sealed class HistorianWiringTests { /// /// When the Proxy sends a HistoryRead but the supervisor never enabled the historian /// (OTOPCUA_HISTORIAN_ENABLED unset), we expect a clean Success=false with a /// self-explanatory error — not an exception or a hang against localhost. /// [Fact] public async Task HistoryReadAsync_returns_disabled_error_when_no_historian_configured() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var mx = new MxAccessClient(pump, new MxProxyAdapter(), "HistorianWiringTests"); using var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, historian: null); var resp = await backend.HistoryReadAsync(new HistoryReadRequest { TagReferences = new[] { "TestTag" }, StartUtcUnixMs = 0, EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), MaxValuesPerTag = 100, }, CancellationToken.None); resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("Historian disabled"); resp.Tags.ShouldBeEmpty(); } /// /// When the historian is wired up, we expect the backend to call through and map /// samples onto the IPC wire shape. Uses a fake /// that returns a single known-good sample so we can assert the mapping stays sane. /// [Fact] public async Task HistoryReadAsync_maps_sample_to_GalaxyDataValue() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var mx = new MxAccessClient(pump, new MxProxyAdapter(), "HistorianWiringTests"); var fake = new FakeHistorianDataSource(new HistorianSample { Value = 42.5, Quality = 192, // Good TimestampUtc = new DateTime(2026, 4, 18, 9, 0, 0, DateTimeKind.Utc), }); using var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, fake); var resp = await backend.HistoryReadAsync(new HistoryReadRequest { TagReferences = new[] { "TankLevel" }, StartUtcUnixMs = 0, EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), MaxValuesPerTag = 100, }, CancellationToken.None); resp.Success.ShouldBeTrue(); resp.Tags.Length.ShouldBe(1); resp.Tags[0].TagReference.ShouldBe("TankLevel"); resp.Tags[0].Values.Length.ShouldBe(1); resp.Tags[0].Values[0].StatusCode.ShouldBe(0u); // Good resp.Tags[0].Values[0].ValueBytes.ShouldNotBeNull(); resp.Tags[0].Values[0].SourceTimestampUtcUnixMs.ShouldBe( new DateTimeOffset(2026, 4, 18, 9, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds()); } private sealed class FakeHistorianDataSource : IHistorianDataSource { private readonly HistorianSample _sample; public FakeHistorianDataSource(HistorianSample sample) => _sample = sample; public Task> ReadRawAsync(string tagName, DateTime s, DateTime e, int max, CancellationToken ct) => Task.FromResult(new List { _sample }); public Task> ReadAggregateAsync(string tagName, DateTime s, DateTime e, double ms, string col, CancellationToken ct) => Task.FromResult(new List()); public Task> ReadAtTimeAsync(string tagName, DateTime[] ts, CancellationToken ct) => Task.FromResult(new List()); public Task> ReadEventsAsync(string? src, DateTime s, DateTime e, int max, CancellationToken ct) => Task.FromResult(new List()); public HistorianHealthSnapshot GetHealthSnapshot() => new(); public void Dispose() { } } } }