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