fix(secrets): Grpc hub client — per-row parse isolation (review Critical) + real-client CallInvoker tests
A single malformed row or manifest entry poisoned the whole sweep, permanently. WireToDomain.ToRows/ToManifest materialized eagerly, so one unparseable element threw out of the fetch callback, past SecretReplicationReconciler's per-batch handling, into SweepSafelyAsync's whole-sweep catch. The follower's pull set is derived from the hub's manifest, so the identical bad element arrived in the identical batch on every interval and nothing ever converged — logged as "sync failed", indistinguishable from a hub outage. Isolation now matches the Akka sibling's split exactly: WireToDomain stays a strict per-item parser and loses its two batch entry points (their absence is pinned by a reflection test), while GrpcSecretsHubClient converts each item inside its own try — the same shape as SecretReplicationActor.Materialize. A dropped item costs one warning naming the secret and a reason category, and every well-formed sibling still converges. Only parse-shaped exceptions are caught (ArgumentException, FormatException); a defect still propagates. A row whose own name is unparseable is logged under a marker rather than echoing the untrusted string, and no ciphertext, DEK or credential reaches a log line. GrpcSecretsHubClient had zero coverage — the existing fake implements the ISecretsHubReader seam directly, bypassing the header, the deadline and the conversion, which are the only things that class does. It is now driven for real through a fake CallInvoker handed to the generated client. The stale doc comment claiming the reconciler isolated the failure is replaced by one describing the mechanism that actually exists, and Endpoint gains a note that a plain-http hub sends the bearer token cleartext (doc only, no validation change). Tests 52 -> 67 in the package suite; solution green, no new warnings. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
+12
-7
@@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using Google.Protobuf;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
using ZB.MOM.WW.Secrets.Replicator.Grpc.V1;
|
||||
@@ -192,14 +193,18 @@ public sealed class WireToDomainTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_conversions_preserve_order()
|
||||
public void There_is_no_batch_entry_point_to_convert_a_whole_response_with()
|
||||
{
|
||||
SecretRow first = FullRow();
|
||||
SecretRow second = FullRow();
|
||||
second.Name = "api/key";
|
||||
// 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();
|
||||
|
||||
IReadOnlyList<StoredSecret> rows = WireToDomain.ToRows([first, second]);
|
||||
|
||||
Assert.Equal(["db/password", "api/key"], rows.Select(r => r.Name.Value));
|
||||
Assert.Equal(["ToEntry", "ToRow"], members);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user