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());
}
}