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
@@ -52,6 +52,43 @@ public class MxGatewayDataConnectionSecretsTests
Assert.Contains("mxgateway/testconn/api-key", resolver.Requested);
}
[Theory]
[InlineData("secret:mxgw-key ")]
[InlineData(" SECRET: mxgw-key")]
[InlineData("Secret:mxgw-key")]
public async Task ConnectAsync_resolves_padded_or_case_variant_ref_to_the_name_the_scanner_ships(string apiKey)
{
// Final review M3: the adapter and central's SecretReferenceScanner share one parser,
// so the name looked up is exactly the name that was deployed.
var shipped = Assert.Single(ZB.MOM.WW.ScadaBridge.Commons.Types.Secrets.SecretReferenceScanner.Scan([
new ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts.DataConnectionArtifact(
"GW", "MxGateway", System.Text.Json.JsonSerializer.Serialize(new { ApiKey = apiKey }), null, 3),
])).Name;
var fake = new FakeMxGatewayClient();
var resolver = new FakeSecretResolver("resolved-plaintext-key");
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
await adapter.ConnectAsync(Details(apiKey));
Assert.Equal("mxgw-key", shipped);
Assert.Equal([shipped], resolver.Requested);
Assert.Equal("resolved-plaintext-key", fake.ConnectedWith!.ApiKey);
}
[Fact]
public async Task ConnectAsync_fails_closed_on_a_secret_ref_with_no_name()
{
var fake = new FakeMxGatewayClient();
var resolver = new FakeSecretResolver("SHOULD-NOT-BE-USED");
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
await Assert.ThrowsAsync<InvalidOperationException>(() => adapter.ConnectAsync(Details("secret: ")));
Assert.Null(fake.ConnectedWith);
Assert.Empty(resolver.Requested);
}
[Fact]
public async Task ConnectAsync_passes_literal_apikey_unchanged_and_does_not_call_resolver()
{