using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts; using ZB.MOM.WW.Secrets.Abstractions; namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests; /// /// is the central half of design §3.1/§3.3: derive which /// secrets a site's data connections reference, resolve each against the central /// , and report the ones that could not be resolved. It never sees /// or produces plaintext — everything here is ciphertext bytes. /// public class SiteSecretsCollectorTests { private static DataConnectionArtifact Dc(string name, string? primary, string? backup = null) => new(name, "MxGateway", primary, backup, 3); private static StoredSecret Row(string name, long revision, bool deleted = false, string? description = null) => new() { Name = new SecretName(name), Description = description, ContentType = SecretContentType.Text, Ciphertext = [1, 2, 3], Nonce = [4, 5], Tag = [6, 7], WrappedDek = [8, 9], WrapNonce = [10, 11], WrapTag = [12, 13], KekId = "kek-1", Revision = revision, IsDeleted = deleted, CreatedUtc = DateTimeOffset.Parse("2026-01-01T00:00:00Z"), UpdatedUtc = DateTimeOffset.Parse("2026-01-02T00:00:00Z"), }; [Fact] public async Task Collect_ReturnsOnlyReferencedRows_AndNamesMissing() { var store = new FakeSecretStore(Row("a", 3), Row("b", 1, deleted: true), Row("other", 1)); var result = await new SiteSecretsCollector(store).CollectAsync( [Dc("GW1", """{"ApiKey":"secret:a"}"""), Dc("GW2", """{"ApiKey":"secret:b"}""")], default); // Only the referenced names come back — "other" is in the store but never referenced. Assert.Equal(["a"], result.Secrets.Select(s => s.Name)); Assert.Equal(3, result.Secrets[0].Revision); // "b" is tombstoned (IsDeleted) — treated the same as absent. Assert.Equal(["b"], result.Missing.Select(m => m.Name)); Assert.Equal(["GW2"], result.Missing[0].ReferencedBy); } [Fact] public async Task Collect_ResolvedRow_MapsEveryFieldFromTheStoredSecretVerbatim() { var row = Row("mxgw-key", revision: 7, description: "MxGateway API key"); var store = new FakeSecretStore(row); var result = await new SiteSecretsCollector(store).CollectAsync( [Dc("GW1", """{"ApiKey":"secret:mxgw-key"}""")], default); var artifact = Assert.Single(result.Secrets); Assert.Empty(result.Missing); Assert.Equal(row.Name.Value, artifact.Name); Assert.Equal(row.ContentType.ToString(), artifact.ContentType); Assert.Equal(row.Ciphertext, artifact.Ciphertext); Assert.Equal(row.Nonce, artifact.Nonce); Assert.Equal(row.Tag, artifact.Tag); Assert.Equal(row.WrappedDek, artifact.WrappedDek); Assert.Equal(row.WrapNonce, artifact.WrapNonce); Assert.Equal(row.WrapTag, artifact.WrapTag); Assert.Equal(row.KekId, artifact.KekId); Assert.Equal(row.Revision, artifact.Revision); Assert.Equal(row.CreatedUtc, artifact.CreatedUtc); Assert.Equal(row.UpdatedUtc, artifact.UpdatedUtc); Assert.Equal(row.Description, artifact.Description); } [Fact] public async Task Collect_AbsentName_IsReportedMissing_NotThrown() { var store = new FakeSecretStore(); var result = await new SiteSecretsCollector(store).CollectAsync( [Dc("GW1", """{"ApiKey":"secret:never-created"}""")], default); Assert.Empty(result.Secrets); var missing = Assert.Single(result.Missing); Assert.Equal("never-created", missing.Name); Assert.Equal(["GW1"], missing.ReferencedBy); } [Fact] public async Task Collect_NoSecretReferences_ReturnsEmptyWithoutQueryingTheStore() { var store = new FakeSecretStore(Row("unused", 1)); var result = await new SiteSecretsCollector(store).CollectAsync( [Dc("GW1", """{"Endpoint":"http://gw:5000"}""")], default); Assert.Empty(result.Secrets); Assert.Empty(result.Missing); } /// /// In-memory stand-in. Implements only — /// the sole member calls; every other member throws so an /// accidental call (a write, a list, a replication apply) fails the test loudly instead of /// silently no-op'ing. /// private sealed class FakeSecretStore : ISecretStore { private readonly Dictionary _rows; public FakeSecretStore(params StoredSecret[] rows) => _rows = rows.ToDictionary(r => r.Name); public Task GetAsync(SecretName name, CancellationToken ct) => Task.FromResult(_rows.TryGetValue(name, out var row) ? row : null); public Task UpsertAsync(StoredSecret row, CancellationToken ct) => throw new NotSupportedException(); public Task DeleteAsync(SecretName name, string? actor, CancellationToken ct) => throw new NotSupportedException(); public Task> ListAsync(bool includeDeleted, CancellationToken ct) => throw new NotSupportedException(); public Task> GetManifestAsync(CancellationToken ct) => throw new NotSupportedException(); public Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct) => throw new NotSupportedException(); public Task ApplyRewrapAsync( StoredSecret rewrappedRow, byte[] expectedCurrentWrappedDek, CancellationToken ct) => throw new NotSupportedException(); } }