using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc; /// /// Round-trip tests for the additive Secrets / SecretsApplied wire contract /// on / /// (proto field 9 / field 6). Site secrets design §3.3, /// docs/plans/2026-09-28-site-secrets-artifact-deploy-design.md. /// public class SiteCommandDtoMapperSecretsTests { private static SecretArtifact Sample(string name) => new(name, "text/plain", [1, 2, 3], [4], [5], [6, 7], [8], [9], "kek-A", 7, DateTimeOffset.Parse("2026-09-01T00:00:00Z"), DateTimeOffset.Parse("2026-09-02T00:00:00Z"), "d"); /// Verifies that deploy-artifacts secrets round-trip verbatim through the DTO mapper. [Fact] public void DeployArtifacts_Secrets_RoundTrip_Verbatim() { var cmd = new DeployArtifactsCommand("dep", null, null, null, null, null, null, DateTimeOffset.UtcNow, [Sample("A"), Sample("B")]); var back = (DeployArtifactsCommand)SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(cmd)); Assert.Equal(2, back.Secrets!.Count); Assert.Equal(cmd.Secrets![0].Ciphertext, back.Secrets[0].Ciphertext); Assert.Equal(cmd.Secrets[0].Nonce, back.Secrets[0].Nonce); Assert.Equal(cmd.Secrets[0].Tag, back.Secrets[0].Tag); Assert.Equal(cmd.Secrets[0].WrappedDek, back.Secrets[0].WrappedDek); Assert.Equal(cmd.Secrets[0].WrapNonce, back.Secrets[0].WrapNonce); Assert.Equal(cmd.Secrets[0].WrapTag, back.Secrets[0].WrapTag); Assert.Equal(cmd.Secrets[0].ContentType, back.Secrets[0].ContentType); Assert.Equal(cmd.Secrets[0].CreatedUtc, back.Secrets[0].CreatedUtc); Assert.Equal(cmd.Secrets[0].UpdatedUtc, back.Secrets[0].UpdatedUtc); Assert.Equal(cmd.Secrets[0].Description, back.Secrets[0].Description); Assert.Equal("kek-A", back.Secrets[1].KekId); Assert.Equal(7, back.Secrets[1].Revision); } /// Verifies that null deploy-artifacts secrets stay null through the mapper. [Fact] public void DeployArtifacts_NullSecrets_StaysNull() { var cmd = new DeployArtifactsCommand("dep", null, null, null, null, null, null, DateTimeOffset.UtcNow); var dto = SiteCommandDtoMapper.ToProto(cmd); Assert.Null(dto.Secrets); Assert.Null(((DeployArtifactsCommand)SiteCommandDtoMapper.FromProto(dto)).Secrets); } /// Verifies that empty deploy-artifacts secrets stay empty rather than becoming null. [Fact] public void DeployArtifacts_EmptySecrets_StaysEmpty_NotNull() { var cmd = new DeployArtifactsCommand("dep", null, null, null, null, null, null, DateTimeOffset.UtcNow, []); var back = (DeployArtifactsCommand)SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(cmd)); Assert.NotNull(back.Secrets); Assert.Empty(back.Secrets!); } /// Verifies that a command DTO that lacks the secrets field maps to null secrets. [Fact] public void DeployArtifacts_CommandDtoWithoutField9_MapsToNullSecrets() { // Simulates an OLD CENTRAL that never set field 9 — the DTO here carries no // Secrets wrapper at all, exactly as a pre-upgrade sender's wire bytes would // decode. FromProto must treat that absence as null, not an empty list. var dto = new DeployArtifactsCommandDto { DeploymentId = "dep", Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow) }; Assert.Null(dto.Secrets); var back = SiteCommandDtoMapper.FromProto(dto); Assert.Null(back.Secrets); } /// Verifies that the applied-secrets count of an artifact response round-trips through the mapper. /// The applied-secrets count, or when the site did not report one. [Theory] [InlineData(null)] [InlineData(3)] public void ArtifactResponse_SecretsApplied_RoundTrips(int? applied) { var r = new ArtifactDeploymentResponse("dep", "site-a", true, null, DateTimeOffset.UtcNow, applied); Assert.Equal(applied, SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(r)).SecretsApplied); } /// Verifies that the removed-secrets list of an artifact response round-trips through the mapper. [Fact] public void ArtifactResponse_SecretsRemoved_RoundTrips() { var r = new ArtifactDeploymentResponse( "dep", "site-a", true, null, DateTimeOffset.UtcNow, 0, ["old-1", "old-2"]); Assert.Equal(["old-1", "old-2"], SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(r)).SecretsRemoved!); } /// Verifies that an artifact response with no removed secrets is null on both sides of the mapper. [Fact] public void ArtifactResponse_NoSecretsRemoved_IsNullOnBothSides() { // Absent on the wire (an old site, or nothing removed) must read back as null. var none = new ArtifactDeploymentResponse("dep", "site-a", true, null, DateTimeOffset.UtcNow, 1); Assert.Null(SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(none)).SecretsRemoved); var empty = none with { SecretsRemoved = [] }; Assert.Null(SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(empty)).SecretsRemoved); } }