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:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -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()
{