using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MessagePack; 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 HistoryReadProcessedTests { [Fact] public async Task ReturnsDisabledError_When_NoHistorianConfigured() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var mx = new MxAccessClient(pump, new MxProxyAdapter(), "processed-test"); using var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, historian: null); var resp = await backend.HistoryReadProcessedAsync(new HistoryReadProcessedRequest { TagReference = "T", StartUtcUnixMs = 0, EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), IntervalMs = 1000, AggregateColumn = "Average", }, CancellationToken.None); resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("Historian disabled"); } [Fact] public async Task Rejects_NonPositiveInterval() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var mx = new MxAccessClient(pump, new MxProxyAdapter(), "processed-test"); var fake = new FakeHistorianDataSource(); using var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, fake); var resp = await backend.HistoryReadProcessedAsync(new HistoryReadProcessedRequest { TagReference = "T", IntervalMs = 0, AggregateColumn = "Average", }, CancellationToken.None); resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("IntervalMs"); } [Fact] public async Task Maps_AggregateSample_With_Value_To_Good() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var mx = new MxAccessClient(pump, new MxProxyAdapter(), "processed-test"); var fake = new FakeHistorianDataSource(new HistorianAggregateSample { Value = 12.34, TimestampUtc = new DateTime(2026, 4, 18, 10, 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.HistoryReadProcessedAsync(new HistoryReadProcessedRequest { TagReference = "T", StartUtcUnixMs = 0, EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), IntervalMs = 60_000, AggregateColumn = "Average", }, CancellationToken.None); resp.Success.ShouldBeTrue(); resp.Values.Length.ShouldBe(1); resp.Values[0].StatusCode.ShouldBe(0u); // Good resp.Values[0].ValueBytes.ShouldNotBeNull(); MessagePackSerializer.Deserialize(resp.Values[0].ValueBytes!).ShouldBe(12.34); fake.LastAggregateColumn.ShouldBe("Average"); fake.LastIntervalMs.ShouldBe(60_000d); } [Fact] public async Task Maps_Null_Bucket_To_BadNoData() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var mx = new MxAccessClient(pump, new MxProxyAdapter(), "processed-test"); var fake = new FakeHistorianDataSource(new HistorianAggregateSample { Value = null, TimestampUtc = DateTime.UtcNow, }); using var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, fake); var resp = await backend.HistoryReadProcessedAsync(new HistoryReadProcessedRequest { TagReference = "T", IntervalMs = 1000, AggregateColumn = "Minimum", }, CancellationToken.None); resp.Success.ShouldBeTrue(); resp.Values.Length.ShouldBe(1); resp.Values[0].StatusCode.ShouldBe(0x800E0000u); // BadNoData resp.Values[0].ValueBytes.ShouldBeNull(); } private sealed class FakeHistorianDataSource : IHistorianDataSource { private readonly HistorianAggregateSample[] _samples; public string? LastAggregateColumn { get; private set; } public double LastIntervalMs { get; private set; } public FakeHistorianDataSource(params HistorianAggregateSample[] samples) => _samples = samples; public Task> ReadRawAsync(string tag, DateTime s, DateTime e, int max, CancellationToken ct) => Task.FromResult(new List()); public Task> ReadAggregateAsync( string tag, DateTime s, DateTime e, double intervalMs, string col, CancellationToken ct) { LastAggregateColumn = col; LastIntervalMs = intervalMs; return Task.FromResult(new List(_samples)); } public Task> ReadAtTimeAsync(string tag, 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() { } } }