fix(site-secrets): ship an empty Secrets list, not null, when a site references no secrets
The site applies DeployArtifactsCommand.Secrets as the full set (rows absent from it are soft-removed and DeployedSecretsMirror deletes them on both nodes), while null means 'central shipped no secrets' and touches nothing. The command builder shipped null for a site with no references, so dropping the LAST secret: reference never removed the secret from either site node. Found by the Task 13 live gate (check 5: site-a-a and site-a-b both still held sb-gate-mxgw r1, deployed_secrets.is_removed=0 after the removal deploy). Central now always ships a list. Old sites ignore proto field 9; the artifact hash is unchanged (empty and null hash identically); SecretsDeployed stays null and no DeploySiteSecrets audit row is written when nothing shipped. Regression test NoSecretReferences_ShipsEmptySecretsList_SoTheSiteRemovesPreviouslyDeployedOnes (red before, green after). Spec notes in Component-DeploymentManager/SiteRuntime + the design doc's as-built section.
This commit is contained in:
@@ -726,6 +726,44 @@ public class ArtifactDeploymentServiceTests : TestKit
|
||||
await _siteRepo.Received(1).UpdateSiteAsync(site, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoSecretReferences_ShipsEmptySecretsList_SoTheSiteRemovesPreviouslyDeployedOnes()
|
||||
{
|
||||
// The site applies DeployArtifactsCommand.Secrets as the FULL set: rows absent from it
|
||||
// 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).
|
||||
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>())
|
||||
.Returns(new List<DataConnection>
|
||||
{
|
||||
new("GW1", "MxGateway", 1) { PrimaryConfiguration = """{"ApiKey":"a-literal-key"}""" }
|
||||
});
|
||||
|
||||
var recorder = new ArtifactProbeRecorder();
|
||||
var probe = Sys.ActorOf(Props.Create(() => new SecretsAckProbeActor(recorder, _ => 0)));
|
||||
var service = CreateServiceWithCommActor(probe);
|
||||
|
||||
var result = await service.DeployToAllSitesAsync("admin");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
var cmd = Assert.Single(recorder.Received);
|
||||
Assert.NotNull(cmd.Secrets);
|
||||
Assert.Empty(cmd.Secrets!);
|
||||
Assert.Null(cmd.MissingSecrets);
|
||||
var siteResult = Assert.Single(result.Value.SiteResults);
|
||||
Assert.True(siteResult.Success);
|
||||
// Nothing shipped => nothing to confirm; SecretsDeployed stays null, and no
|
||||
// DeploySiteSecrets audit row is written for a deploy that carried no secrets.
|
||||
Assert.Null(siteResult.SecretsDeployed);
|
||||
Assert.NotNull(site.ArtifactRevisionHash);
|
||||
await _audit.DidNotReceive().LogAsync(
|
||||
Arg.Any<string>(), "DeploySiteSecrets", Arg.Any<string>(), Arg.Any<string>(),
|
||||
Arg.Any<string>(), Arg.Any<object?>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SecretDeploy_WritesOneAuditRowPerSite_WithoutMaterial()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user