Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/NotificationOutboxRepositoryPerSiteKpiTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

96 lines
4.6 KiB
C#

using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
// Coverage for per-site KPI aggregation in the Notification Outbox repository
// (per-site KPIs).
public class NotificationOutboxRepositoryPerSiteKpiTests
{
private static ScadaBridgeDbContext NewContext() => SqliteTestHelper.CreateInMemoryContext();
private static Notification NewNotification(
string sourceSiteId,
NotificationStatus status,
DateTimeOffset createdAt,
DateTimeOffset? deliveredAt = null)
{
return new Notification(
Guid.NewGuid().ToString(), NotificationType.Email, "Ops List", "Subject", "Body", sourceSiteId)
{
Status = status,
CreatedAt = createdAt,
DeliveredAt = deliveredAt,
};
}
/// <summary>Verifies that per-site KPIs are aggregated separately for each site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputePerSiteKpisAsync_AggregatesMetricsPerSite()
{
await using var ctx = NewContext();
var now = DateTimeOffset.UtcNow;
// plant-a: 1 pending (stuck, created 20m ago), 1 parked
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Pending, createdAt: now.AddMinutes(-20)));
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Parked, createdAt: now.AddMinutes(-5)));
// plant-b: 1 delivered in-window, 1 pending (fresh)
ctx.Notifications.Add(NewNotification("plant-b", NotificationStatus.Delivered, createdAt: now.AddHours(-2), deliveredAt: now.AddMinutes(-2)));
ctx.Notifications.Add(NewNotification("plant-b", NotificationStatus.Pending, createdAt: now.AddMinutes(-1)));
// plant-c: 2 non-terminal rows of clearly different ages — pending 90m ago,
// retrying 40m ago. Both predate the 10m stuck cutoff. Exercises the
// in-memory g.Min(CreatedAt) oldest-age reduction and the Retrying branch
// of the QueueDepth/StuckCount predicates.
ctx.Notifications.Add(NewNotification("plant-c", NotificationStatus.Pending, createdAt: now.AddMinutes(-90)));
ctx.Notifications.Add(NewNotification("plant-c", NotificationStatus.Retrying, createdAt: now.AddMinutes(-40)));
await ctx.SaveChangesAsync();
var repo = new NotificationOutboxRepository(ctx);
var result = await repo.ComputePerSiteKpisAsync(
stuckCutoff: now.AddMinutes(-10), deliveredSince: now.AddMinutes(-30));
var a = result.Single(s => s.SourceSiteId == "plant-a");
Assert.Equal(1, a.QueueDepth);
Assert.Equal(1, a.StuckCount);
Assert.Equal(1, a.ParkedCount);
Assert.Equal(0, a.DeliveredLastInterval);
Assert.NotNull(a.OldestPendingAge);
var b = result.Single(s => s.SourceSiteId == "plant-b");
Assert.Equal(1, b.QueueDepth);
Assert.Equal(0, b.StuckCount);
Assert.Equal(1, b.DeliveredLastInterval);
// plant-c: both the Pending and Retrying rows count toward QueueDepth;
// both predate the stuck cutoff so both are stuck. OldestPendingAge must
// reflect the older (90m) row, not the 10m Retrying one.
var c = result.Single(s => s.SourceSiteId == "plant-c");
Assert.Equal(2, c.QueueDepth);
Assert.Equal(2, c.StuckCount);
Assert.Equal(0, c.ParkedCount);
Assert.NotNull(c.OldestPendingAge);
// Tolerant lower bound to absorb clock skew between seed time and the
// `now` captured inside ComputePerSiteKpisAsync.
Assert.True(c.OldestPendingAge >= TimeSpan.FromMinutes(85),
$"expected OldestPendingAge >= 85m, got {c.OldestPendingAge}");
Assert.True(c.OldestPendingAge < TimeSpan.FromMinutes(95),
$"expected OldestPendingAge < 95m, got {c.OldestPendingAge}");
}
/// <summary>Verifies that having no notifications yields an empty per-site KPI result.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputePerSiteKpisAsync_ReturnsEmpty_WhenNoNotifications()
{
await using var ctx = NewContext();
var repo = new NotificationOutboxRepository(ctx);
var result = await repo.ComputePerSiteKpisAsync(
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddMinutes(-30));
Assert.Empty(result);
}
}