using NSubstitute; using ZB.MOM.WW.ScadaBridge.AuditLog.Kpi; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Kpi; /// /// M6 "KPI History & Trends" (K8) coverage for — /// the the /// central recorder enumerates to snapshot Audit Log KPIs. Asserts the source maps the /// repository's onto exactly three Global-scope samples /// with the canonical metric names, the recorder-supplied capture instant, and a null /// ScopeKey. /// public class AuditLogKpiSampleSourceTests { private static readonly DateTime CapturedAt = new(2026, 6, 17, 12, 30, 0, DateTimeKind.Utc); [Fact] public void Source_is_AuditLog() { var repo = Substitute.For(); var sut = new AuditLogKpiSampleSource(repo); Assert.Equal(KpiSources.AuditLog, sut.Source); } [Fact] public async Task CollectAsync_emits_three_global_samples_mapped_from_snapshot() { // Distinct values so a swapped mapping (e.g. error ↔ backlog) is caught. var snapshot = new AuditLogKpiSnapshot( TotalEventsLastHour: 4200, ErrorEventsLastHour: 17, BacklogTotal: 305, AsOfUtc: new DateTime(2026, 6, 17, 12, 30, 5, DateTimeKind.Utc)); var repo = Substitute.For(); repo.GetKpiSnapshotAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(snapshot); var sut = new AuditLogKpiSampleSource(repo); var samples = await sut.CollectAsync(CapturedAt); Assert.Equal(3, samples.Count); // Every sample: AuditLog source, Global scope, null ScopeKey, the // recorder-stamped capture instant. Assert.All(samples, s => { Assert.Equal(KpiSources.AuditLog, s.Source); Assert.Equal(KpiScopes.Global, s.Scope); Assert.Null(s.ScopeKey); Assert.Equal(CapturedAt, s.CapturedAtUtc); }); // Exact (Metric, Value) catalog mapping. AssertMetric(samples, "totalEventsLastHour", 4200); AssertMetric(samples, "errorEventsLastHour", 17); AssertMetric(samples, "backlogTotal", 305); } [Fact] public async Task CollectAsync_anchors_the_window_on_capturedAtUtc() { var repo = Substitute.For(); repo.GetKpiSnapshotAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new AuditLogKpiSnapshot(0, 0, 0, CapturedAt)); var sut = new AuditLogKpiSampleSource(repo); await sut.CollectAsync(CapturedAt); // The trailing 1h window is anchored on the recorder's shared instant, // not the repository's server-side UtcNow, so the sample is reproducible. await repo.Received(1).GetKpiSnapshotAsync( TimeSpan.FromHours(1), CapturedAt, Arg.Any()); } [Fact] public async Task CollectAsync_returns_empty_when_snapshot_is_null() { var repo = Substitute.For(); repo.GetKpiSnapshotAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns((AuditLogKpiSnapshot?)null!); var sut = new AuditLogKpiSampleSource(repo); var samples = await sut.CollectAsync(CapturedAt); Assert.NotNull(samples); Assert.Empty(samples); } private static void AssertMetric( IReadOnlyList samples, string metric, double expectedValue) { var sample = Assert.Single(samples, s => s.Metric == metric); Assert.Equal(expectedValue, sample.Value); } }