dd0a846b64
Secrets were per-node SQLite, so a secret written on one node was invisible to the rest of a cluster. G-7's design resolved the "shared SQL store vs Akka replicator" fork to build only the former; both are built here so the choice is a deployment decision (availability vs partition tolerance) rather than a library limitation. Two new packages — ZB.MOM.WW.Secrets.Replicator.SqlServer (shared store, plus a local-store-with-hub mode) and .Replicator.AkkaDotNet (peer-to-peer over distributed pub/sub). Core gains ISecretsStoreMigrator, one shared SecretLastWriterWins predicate so no two stores can disagree on a tie, the transport-agnostic reconciler, and ReplicatingSecretStore — which closes a real gap: nothing had ever called ISecretReplicator.PublishAsync, so the seam was inert and local writes would not have propagated at all. Verified 182 pass / 1 skip / 0 warnings, including 15 live tests against a real SQL Server 2022 (the SQLite suite ported case-for-case, so any behavioural divergence between the stores fails) and a 9-test in-process 2-node Akka cluster over real remoting. A post-build review caught six defects, all fixed and now covered: both replication modes could not resolve from the container (no test had built one), an unbounded fetch that broke past SQL Server's 2100-parameter cap, a poison row that aborted the rest of its batch forever, Enum.Parse on peer input that could restart the actor in a loop, null crypto blobs crossing the trust boundary, and a silently dropped pull-read failure. Packed at 0.2.0 and vulnerability-scanned clean; not yet published to the feed. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
52 lines
1.9 KiB
C#
52 lines
1.9 KiB
C#
using ZB.MOM.WW.Secrets.Abstractions;
|
|
using ZB.MOM.WW.Secrets.Replicator.SqlServer;
|
|
using ZB.MOM.WW.Secrets.Sqlite;
|
|
|
|
namespace ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests.Fakes;
|
|
|
|
/// <summary>
|
|
/// An <see cref="ISecretReplicationHub"/> backed by a real SQLite store, standing in for the shared
|
|
/// SQL-Server hub in offline convergence tests.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Deliberately backed by a real store rather than a dictionary: the behaviour under test is
|
|
/// last-writer-wins convergence, which lives in <c>ApplyReplicatedAsync</c>. A hand-rolled fake would
|
|
/// be re-implementing the very logic the tests exist to check, and would happily agree with a broken
|
|
/// reconciler. The live SQL-Server tests then confirm the T-SQL store behaves the same way.
|
|
/// </remarks>
|
|
/// <param name="store">The SQLite store standing in for the hub database.</param>
|
|
public sealed class SqliteBackedHub(SqliteSecretStore store) : ISecretReplicationHub
|
|
{
|
|
/// <summary>Number of times the sweep asked this hub for rows — asserts the fetch was skipped.</summary>
|
|
public int GetManyCallCount { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<SecretManifestEntry>> GetManifestAsync(CancellationToken ct) =>
|
|
store.GetManifestAsync(ct);
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<StoredSecret>> GetManyAsync(
|
|
IReadOnlyList<SecretName> names, CancellationToken ct)
|
|
{
|
|
GetManyCallCount++;
|
|
|
|
var rows = new List<StoredSecret>(names.Count);
|
|
|
|
foreach (SecretName name in names)
|
|
{
|
|
StoredSecret? row = await store.GetAsync(name, ct);
|
|
|
|
if (row is not null)
|
|
{
|
|
rows.Add(row);
|
|
}
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct) =>
|
|
store.ApplyReplicatedAsync(row, ct);
|
|
}
|