fix(site-secrets): one shared secret: reference parser for scanner and MxGateway adapter

Final review M3: SecretReferenceScanner.TryParseSecretReference (trimmed, case-insensitive
prefix, trimmed name, empty = not a reference, ${secret:} never matches) is used by the scanner
and by MxGatewayDataConnection's ApiKey resolution, so a padded or case-variant reference
resolves the name central ships. A bare 'secret:' ApiKey now fails closed. M7: unit test pins
stamped hash == ComputeCurrentArtifactHashesAsync with a non-empty Secrets list.
This commit is contained in:
Joseph Doherty
2026-09-28 21:08:34 -04:00
parent b1b49bb36c
commit 67a3950f74
5 changed files with 149 additions and 7 deletions
@@ -470,6 +470,46 @@ public class ArtifactDeploymentServiceTests : TestKit
}
}
[Fact]
public async Task ComputeCurrentArtifactHashesAsync_MatchesTheHashADeployStamps_WithSecrets()
{
// Final review M7: 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.
var site = new Site("Site One", "site-1") { Id = 1 };
var sites = new List<Site> { site };
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
_siteRepo.GetDataConnectionsBySiteIdAsync(1, Arg.Any<CancellationToken>())
.Returns(new List<DataConnection>
{
new("GW1", "MxGateway", 1)
{
PrimaryConfiguration = """{"ApiKey":"secret:key1","Password":"secret:key2"}"""
}
});
_secretStore.GetAsync(Arg.Is<SecretName>(n => n.Value == "key1"), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<StoredSecret?>(SecretRow("key1", 3)));
_secretStore.GetAsync(Arg.Is<SecretName>(n => n.Value == "key2"), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<StoredSecret?>(SecretRow("key2", 7)));
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new SecretsAckProbeActor(
recorder, cmd => cmd.Secrets == null ? (int?)null : cmd.Secrets.Count)));
var service = CreateServiceWithCommActor(probe);
var deployResult = await service.DeployToAllSitesAsync("admin");
Assert.True(deployResult.IsSuccess);
Assert.Equal(1, deployResult.Value.SuccessCount);
var shipped = Assert.Single(recorder.Received);
Assert.Equal(2, shipped.Secrets!.Count);
Assert.NotNull(site.ArtifactRevisionHash);
var current = await service.ComputeCurrentArtifactHashesAsync(sites);
Assert.Equal(site.ArtifactRevisionHash, current[site.Id]);
}
[Fact]
public async Task ComputeCurrentArtifactHashesAsync_HoistsTheGlobalQueriesOnce()
{