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
@@ -18,7 +18,7 @@ using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests;
/// <summary>
/// WP-7: Tests for system-wide artifact deployment.
/// Tests for system-wide artifact deployment.
/// </summary>
public class ArtifactDeploymentServiceTests : TestKit
{
@@ -31,13 +31,14 @@ public class ArtifactDeploymentServiceTests : TestKit
private readonly ArtifactRevisionHashService _hashService = new();
private readonly ISecretStore _secretStore;
/// <summary>Initializes the test class with substitute repositories and an audit service.</summary>
public ArtifactDeploymentServiceTests()
{
_siteRepo = Substitute.For<ISiteRepository>();
_deploymentRepo = Substitute.For<IDeploymentManagerRepository>();
_templateRepo = Substitute.For<ITemplateEngineRepository>();
_externalSystemRepo = Substitute.For<IExternalSystemRepository>();
// DeploymentManager-025/-027: the notification repo is retained only so the
// The notification repo is retained only so the
// tests can assert the artifact path NEVER touches it (it is no longer a
// constructor dependency of ArtifactDeploymentService).
_notificationRepo = Substitute.For<INotificationRepository>();
@@ -64,6 +65,8 @@ public class ArtifactDeploymentServiceTests : TestKit
UpdatedUtc = DateTimeOffset.Parse("2026-01-02T00:00:00Z"),
};
/// <summary>Verifies that deploying to all sites when none are configured returns a failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_NoSites_ReturnsFailure()
{
@@ -77,6 +80,7 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Contains("No sites", result.Error);
}
/// <summary>Verifies that a per-site artifact result carries the site's identifying information.</summary>
[Fact]
public void SiteArtifactResult_ContainsSiteInfo()
{
@@ -87,6 +91,7 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Null(result.ErrorMessage);
}
/// <summary>Verifies that the artifact deployment summary counts successes and failures correctly.</summary>
[Fact]
public void ArtifactDeploymentSummary_CountsCorrectly()
{
@@ -103,12 +108,14 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Equal(3, summary.SiteResults.Count);
}
// ── DeploymentManager-010: one logical deployment id across all per-site commands ──
// ── One logical deployment id across all per-site commands ──
/// <summary>Verifies that every per-site command carries the same deployment id as the summary.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_AllPerSiteCommandsShareTheSummaryDeploymentId()
{
// DeploymentManager-010: previously each per-site DeployArtifactsCommand
// Previously each per-site DeployArtifactsCommand
// minted its own GUID, so one logical deployment produced N+1 unrelated
// ids. Every per-site command must now carry the SAME id, equal to the
// id reported in the summary and audit log.
@@ -144,8 +151,10 @@ public class ArtifactDeploymentServiceTests : TestKit
Arg.Any<CancellationToken>());
}
// ── DeploymentManager-014: real per-site success/failure coverage ──
// ── Real per-site success/failure coverage ──
/// <summary>Verifies that a partial failure is reported as a per-site success and failure matrix.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_PartialFailure_ReportsPerSiteMatrix()
{
@@ -170,12 +179,14 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Contains(result.Value.SiteResults, r => r.SiteId == "fail-site" && !r.Success);
}
// ── DeploymentManager-023: global artifact queries hoisted out of the per-site loop ──
// ── Global artifact queries hoisted out of the per-site loop ──
/// <summary>Verifies that the global artifact queries run once in total rather than once per site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_HoistsGlobalArtifactQueriesOutOfPerSiteLoop()
{
// DeploymentManager-023: previously each per-site iteration of the deploy-many
// Previously each per-site iteration of the deploy-many
// loop re-issued the global artifact queries (shared scripts, external systems,
// DB connections) — a textbook N+1 over the global sets. With three sites the
// queries must now be issued ONCE in total, regardless of site count.
@@ -197,7 +208,7 @@ public class ArtifactDeploymentServiceTests : TestKit
await _templateRepo.Received(1).GetAllSharedScriptsAsync(Arg.Any<CancellationToken>());
await _externalSystemRepo.Received(1).GetAllExternalSystemsAsync(Arg.Any<CancellationToken>());
await _externalSystemRepo.Received(1).GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>());
// DeploymentManager-025/-027: notification lists and SMTP configuration are
// Notification lists and SMTP configuration are
// central-only — the artifact path must NEVER fetch them, and the per-site
// command must NEVER carry them.
await _notificationRepo.DidNotReceive().GetAllNotificationListsAsync(Arg.Any<CancellationToken>());
@@ -221,6 +232,7 @@ public class ArtifactDeploymentServiceTests : TestKit
/// silently used the engine default (50 x 30 s) — while the sibling
/// <c>DatabaseConnectionArtifact</c> had always carried its equivalents.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployedExternalSystemArtifact_CarriesRetryPolicy()
{
@@ -246,10 +258,12 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Equal(TimeSpan.FromSeconds(120), artifact.RetryDelay);
}
/// <summary>Verifies that retrying a single site still runs the global artifact queries exactly once.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_SingleSitePath_StillRunsTheGlobalQueriesOnce()
{
// DeploymentManager-023: the single-site convenience overload still owns its
// The single-site convenience overload still owns its
// own global-fetch (it cannot inherit from a sweep), so for one site every
// global query is issued exactly once. Pin this so a future refactor cannot
// accidentally route RetryForSiteAsync through the multi-site loop and lose
@@ -264,7 +278,7 @@ public class ArtifactDeploymentServiceTests : TestKit
await _templateRepo.Received(1).GetAllSharedScriptsAsync(Arg.Any<CancellationToken>());
await _externalSystemRepo.Received(1).GetAllExternalSystemsAsync(Arg.Any<CancellationToken>());
await _externalSystemRepo.Received(1).GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>());
// DeploymentManager-025/-027: central-only — never fetched, never shipped.
// Central-only — never fetched, never shipped.
await _notificationRepo.DidNotReceive().GetAllNotificationListsAsync(Arg.Any<CancellationToken>());
await _notificationRepo.DidNotReceive().GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>());
Assert.All(recorder.Received, cmd =>
@@ -274,6 +288,8 @@ public class ArtifactDeploymentServiceTests : TestKit
});
}
/// <summary>Verifies that a successful retry for a site returns success and writes an audit entry.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_SiteSucceeds_ReturnsSuccessAndAudits()
{
@@ -290,8 +306,10 @@ public class ArtifactDeploymentServiceTests : TestKit
Arg.Any<string>(), "retry-site", Arg.Any<object>(), Arg.Any<CancellationToken>());
}
// ── PLAN-07 Task 4: single-site artifact deployment ──
// ── Single-site artifact deployment ──
/// <summary>Verifies that a single-site deploy sends artifacts only to the target site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToSiteAsync_DeploysOnlyToTargetSite()
{
@@ -313,6 +331,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.DidNotReceive().GetDataConnectionsBySiteIdAsync(2, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a single-site deploy to an unknown site returns a failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToSiteAsync_UnknownSite_ReturnsFailure()
{
@@ -327,6 +347,8 @@ public class ArtifactDeploymentServiceTests : TestKit
// ── Site artifact revision stamp (pending-updates §3.1) ──
/// <summary>Verifies that a successful deploy stamps the site's artifact hash and timestamp and saves.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_SuccessfulSite_StampsHashAndTimestampAndSaves()
{
@@ -348,6 +370,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a failed site keeps its previous artifact stamp.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_FailedSite_KeepsItsPreviousStamp()
{
@@ -376,6 +400,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.Received(1).UpdateSiteAsync(ok, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that the deployment record is still persisted and success reported when saving the stamp throws.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSitesAsync_StampSaveThrows_DeploymentRecordStillPersistedAndSuccessReported()
{
@@ -406,6 +432,8 @@ public class ArtifactDeploymentServiceTests : TestKit
});
}
/// <summary>Verifies that a successful retry stamps the site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_Success_StampsTheSite()
{
@@ -426,6 +454,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a failed retry does not stamp the site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_Failure_DoesNotStampTheSite()
{
@@ -443,6 +473,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.DidNotReceive().UpdateSiteAsync(Arg.Any<Site>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that the computed current artifact hashes match the hash a deploy stamps.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputeCurrentArtifactHashesAsync_MatchesTheHashADeployStamps()
{
@@ -470,10 +502,12 @@ public class ArtifactDeploymentServiceTests : TestKit
}
}
/// <summary>Verifies that the computed current artifact hashes match the stamped hash when data connections reference secrets.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputeCurrentArtifactHashesAsync_MatchesTheHashADeployStamps_WithSecrets()
{
// Final review M7: the parity above runs with no data connections. Pin it for a site
// The parity above runs with no data connections. Pin it for a site
// whose connections reference secrets, so the stamped hash (built from a command that
// carries a non-empty Secrets list) equals the "what would it be now" hash — a
// successful secrets deploy must read Current, not Out of date.
@@ -510,6 +544,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Equal(site.ArtifactRevisionHash, current[site.Id]);
}
/// <summary>Verifies that computing the current artifact hashes runs the global queries once.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputeCurrentArtifactHashesAsync_HoistsTheGlobalQueriesOnce()
{
@@ -531,6 +567,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.Received(1).GetDataConnectionsBySiteIdAsync(3, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that computing the current hashes with no sites returns an empty result without querying.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputeCurrentArtifactHashesAsync_NoSites_ReturnsEmptyWithoutQuerying()
{
@@ -544,6 +582,8 @@ public class ArtifactDeploymentServiceTests : TestKit
// ── Secrets ride the artifact deploy (design §3.1/§3.3) ──
/// <summary>Verifies that a site with a missing secret fails alone and nothing is sent to it.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployToAllSites_SiteWithMissingSecret_FailsThatSiteOnly_AndSendsNothingToIt()
{
@@ -589,6 +629,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Null(badSite.ArtifactRevisionHash);
}
/// <summary>Verifies that a missing secret is represented by the missing marker in the computed hash.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputeCurrentHashes_MissingSecret_UsesMissingMarker()
{
@@ -612,6 +654,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.NotEqual(beforeCreated[1], afterCreated[1]);
}
/// <summary>Verifies that deploy logs never contain secret material.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployLogs_NeverContainSecretMaterial()
{
@@ -640,7 +684,7 @@ public class ArtifactDeploymentServiceTests : TestKit
.Returns(Task.FromResult<StoredSecret?>(resolvedRow));
var recorder = new ArtifactProbeRecorder();
// Task 7: ok-site's command ships one secret, so the probe must ack it (SecretsApplied
// ok-site's command ships one secret, so the probe must ack it (SecretsApplied
// = shipped count) for the site to reconcile as a success — an unset SecretsApplied
// now reads as an old site (see the dedicated reconciliation tests below).
var probe = Sys.ActorOf(Props.Create(() =>
@@ -665,8 +709,10 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.DoesNotContain(capturingLogger.Messages, m => m.Contains(ciphertextHex, StringComparison.OrdinalIgnoreCase));
}
// ── Task 7: stamp gating on secret application + DeploySiteSecrets audit row ──
// ── Stamp gating on secret application + DeploySiteSecrets audit row ──
/// <summary>Verifies that an old site that reports no applied secrets fails and is not stamped when secrets were shipped.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OldSite_NullSecretsApplied_WithSecrets_FailsAndDoesNotStamp()
{
@@ -703,6 +749,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.DidNotReceive().UpdateSiteAsync(site, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a partial secrets apply fails the deploy and does not stamp the site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PartialApply_FailsAndDoesNotStamp()
{
@@ -740,6 +788,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.DidNotReceive().UpdateSiteAsync(site, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that an old site is still stamped when no secrets were shipped.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OldSite_NoSecrets_StillStamps()
{
@@ -766,6 +816,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.Received(1).UpdateSiteAsync(site, Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a site with no secret references receives an empty secrets list so it removes previously deployed ones.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task NoSecretReferences_ShipsEmptySecretsList_SoTheSiteRemovesPreviouslyDeployedOnes()
{
@@ -773,7 +825,7 @@ public class ArtifactDeploymentServiceTests : TestKit
// are marked removed and the mirror deletes them, while a NULL Secrets means "central
// shipped no secrets" and touches nothing. Dropping a site's LAST secret reference must
// therefore ship an empty list, not null — otherwise the previously deployed secret stays
// live on both site nodes forever (Task 13 live gate, check 5).
// live on both site nodes forever.
var site = new Site("Site One", "site-1") { Id = 1 };
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<Site> { site });
_siteRepo.GetDataConnectionsBySiteIdAsync(1, Arg.Any<CancellationToken>())
@@ -804,6 +856,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Arg.Any<string>(), Arg.Any<object?>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a secret deploy writes one audit row per site without any secret material.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SecretDeploy_WritesOneAuditRowPerSite_WithoutMaterial()
{
@@ -879,9 +933,11 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.DoesNotContain("\"tag\"", json, StringComparison.OrdinalIgnoreCase);
}
// ── Task 7 fix round 1: the same gate + audit row on the RetryForSiteAsync path ──
// ── The same gate + audit row on the RetryForSiteAsync path ──
// (review gap: only the DeployToAllSitesAsync/sweep path had coverage above)
/// <summary>Verifies that a retry for an old site that reports no applied secrets fails and is not stamped when secrets were shipped.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_OldSite_NullSecretsApplied_WithSecrets_FailsAndDoesNotStamp()
{
@@ -910,6 +966,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.DidNotReceive().UpdateSiteAsync(Arg.Any<Site>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a partial secrets apply on retry fails and does not stamp the site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_PartialApply_FailsAndDoesNotStamp()
{
@@ -940,6 +998,8 @@ public class ArtifactDeploymentServiceTests : TestKit
await _siteRepo.DidNotReceive().UpdateSiteAsync(Arg.Any<Site>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that a retry with a missing secret fails closed, sends nothing and writes one audit row.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_MissingSecret_FailsClosed_SendsNothing_WritesOneAuditRow()
{
@@ -988,6 +1048,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Contains("\"secrets\":[]", json);
}
/// <summary>Verifies that a successful retry with secrets stamps the site and writes one audit row.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSiteAsync_HappyPath_WithSecrets_StampsAndWritesOneAuditRow()
{
@@ -1088,12 +1150,27 @@ public class ArtifactDeploymentServiceTests : TestKit
/// </summary>
private sealed class CapturingLogger<T> : ILogger<T>
{
/// <summary>Gets the formatted log messages captured so far.</summary>
public List<string> Messages { get; } = [];
/// <summary>Returns no scope because the fake does not track scopes.</summary>
/// <typeparam name="TState">The type of the state object.</typeparam>
/// <param name="state">The scope or log state.</param>
/// <returns>A no-op scope.</returns>
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
/// <summary>Reports whether logging is enabled for the given level.</summary>
/// <param name="logLevel">The log level.</param>
/// <returns>True if the level is enabled; otherwise false.</returns>
public bool IsEnabled(LogLevel logLevel) => true;
/// <summary>Records the log entry for later assertions.</summary>
/// <typeparam name="TState">The type of the state object.</typeparam>
/// <param name="logLevel">The log level.</param>
/// <param name="eventId">The event id.</param>
/// <param name="state">The scope or log state.</param>
/// <param name="exception">The exception associated with the entry, if any.</param>
/// <param name="formatter">Formats the state and exception into the message text.</param>
public void Log<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
@@ -1108,13 +1185,14 @@ public class ArtifactDeploymentServiceTests : TestKit
private sealed class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
/// <summary>Does nothing; there is nothing to release.</summary>
public void Dispose() { }
}
}
/// <summary>
/// Per-test recorder for <see cref="ArtifactProbeActor"/>. DeploymentManager-024:
/// each test owns its own instance, passed into the actor's constructor, so
/// Per-test recorder for <see cref="ArtifactProbeActor"/>.
/// Each test owns its own instance, passed into the actor's constructor, so
/// the received-command list is no longer shared static state that races
/// under parallel test execution.
/// </summary>
@@ -1131,6 +1209,9 @@ public class ArtifactDeploymentServiceTests : TestKit
/// </summary>
private class ArtifactProbeActor : ReceiveActor
{
/// <summary>Initializes the actor with the per-test recorder and the site ids that should fail.</summary>
/// <param name="recorder">The recorder that receives every deploy command.</param>
/// <param name="failingSites">The site ids for which the actor replies with a failure.</param>
public ArtifactProbeActor(ArtifactProbeRecorder recorder, params string[] failingSites)
{
var failSet = new HashSet<string>(failingSites);
@@ -1149,16 +1230,10 @@ public class ArtifactDeploymentServiceTests : TestKit
}
}
/// <summary>
/// Task 7: stand-in CentralCommunicationActor that always reports the deploy itself as
/// successful but lets a test control exactly what <see cref="ArtifactDeploymentResponse.SecretsApplied"/>
/// comes back for a given command — an old site (<c>null</c>), a partial apply (a count
/// short of what was shipped), or a correct ack (the shipped count) — so the secrets-
/// reconciliation gate can be exercised independently of the wire-level success/failure
/// the shared <see cref="ArtifactProbeActor"/> models.
/// </summary>
// ── Final review M5: a central secret-store outage is contained to the sites that need it ──
// ── A central secret-store outage is contained to the sites that need it ──
/// <summary>Verifies that a central secret-store outage fails only the secret-referencing site and audits it.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StoreOutage_FailsOnlyTheSecretReferencingSite_AndAuditsIt()
{
@@ -1186,7 +1261,7 @@ public class ArtifactDeploymentServiceTests : TestKit
Arg.Any<object>(), Arg.Any<CancellationToken>()))
.Do(ci => audited = ci[5]);
// Before M5 the store exception escaped the build loop and the whole sweep threw.
// Previously the store exception escaped the build loop and the whole sweep threw.
var result = await service.DeployToAllSitesAsync("admin");
Assert.True(result.IsSuccess);
@@ -1210,6 +1285,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Contains("Central secret store could not be read", json);
}
/// <summary>Verifies that a secret-store outage omits only the secret-referencing site from the computed hashes.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ComputeCurrentHashes_StoreOutage_OmitsOnlyTheSecretReferencingSite()
{
@@ -1234,6 +1311,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.True(hashes.ContainsKey(2));
}
/// <summary>Verifies that a secret-store outage on retry fails the site without sending anything.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSite_StoreOutage_FailsWithoutSending()
{
@@ -1261,6 +1340,8 @@ public class ArtifactDeploymentServiceTests : TestKit
// ── Removal-only deploys are audited (the site reports the names it retired) ──
/// <summary>Verifies that a removal-only deploy writes an audit row naming the removed secrets.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RemovalOnlyDeploy_WritesAuditRowNamingTheRemovedSecrets()
{
@@ -1294,6 +1375,8 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.Contains("\"success\":true", json);
}
/// <summary>Verifies that a removal-only retry writes an audit row naming the removed secrets.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RetryForSite_RemovalOnly_WritesAuditRowNamingTheRemovedSecrets()
{
@@ -1320,6 +1403,9 @@ public class ArtifactDeploymentServiceTests : TestKit
/// <summary>Acks every command as applied (0 shipped secrets) and reports the given names as retired.</summary>
private class RemovingProbeActor : ReceiveActor
{
/// <summary>Initializes the actor with the recorder and the secret names to report as retired.</summary>
/// <param name="recorder">The recorder that receives every deploy command.</param>
/// <param name="removed">The secret names reported as removed by the site.</param>
public RemovingProbeActor(ArtifactProbeRecorder recorder, IReadOnlyList<string> removed)
{
Receive<SiteEnvelope>(env =>
@@ -1335,8 +1421,12 @@ public class ArtifactDeploymentServiceTests : TestKit
}
}
/// <summary>Stand-in CentralCommunicationActor that always reports the deploy itself as successful but lets a test control exactly what <see cref="ArtifactDeploymentResponse.SecretsApplied"/> comes back for a given command: an old site (<c>null</c>), a partial apply, or a correct ack, so the secrets-reconciliation gate can be exercised independently of the wire-level success or failure that <see cref="ArtifactProbeActor"/> models.</summary>
private class SecretsAckProbeActor : ReceiveActor
{
/// <summary>Initializes the actor with the recorder and the function that decides the applied-secrets count.</summary>
/// <param name="recorder">The recorder that receives every deploy command.</param>
/// <param name="secretsApplied">Computes the applied-secrets count to acknowledge for a command.</param>
public SecretsAckProbeActor(ArtifactProbeRecorder recorder, Func<DeployArtifactsCommand, int?> secretsApplied)
{
Receive<SiteEnvelope>(env =>