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 HistoryReadEventsTests { private static MxAccessGalaxyBackend BuildBackend(IHistorianDataSource? h, StaPump pump) => new( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), new MxAccessClient(pump, new MxProxyAdapter(), "events-test"), h); [Fact] public async Task Returns_disabled_error_when_no_historian_configured() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); using var backend = BuildBackend(null, pump); var resp = await backend.HistoryReadEventsAsync(new HistoryReadEventsRequest { SourceName = "TankA", StartUtcUnixMs = 0, EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), MaxEvents = 100, }, CancellationToken.None); resp.Success.ShouldBeFalse(); resp.Error.ShouldContain("Historian disabled"); } [Fact] public async Task Maps_HistorianEventDto_to_GalaxyHistoricalEvent_wire_shape() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var eventId = Guid.NewGuid(); var eventTime = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc); var receivedTime = eventTime.AddMilliseconds(150); var fake = new FakeHistorian(new HistorianEventDto { Id = eventId, Source = "TankA.Level.HiHi", EventTime = eventTime, ReceivedTime = receivedTime, DisplayText = "HiHi alarm tripped", Severity = 900, }); using var backend = BuildBackend(fake, pump); var resp = await backend.HistoryReadEventsAsync(new HistoryReadEventsRequest { SourceName = "TankA.Level.HiHi", StartUtcUnixMs = 0, EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), MaxEvents = 50, }, CancellationToken.None); resp.Success.ShouldBeTrue(); resp.Events.Length.ShouldBe(1); var got = resp.Events[0]; got.EventId.ShouldBe(eventId.ToString()); got.SourceName.ShouldBe("TankA.Level.HiHi"); got.DisplayText.ShouldBe("HiHi alarm tripped"); got.Severity.ShouldBe(900); got.EventTimeUtcUnixMs.ShouldBe(new DateTimeOffset(eventTime, TimeSpan.Zero).ToUnixTimeMilliseconds()); got.ReceivedTimeUtcUnixMs.ShouldBe(new DateTimeOffset(receivedTime, TimeSpan.Zero).ToUnixTimeMilliseconds()); fake.LastSourceName.ShouldBe("TankA.Level.HiHi"); fake.LastMaxEvents.ShouldBe(50); } [Fact] public async Task Null_source_name_is_passed_through_as_all_sources() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var fake = new FakeHistorian(); using var backend = BuildBackend(fake, pump); await backend.HistoryReadEventsAsync(new HistoryReadEventsRequest { SourceName = null, StartUtcUnixMs = 0, EndUtcUnixMs = 1, MaxEvents = 10, }, CancellationToken.None); fake.LastSourceName.ShouldBeNull(); } private sealed class FakeHistorian : IHistorianDataSource { private readonly HistorianEventDto[] _events; public string? LastSourceName { get; private set; } = ""; public int LastMaxEvents { get; private set; } public FakeHistorian(params HistorianEventDto[] events) => _events = events; public Task> ReadEventsAsync(string? src, DateTime s, DateTime e, int max, CancellationToken ct) { LastSourceName = src; LastMaxEvents = max; return Task.FromResult(new List(_events)); } 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 ms, string col, CancellationToken ct) => Task.FromResult(new List()); public Task> ReadAtTimeAsync(string tag, DateTime[] ts, CancellationToken ct) => Task.FromResult(new List()); public HistorianHealthSnapshot GetHealthSnapshot() => new(); public void Dispose() { } } }