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:
+118
-28
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user