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.
This commit is contained in:
+136
-14
@@ -14,26 +14,30 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
|
||||
|
||||
// Regression coverage for ConfigurationDatabase-010 (repositories / InstanceLocator lacked
|
||||
// direct tests) and ConfigurationDatabase-011 (inconsistent constructor null-guarding).
|
||||
// Regression coverage for repositories / InstanceLocator that lacked direct tests, and for
|
||||
// inconsistent constructor null-guarding.
|
||||
|
||||
public class ExternalSystemRepositoryTests : IDisposable
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly ExternalSystemRepository _repository;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>ExternalSystemRepositoryTests</c> class.</summary>
|
||||
public ExternalSystemRepositoryTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
_repository = new ExternalSystemRepository(_context);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added external system round-trips through get by id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddExternalSystem_AndGetById_RoundTrips()
|
||||
{
|
||||
@@ -46,6 +50,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Equal("Sys", loaded!.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the external system timeout seconds value round-trips.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddExternalSystem_TimeoutSeconds_RoundTrips()
|
||||
{
|
||||
@@ -61,6 +67,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Equal(15, loaded!.TimeoutSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting methods by external system id returns only the parent system's methods.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMethodsByExternalSystemId_FiltersByParent()
|
||||
{
|
||||
@@ -76,6 +84,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Single(methods);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a database connection removes the entity.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteDatabaseConnection_RemovesEntity()
|
||||
{
|
||||
@@ -89,14 +99,17 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetDatabaseConnectionByIdAsync(conn.Id));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing an ExternalSystemRepository with a null context throws an ArgumentNullException.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullContext_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new ExternalSystemRepository(null!));
|
||||
}
|
||||
|
||||
// ── ExternalSystemGateway-011: name-keyed repository lookups ──
|
||||
// ── Name-keyed repository lookups ──
|
||||
|
||||
/// <summary>Verifies that getting an external system by name returns the matching row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetExternalSystemByName_ReturnsMatchingRow()
|
||||
{
|
||||
@@ -113,6 +126,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Equal("https://beta.test", loaded.EndpointUrl);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an external system by a missing name returns null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetExternalSystemByName_MissingName_ReturnsNull()
|
||||
{
|
||||
@@ -123,6 +138,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetExternalSystemByNameAsync("DoesNotExist"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a method by name returns the method scoped to its parent system.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMethodByName_ReturnsMethodScopedToParentSystem()
|
||||
{
|
||||
@@ -147,6 +164,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Equal("/b", method.Path);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a method by a missing name returns null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetMethodByName_MissingMethod_ReturnsNull()
|
||||
{
|
||||
@@ -157,6 +176,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetMethodByNameAsync(sys.Id, "noSuchMethod"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a database connection by name returns the matching row.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetDatabaseConnectionByName_ReturnsMatchingRow()
|
||||
{
|
||||
@@ -173,6 +194,8 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
Assert.Equal("Server=hist;Database=h;", loaded.ConnectionString);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting a database connection by a missing name returns null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetDatabaseConnectionByName_MissingName_ReturnsNull()
|
||||
{
|
||||
@@ -189,18 +212,22 @@ public class NotificationRepositoryTests : IDisposable
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly NotificationRepository _repository;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>NotificationRepositoryTests</c> class.</summary>
|
||||
public NotificationRepositoryTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
_repository = new NotificationRepository(_context);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added notification list with recipients round-trips.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddNotificationList_WithRecipients_RoundTrips()
|
||||
{
|
||||
@@ -217,6 +244,8 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Single(all[0].Recipients);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added SMTP configuration round-trips through get by id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSmtpConfiguration_AndGetById_RoundTrips()
|
||||
{
|
||||
@@ -229,6 +258,8 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Equal("smtp.example.test", loaded!.Host);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added SMS configuration round-trips through get by id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSmsConfiguration_AndGetById_RoundTrips()
|
||||
{
|
||||
@@ -241,6 +272,8 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Equal("ACbyid123", loaded!.AccountSid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a notification list removes the entity.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteNotificationList_RemovesEntity()
|
||||
{
|
||||
@@ -254,6 +287,8 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetNotificationListByIdAsync(list.Id));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added SMS configuration round-trips.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSmsConfiguration_RoundTrips()
|
||||
{
|
||||
@@ -282,6 +317,8 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetSmsConfigurationAsync());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a notification list persists its type.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task NotificationList_PersistsType()
|
||||
{
|
||||
@@ -293,6 +330,7 @@ public class NotificationRepositoryTests : IDisposable
|
||||
Assert.Equal(NotificationType.Email, loaded!.Type);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing a NotificationRepository with a null context throws an ArgumentNullException.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullContext_Throws()
|
||||
{
|
||||
@@ -304,17 +342,21 @@ public class NotificationOutboxConfigurationTests : IDisposable
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>NotificationOutboxConfigurationTests</c> class.</summary>
|
||||
public NotificationOutboxConfigurationTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a fully populated notification round-trips.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Notification_FullyPopulated_RoundTrips()
|
||||
{
|
||||
@@ -375,10 +417,12 @@ public class NotificationOutboxConfigurationTests : IDisposable
|
||||
Assert.Equal(originParentExecutionId, loaded.OriginParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null origin execution id round-trips as null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Notification_NullOriginExecutionId_RoundTripsAsNull()
|
||||
{
|
||||
// Audit Log #23: OriginExecutionId is an additive nullable column —
|
||||
// OriginExecutionId is an additive nullable column —
|
||||
// notifications raised outside a script execution (or submitted before
|
||||
// the column existed) persist and reload it as null.
|
||||
var id = Guid.NewGuid().ToString();
|
||||
@@ -395,6 +439,8 @@ public class NotificationOutboxConfigurationTests : IDisposable
|
||||
Assert.Null(loaded!.OriginExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null origin parent execution id round-trips as null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Notification_NullOriginParentExecutionId_RoundTripsAsNull()
|
||||
{
|
||||
@@ -415,6 +461,8 @@ public class NotificationOutboxConfigurationTests : IDisposable
|
||||
Assert.Null(loaded!.OriginParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the notification status is persisted as a string.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Notification_StatusPersistsAsString()
|
||||
{
|
||||
@@ -437,18 +485,20 @@ public class NotificationOutboxConfigurationTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Coverage for the Notification Outbox repository (Task 5 of the notification-outbox feature).
|
||||
// Coverage for the Notification Outbox repository (notification-outbox feature).
|
||||
public class NotificationOutboxRepositoryTests : IDisposable
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly NotificationOutboxRepository _repository;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>NotificationOutboxRepositoryTests</c> class.</summary>
|
||||
public NotificationOutboxRepositoryTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
_repository = new NotificationOutboxRepository(_context);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
@@ -480,6 +530,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
// — the method is raw-SQL (IF NOT EXISTS … INSERT) so it must execute against
|
||||
// SQL Server, not the SQLite in-memory provider this class uses.
|
||||
|
||||
/// <summary>Verifies that get-due returns pending and due retrying notifications, ordered by creation time and capped at the batch size.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetDueAsync_ReturnsPendingAndDueRetrying_OrderedByCreatedAt_CappedAtBatchSize()
|
||||
{
|
||||
@@ -515,6 +567,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Equal(retryingDue.NotificationId, capped[1].NotificationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that get by id returns the row when present and null otherwise.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ReturnsRowOrNull()
|
||||
{
|
||||
@@ -527,6 +581,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetByIdAsync("does-not-exist"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an update persists the status transition.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAsync_PersistsStatusTransition()
|
||||
{
|
||||
@@ -553,6 +609,7 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
/// retry/discard one-shots) depend on it to avoid reporting success against a
|
||||
/// purged notification.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAsync_RowPurgedBeforeWrite_ReturnsFalse()
|
||||
{
|
||||
@@ -573,6 +630,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.False(await _repository.UpdateAsync(loaded));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a conditional update from Parked applies and returns true when the status is Parked.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TryUpdateFromParkedAsync_StatusIsParked_UpdatesAndReturnsTrue()
|
||||
{
|
||||
@@ -596,8 +655,9 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
/// <summary>
|
||||
/// Direct CAS-miss: the row is currently Delivered (never Parked in this
|
||||
/// scenario), so the conditional WHERE Status == Parked matches nothing and
|
||||
/// the row is left completely untouched — arch-review 2026-08-19 P5-4.
|
||||
/// the row is left completely untouched.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TryUpdateFromParkedAsync_StatusIsNotParked_ReturnsFalse_AndLeavesRowUnchanged()
|
||||
{
|
||||
@@ -617,6 +677,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Equal(deliveredAt, reloaded.DeliveredAt);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a conditional update from Parked returns false when the row was purged before the write.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TryUpdateFromParkedAsync_RowPurgedBeforeWrite_ReturnsFalse()
|
||||
{
|
||||
@@ -636,7 +698,7 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic (non-sleep) proof of the arch-review 2026-08-19 P5-4 race:
|
||||
/// Deterministic (non-sleep) proof of the race:
|
||||
/// the operator reads a Parked row, but before its write lands the
|
||||
/// dispatcher (or a concurrent second operator action) has already moved
|
||||
/// the row to Delivered. <see cref="TryUpdateFromParkedAsync"/>'s CAS must
|
||||
@@ -646,6 +708,7 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
/// operator path used PRE-FIX — has no such guard, which is the mechanism
|
||||
/// the finding describes.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task TryUpdateFromParkedAsync_ConcurrentlyDelivered_ReturnsFalse_AndDoesNotOverwrite()
|
||||
{
|
||||
@@ -683,13 +746,14 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
/// <see cref="NotificationOutboxRepository.UpdateAsync"/> instead — the
|
||||
/// method the operator Retry/Discard path called pre-fix — and shows it
|
||||
/// silently clobbers the Delivered row. This documents the exact defect
|
||||
/// arch-review 2026-08-19 P5-4 fixed by routing the operator path through
|
||||
/// fixed by routing the operator path through
|
||||
/// <see cref="TryUpdateFromParkedAsync"/>: UpdateAsync itself is unchanged
|
||||
/// (the dispatcher still needs an unconditional write), so this scenario
|
||||
/// remains "true" by design — it is the operator actor's call-site choice
|
||||
/// that was the bug, proven fixed by the actor-level tests in
|
||||
/// NotificationOutbox.Tests (Discard_ConcurrentlyDelivered_...).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task UpdateAsync_Unconditional_WouldOverwriteConcurrentlyDeliveredRow_DemonstratingP5_4Mechanism()
|
||||
{
|
||||
@@ -711,7 +775,7 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
operatorRead!.Status = NotificationStatus.Discarded;
|
||||
// The unconditional UPDATE has no status guard, so it "succeeds" here —
|
||||
// this is the bug. It clobbers the Delivered row to Discarded and drops
|
||||
// DeliveredAt, which is exactly the silent-overwrite hazard P5-4 closes
|
||||
// DeliveredAt, which is exactly the silent-overwrite hazard the conditional update closes
|
||||
// by moving the operator path to TryUpdateFromParkedAsync.
|
||||
Assert.True(await _repository.UpdateAsync(operatorRead));
|
||||
|
||||
@@ -721,6 +785,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Null(reloaded.DeliveredAt);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query applies its filters, orders by creation time descending and paginates.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task QueryAsync_AppliesFilters_OrdersByCreatedAtDescending_AndPaginates()
|
||||
{
|
||||
@@ -755,6 +821,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Equal("Tank Level 0", page2[1].Subject);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query's subject keyword filter matches by containment.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task QueryAsync_SubjectKeyword_UsesContains()
|
||||
{
|
||||
@@ -770,6 +838,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Equal("Tank Level High", rows[0].Subject);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a stuck-only query returns non-terminal rows older than the cutoff.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task QueryAsync_StuckOnly_ReturnsNonTerminalRowsOlderThanCutoff()
|
||||
{
|
||||
@@ -798,6 +868,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
rows.Select(r => r.Subject).OrderBy(s => s).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query's from/to filter applies to the creation time.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task QueryAsync_FromTo_FilterAgainstCreatedAt()
|
||||
{
|
||||
@@ -820,6 +892,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
rows.Select(r => r.Subject).OrderBy(s => s).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query's status and type filters apply.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task QueryAsync_StatusAndTypeFilters()
|
||||
{
|
||||
@@ -838,6 +912,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Equal("Parked Row", rows[0].Subject);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting terminal rows older than the cutoff removes only those rows and leaves the others.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteTerminalOlderThanAsync_DeletesTerminalRowsOlderThanCutoff_LeavesOthers()
|
||||
{
|
||||
@@ -867,6 +943,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Contains(oldPending.NotificationId, remaining);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that computing KPIs populates the snapshot fields.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ComputeKpisAsync_ComputesSnapshotFields()
|
||||
{
|
||||
@@ -903,6 +981,8 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.True(kpis.OldestPendingAge!.Value >= TimeSpan.FromMinutes(115));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that computing KPIs with no non-terminal rows yields a null oldest-pending age.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ComputeKpisAsync_NoNonTerminalRows_OldestPendingAgeIsNull()
|
||||
{
|
||||
@@ -919,6 +999,7 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Null(kpis.OldestPendingAge);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing a NotificationOutboxRepository with a null context throws an ArgumentNullException.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullContext_Throws()
|
||||
{
|
||||
@@ -931,18 +1012,22 @@ public class SiteRepositoryTests : IDisposable
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly SiteRepository _repository;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>SiteRepositoryTests</c> class.</summary>
|
||||
public SiteRepositoryTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
_repository = new SiteRepository(_context);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an added site round-trips through get by identifier.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddSite_AndGetByIdentifier_RoundTrips()
|
||||
{
|
||||
@@ -955,6 +1040,8 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Equal("Site1", loaded!.Name);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a site through the stub-attach path removes the entity.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteSite_ViaStubAttachPath_RemovesEntity()
|
||||
{
|
||||
@@ -972,6 +1059,8 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetSiteByIdAsync(id));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a data connection through the stub-attach path removes the entity.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteDataConnection_ViaStubAttachPath_RemovesEntity()
|
||||
{
|
||||
@@ -991,6 +1080,8 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetDataConnectionByIdAsync(id));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting instances by site id returns only that site's instances.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetInstancesBySiteId_FiltersBySite()
|
||||
{
|
||||
@@ -1007,6 +1098,8 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Single(instances);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting instances referencing a data connection returns an empty list when there are no bindings.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetInstancesReferencingDataConnection_NoBindings_ReturnsEmpty()
|
||||
{
|
||||
@@ -1025,6 +1118,8 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting instances referencing a data connection returns the instance bound to it.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetInstancesReferencingDataConnection_OneBinding_ReturnsThatInstance()
|
||||
{
|
||||
@@ -1049,6 +1144,8 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Equal(instance.Id, result[0].Id);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting instances referencing a data connection returns one instance when two bindings belong to the same instance.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetInstancesReferencingDataConnection_TwoBindingsSameInstance_ReturnsOne()
|
||||
{
|
||||
@@ -1077,6 +1174,7 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Equal(instance.Id, result[0].Id);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing a SiteRepository with a null context throws an ArgumentNullException.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullContext_Throws()
|
||||
{
|
||||
@@ -1093,6 +1191,7 @@ public class SiteRepositoryTests : IDisposable
|
||||
/// up to date. (The bump is unattributed — a connection has no owning template —
|
||||
/// so both the global and structure counters must move.)
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SaveChanges_DataConnectionEdit_BumpsWatermark()
|
||||
{
|
||||
@@ -1134,6 +1233,7 @@ public class SiteRepositoryTests : IDisposable
|
||||
/// that touches no data connection must leave the watermark alone, or every
|
||||
/// site/area edit would needlessly invalidate the whole flatten cache.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SaveChanges_WithoutDataConnectionChange_DoesNotBumpWatermark()
|
||||
{
|
||||
@@ -1154,12 +1254,14 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly DeploymentManagerRepository _repository;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>DeploymentManagerRepositoryTests</c> class.</summary>
|
||||
public DeploymentManagerRepositoryTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
_repository = new DeploymentManagerRepository(_context);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
@@ -1180,6 +1282,8 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting the current deployment status returns the most recently added record.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AddDeploymentRecord_AndGetCurrentStatus_ReturnsMostRecent()
|
||||
{
|
||||
@@ -1196,10 +1300,12 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
Assert.Equal("d-002", current!.DeploymentId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that when records share a timestamp, the current deployment status picks the highest id as a deterministic tiebreaker.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetCurrentDeploymentStatus_SameTickRecords_DeterministicTiebreakerPicksHighestId()
|
||||
{
|
||||
// DeploymentManager-026: deployments are insert-only, so two records for the
|
||||
// Deployments are insert-only, so two records for the
|
||||
// same instance can tie on DeployedAt when created within the same clock tick
|
||||
// (rapid redeploy, or redeploy right after a timed-out attempt). Without a
|
||||
// secondary sort key the "current" read is non-deterministic. The repository
|
||||
@@ -1221,6 +1327,8 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
Assert.Equal("d-new", current!.DeploymentId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting a deployment record through the stub-attach path removes the entity.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteDeploymentRecord_ViaStubAttachPath_RemovesEntity()
|
||||
{
|
||||
@@ -1239,7 +1347,7 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConfigurationDatabase-024: CD-017 added optimistic-concurrency to
|
||||
/// Optimistic concurrency was added to
|
||||
/// <c>DeleteDeploymentRecordAsync(int id, byte[] expectedRowVersion)</c> — the stub-attach
|
||||
/// path now seeds <c>OriginalValues["RowVersion"]</c> from the caller's last-observed
|
||||
/// value so the generated SQL becomes <c>DELETE … WHERE Id = @id AND RowVersion = @prior</c>.
|
||||
@@ -1248,10 +1356,10 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
/// admin / handler shape), calls Delete with that token, and the delete completes
|
||||
/// without throwing <see cref="Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException"/>.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteDeploymentRecord_CurrentRowVersion_StubAttachPath_DeleteSucceeds()
|
||||
{
|
||||
// STM: CD-024-RowVersionDeleteHappyPath marker.
|
||||
var instance = await SeedInstanceAsync();
|
||||
var record = new DeploymentRecord("d-rv-001", "admin")
|
||||
{
|
||||
@@ -1276,6 +1384,8 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
Assert.Null(await _repository.GetDeploymentRecordByIdAsync(id));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that deleting an instance first removes its deployment records, which have a restrict foreign key.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task DeleteInstance_RemovesRestrictFkDeploymentRecordsFirst()
|
||||
{
|
||||
@@ -1293,10 +1403,12 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
Assert.Empty(await _repository.GetDeploymentsByInstanceIdAsync(instance.Id));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an instance by id includes its native alarm source overrides.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetInstanceByIdAsync_IncludesNativeAlarmSourceOverrides()
|
||||
{
|
||||
// Regression for #226: NativeAlarmSourceOverrides was omitted from the Include chain.
|
||||
// Regression: NativeAlarmSourceOverrides was omitted from the Include chain.
|
||||
var instance = await SeedInstanceAsync();
|
||||
instance.NativeAlarmSourceOverrides.Add(
|
||||
new InstanceNativeAlarmSourceOverride("src/AlarmA") { InstanceId = instance.Id });
|
||||
@@ -1310,10 +1422,12 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
Assert.Equal("src/AlarmA", loaded.NativeAlarmSourceOverrides.First().SourceCanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting an instance by unique name includes its native alarm source overrides.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetInstanceByUniqueNameAsync_IncludesNativeAlarmSourceOverrides()
|
||||
{
|
||||
// Regression for #226: NativeAlarmSourceOverrides was omitted from the Include chain.
|
||||
// Regression: NativeAlarmSourceOverrides was omitted from the Include chain.
|
||||
var instance = await SeedInstanceAsync();
|
||||
instance.NativeAlarmSourceOverrides.Add(
|
||||
new InstanceNativeAlarmSourceOverride("src/AlarmB") { InstanceId = instance.Id });
|
||||
@@ -1327,6 +1441,7 @@ public class DeploymentManagerRepositoryTests : IDisposable
|
||||
Assert.Equal("src/AlarmB", loaded.NativeAlarmSourceOverrides.First().SourceCanonicalName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing a DeploymentManagerRepository with a null context throws an ArgumentNullException.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullContext_Throws()
|
||||
{
|
||||
@@ -1339,18 +1454,22 @@ public class InstanceLocatorTests : IDisposable
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
private readonly InstanceLocator _locator;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>InstanceLocatorTests</c> class.</summary>
|
||||
public InstanceLocatorTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
_locator = new InstanceLocator(_context);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting the site id for an existing instance returns the site identifier.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetSiteIdForInstance_WhenFound_ReturnsSiteIdentifier()
|
||||
{
|
||||
@@ -1367,6 +1486,8 @@ public class InstanceLocatorTests : IDisposable
|
||||
Assert.Equal("SITE-001", result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that getting the site id for a missing instance returns null.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetSiteIdForInstance_WhenInstanceNotFound_ReturnsNull()
|
||||
{
|
||||
@@ -1374,6 +1495,7 @@ public class InstanceLocatorTests : IDisposable
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that constructing an InstanceLocator with a null context throws an ArgumentNullException.</summary>
|
||||
[Fact]
|
||||
public void Constructor_NullContext_Throws()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user