using System.Reflection;
using Google.Protobuf;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replicator.Grpc.V1;
using ZB.MOM.WW.Secrets.Replicator.Grpc.Wire;
namespace ZB.MOM.WW.Secrets.Replicator.Grpc.Tests;
///
/// The proto -> domain boundary: a follower rebuilding rows a hub sent it. These are the checks
/// protobuf itself cannot make — a name that normalizes and validates, a content type that names a
/// real member, and timestamps whose exact representation survives, because last-writer-wins
/// compares them.
///
public sealed class WireToDomainTests
{
private static SecretRow FullRow() => new()
{
Name = "db/password",
Description = "primary db",
ContentType = nameof(SecretContentType.ConnectionString),
Ciphertext = ByteString.CopyFrom(0xAA, 0xBB),
Nonce = ByteString.CopyFrom(1, 2, 3),
Tag = ByteString.CopyFrom(4, 5),
WrappedDek = ByteString.CopyFrom(6),
WrapNonce = ByteString.CopyFrom(7),
WrapTag = ByteString.CopyFrom(8),
KekId = "sha256:abc",
Revision = 7,
IsDeleted = false,
CreatedUtc = new DateTimeOffset(2026, 8, 7, 1, 2, 3, TimeSpan.Zero).ToString("O"),
UpdatedUtc = new DateTimeOffset(2026, 8, 7, 4, 5, 6, TimeSpan.Zero).ToString("O"),
CreatedBy = "alice",
UpdatedBy = "bob",
};
[Fact]
public void Row_maps_every_field_onto_the_domain_type()
{
StoredSecret row = WireToDomain.ToRow(FullRow());
Assert.Equal("db/password", row.Name.Value);
Assert.Equal("primary db", row.Description);
Assert.Equal(SecretContentType.ConnectionString, row.ContentType);
Assert.Equal(new byte[] { 0xAA, 0xBB }, row.Ciphertext);
Assert.Equal(new byte[] { 1, 2, 3 }, row.Nonce);
Assert.Equal(new byte[] { 4, 5 }, row.Tag);
Assert.Equal(new byte[] { 6 }, row.WrappedDek);
Assert.Equal(new byte[] { 7 }, row.WrapNonce);
Assert.Equal(new byte[] { 8 }, row.WrapTag);
Assert.Equal("sha256:abc", row.KekId);
Assert.Equal(7, row.Revision);
Assert.False(row.IsDeleted);
Assert.Null(row.DeletedUtc);
Assert.Equal("alice", row.CreatedBy);
Assert.Equal("bob", row.UpdatedBy);
}
[Fact]
public void Row_rejects_an_unrecognized_content_type()
{
// The reason the wire carries a NAME rather than a proto enum: an unknown value is rejected
// at the boundary instead of silently decoding to the zero value and retyping the secret.
SecretRow wire = FullRow();
wire.ContentType = "Kerberos";
ArgumentException ex = Assert.Throws(() => WireToDomain.ToRow(wire));
Assert.Contains("Kerberos", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void Row_rejects_a_numeric_content_type_that_names_no_member()
{
SecretRow wire = FullRow();
wire.ContentType = "9999";
Assert.Throws(() => WireToDomain.ToRow(wire));
}
[Fact]
public void Row_rejects_a_name_the_domain_would_not_accept()
{
SecretRow wire = FullRow();
wire.ContentType = nameof(SecretContentType.Text);
wire.Name = "../../etc/passwd";
Assert.Throws(() => WireToDomain.ToRow(wire));
}
[Fact]
public void Row_distinguishes_an_absent_description_from_an_empty_one()
{
SecretRow absent = FullRow();
absent.ClearDescription();
Assert.Null(WireToDomain.ToRow(absent).Description);
SecretRow empty = FullRow();
empty.Description = string.Empty;
Assert.Equal(string.Empty, WireToDomain.ToRow(empty).Description);
}
[Fact]
public void Row_distinguishes_absent_from_empty_for_every_nullable_field()
{
SecretRow absent = FullRow();
absent.ClearCreatedBy();
absent.ClearUpdatedBy();
StoredSecret row = WireToDomain.ToRow(absent);
Assert.Null(row.CreatedBy);
Assert.Null(row.UpdatedBy);
}
[Fact]
public void Row_round_trips_a_non_utc_offset_exactly()
{
// The whole reason the contract uses "O" strings and not google.protobuf.Timestamp: an
// offset-bearing DateTimeOffset must come back byte-identical, because LWW compares it.
var updated = new DateTimeOffset(2026, 8, 7, 4, 5, 6, 789, TimeSpan.FromHours(-5));
SecretRow wire = FullRow();
wire.UpdatedUtc = updated.ToString("O");
StoredSecret row = WireToDomain.ToRow(wire);
Assert.Equal(updated, row.UpdatedUtc);
Assert.Equal(updated.Offset, row.UpdatedUtc.Offset);
Assert.Equal(updated.ToString("O"), row.UpdatedUtc.ToString("O"));
}
[Fact]
public void Row_maps_a_tombstone_with_its_deletion_timestamp()
{
var deleted = new DateTimeOffset(2026, 8, 7, 9, 9, 9, TimeSpan.Zero);
SecretRow wire = FullRow();
wire.IsDeleted = true;
wire.DeletedUtc = deleted.ToString("O");
StoredSecret row = WireToDomain.ToRow(wire);
Assert.True(row.IsDeleted);
Assert.Equal(deleted, row.DeletedUtc);
}
[Fact]
public void Row_leaves_the_deletion_timestamp_null_when_the_field_is_absent()
{
SecretRow wire = FullRow();
wire.ClearDeletedUtc();
Assert.Null(WireToDomain.ToRow(wire).DeletedUtc);
}
[Fact]
public void Row_rejects_a_malformed_timestamp()
{
SecretRow wire = FullRow();
wire.UpdatedUtc = "yesterday";
Assert.Throws(() => WireToDomain.ToRow(wire));
}
[Fact]
public void Manifest_entry_maps_and_validates_its_name()
{
var updated = new DateTimeOffset(2026, 8, 7, 4, 5, 6, TimeSpan.FromHours(2));
SecretManifestEntry entry = WireToDomain.ToEntry(new ManifestEntry
{
Name = "api/key",
Revision = 3,
UpdatedUtc = updated.ToString("O"),
IsDeleted = true,
});
Assert.Equal("api/key", entry.Name.Value);
Assert.Equal(3, entry.Revision);
Assert.Equal(updated, entry.UpdatedUtc);
Assert.True(entry.IsDeleted);
}
[Fact]
public void Manifest_entry_rejects_a_name_the_domain_would_not_accept()
{
Assert.Throws(() => WireToDomain.ToEntry(new ManifestEntry
{
Name = "/rooted",
Revision = 1,
UpdatedUtc = DateTimeOffset.UtcNow.ToString("O"),
}));
}
[Fact]
public void There_is_no_batch_entry_point_to_convert_a_whole_response_with()
{
// A batch method here would fail a whole response on one bad element, and since the pull set
// is derived from the hub's manifest the identical bad element would re-fail the identical
// sweep forever. Isolation belongs to GrpcSecretsHubClient, which converts item by item, so
// this pins that the strict parser offers nothing that could poison a batch again.
string[] members = typeof(WireToDomain)
.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Select(m => m.Name)
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Equal(["ToEntry", "ToRow"], members);
}
}