using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Protocol; namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests; /// /// The DTO is the trust boundary for anything a peer sends. Every rejection here must surface as an /// , because that is the exception type the receiving actor filters /// on — anything else escapes its handler, restarts the actor, and (since the row is redelivered) /// loops. /// public sealed class HostileWireInputTests { private static SecretRowDto Valid() => new() { Name = "app/ok", ContentType = "Text", Ciphertext = [1], Nonce = [2], Tag = [3], WrappedDek = [4], WrapNonce = [5], WrapTag = [6], KekId = "sha256:x", Revision = 0, CreatedUtc = "2026-01-01T00:00:00.0000000+00:00", UpdatedUtc = "2026-01-01T00:00:00.0000000+00:00", }; [Fact] public void A_valid_row_materializes() { StoredSecret row = Valid().ToStoredSecret(); Assert.Equal("app/ok", row.Name.Value); Assert.Equal(SecretContentType.Text, row.ContentType); } [Theory] [InlineData("../../etc/passwd")] [InlineData("/rooted")] [InlineData("has space")] [InlineData("")] public void A_malformed_name_is_rejected(string name) { Assert.Throws(() => (Valid() with { Name = name }).ToStoredSecret()); } [Theory] // An in-range number names no member — Enum.Parse would happily produce (SecretContentType)4096 // and the store would persist it for later readers that assume a defined value. [InlineData("4096")] // Overflows the underlying type: Enum.Parse throws OverflowException, which is NOT an // ArgumentException, so it would escape the actor's filter and restart it. [InlineData("99999999999999999999")] [InlineData("NotAContentType")] [InlineData("")] public void An_unrecognized_content_type_is_rejected_as_an_ArgumentException(string contentType) { Assert.Throws( () => (Valid() with { ContentType = contentType }).ToStoredSecret()); } [Fact] public void A_null_crypto_blob_is_rejected_at_the_boundary() { // `required byte[]` means "present in the payload", not "non-null" — an explicit JSON null // satisfies it. Caught here, or it blows up deep in the store's parameter binding instead. Assert.Throws( () => (Valid() with { Ciphertext = null! }).ToStoredSecret()); Assert.Throws( () => (Valid() with { WrappedDek = null! }).ToStoredSecret()); } [Fact] public void A_malformed_timestamp_is_rejected() { Assert.ThrowsAny( () => (Valid() with { UpdatedUtc = "not-a-date" }).ToStoredSecret()); } }