513fa1aa16
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.
137 lines
6.0 KiB
C#
137 lines
6.0 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
|
|
|
|
// Coverage for per-node KPI aggregation in the Notification Outbox repository
|
|
// (per-node stuck-count KPIs).
|
|
public class NotificationOutboxRepositoryPerNodeKpiTests
|
|
{
|
|
private static ScadaBridgeDbContext NewContext() => SqliteTestHelper.CreateInMemoryContext();
|
|
|
|
private static Notification NewNotification(
|
|
string sourceSiteId,
|
|
NotificationStatus status,
|
|
DateTimeOffset createdAt,
|
|
DateTimeOffset? deliveredAt = null,
|
|
string? sourceNode = null)
|
|
{
|
|
return new Notification(
|
|
Guid.NewGuid().ToString(), NotificationType.Email, "Ops List", "Subject", "Body", sourceSiteId)
|
|
{
|
|
Status = status,
|
|
CreatedAt = createdAt,
|
|
DeliveredAt = deliveredAt,
|
|
SourceNode = sourceNode,
|
|
};
|
|
}
|
|
|
|
/// <summary>Verifies that per-node KPIs are aggregated separately for each source node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ComputePerNodeKpisAsync_AggregatesMetricsPerNode()
|
|
{
|
|
await using var ctx = NewContext();
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
// node-a: 1 pending (stuck, created 20m ago), 1 parked
|
|
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Pending,
|
|
createdAt: now.AddMinutes(-20), sourceNode: "node-a"));
|
|
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Parked,
|
|
createdAt: now.AddMinutes(-5), sourceNode: "node-a"));
|
|
// node-b: 1 delivered in-window, 1 pending (fresh)
|
|
ctx.Notifications.Add(NewNotification("plant-b", NotificationStatus.Delivered,
|
|
createdAt: now.AddHours(-2), deliveredAt: now.AddMinutes(-2), sourceNode: "node-b"));
|
|
ctx.Notifications.Add(NewNotification("plant-b", NotificationStatus.Pending,
|
|
createdAt: now.AddMinutes(-1), sourceNode: "node-b"));
|
|
// NULL SourceNode — must be excluded from per-node results
|
|
ctx.Notifications.Add(NewNotification("plant-c", NotificationStatus.Pending,
|
|
createdAt: now.AddMinutes(-5), sourceNode: null));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var repo = new NotificationOutboxRepository(ctx);
|
|
var result = await repo.ComputePerNodeKpisAsync(
|
|
stuckCutoff: now.AddMinutes(-10), deliveredSince: now.AddMinutes(-30));
|
|
|
|
// Only node-a and node-b — the null-node row is excluded.
|
|
Assert.Equal(2, result.Count);
|
|
|
|
var a = result.Single(n => n.SourceNode == "node-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(n => n.SourceNode == "node-b");
|
|
Assert.Equal(1, b.QueueDepth);
|
|
Assert.Equal(0, b.StuckCount);
|
|
Assert.Equal(0, b.ParkedCount);
|
|
Assert.Equal(1, b.DeliveredLastInterval);
|
|
Assert.NotNull(b.OldestPendingAge);
|
|
}
|
|
|
|
/// <summary>Verifies that notifications with a null source node are excluded from the per-node KPIs.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ComputePerNodeKpisAsync_ExcludesNullSourceNode()
|
|
{
|
|
await using var ctx = NewContext();
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
// Only null-node rows — result must be empty.
|
|
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Pending,
|
|
createdAt: now.AddMinutes(-5), sourceNode: null));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var repo = new NotificationOutboxRepository(ctx);
|
|
var result = await repo.ComputePerNodeKpisAsync(
|
|
stuckCutoff: now.AddMinutes(-10), deliveredSince: now.AddMinutes(-30));
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
/// <summary>Verifies that having no notifications yields an empty per-node KPI result.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ComputePerNodeKpisAsync_ReturnsEmpty_WhenNoNotifications()
|
|
{
|
|
await using var ctx = NewContext();
|
|
var repo = new NotificationOutboxRepository(ctx);
|
|
var result = await repo.ComputePerNodeKpisAsync(
|
|
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddMinutes(-30));
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
/// <summary>Verifies that the oldest pending age reflects the older row.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ComputePerNodeKpisAsync_OldestPendingAge_ReflectsOlderRow()
|
|
{
|
|
await using var ctx = NewContext();
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
// node-a: pending 90m ago, retrying 40m ago.
|
|
// OldestPendingAge must reflect the 90m row.
|
|
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Pending,
|
|
createdAt: now.AddMinutes(-90), sourceNode: "node-a"));
|
|
ctx.Notifications.Add(NewNotification("plant-a", NotificationStatus.Retrying,
|
|
createdAt: now.AddMinutes(-40), sourceNode: "node-a"));
|
|
await ctx.SaveChangesAsync();
|
|
|
|
var repo = new NotificationOutboxRepository(ctx);
|
|
var result = await repo.ComputePerNodeKpisAsync(
|
|
stuckCutoff: now.AddMinutes(-10), deliveredSince: now.AddMinutes(-30));
|
|
|
|
var a = result.Single(n => n.SourceNode == "node-a");
|
|
Assert.Equal(2, a.QueueDepth);
|
|
Assert.Equal(2, a.StuckCount);
|
|
Assert.NotNull(a.OldestPendingAge);
|
|
Assert.True(a.OldestPendingAge >= TimeSpan.FromMinutes(85),
|
|
$"expected OldestPendingAge >= 85m, got {a.OldestPendingAge}");
|
|
Assert.True(a.OldestPendingAge < TimeSpan.FromMinutes(95),
|
|
$"expected OldestPendingAge < 95m, got {a.OldestPendingAge}");
|
|
}
|
|
}
|