namespace ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests; /// /// Offline pins for the migrator's retry classification. A /// cannot be constructed normally, so the error-number predicate is extracted as an internal static /// and pinned here by number; the concurrent-provision behaviour itself is exercised by the /// env-gated live suite. /// public sealed class SqlServerSecretsStoreMigratorTests { /// /// The provisioning-race outcomes the ScadaBridge central shared-store live gate (2026-08-07) /// proved reachable — or that the multi-statement DDL makes reachable — when N nodes provision /// one virgin database at once. Losing such a race means the object now exists, which is the /// desired end state, so a retry of the idempotent IF NOT EXISTS path must be taken. /// [Theory] [InlineData(1205)] // deadlock victim (pre-0.5.1 was the ONLY retried number) [InlineData(2714)] // "There is already an object named ..." — lost CREATE SCHEMA/TABLE (the live-gate defect) [InlineData(1913)] // "index ... already exists" — lost CREATE INDEX [InlineData(2627)] // PK violation — lost schema_version MERGE-insert public void Provisioning_race_error_numbers_are_retryable(int errorNumber) => Assert.True(SqlServerSecretsStoreMigrator.IsRetryableMigrationErrorNumber(errorNumber)); /// /// Errors that reproduce identically on retry must propagate so the node fails closed instead /// of spinning through the retry budget against a genuine misconfiguration. /// [Theory] [InlineData(2705)] // duplicate column — unreachable from the race (single CREATE TABLE statement) [InlineData(208)] // invalid object name [InlineData(229)] // permission denied [InlineData(2759)] // "CREATE SCHEMA failed due to previous errors" — retryable only via its companion 2714 [InlineData(4060)] // cannot open database [InlineData(18456)] // login failed [InlineData(0)] [InlineData(-2)] // client-side timeout public void Non_race_error_numbers_are_not_retryable(int errorNumber) => Assert.False(SqlServerSecretsStoreMigrator.IsRetryableMigrationErrorNumber(errorNumber)); /// The jittered back-off stays inside [window/2, window) and grows with the attempt. [Theory] [InlineData(1, 50, 100)] [InlineData(2, 100, 200)] [InlineData(4, 400, 800)] public void Backoff_is_bounded_by_the_exponential_window(int attempt, double minMs, double maxMs) { var random = new Random(12345); for (int i = 0; i < 1000; i++) { double ms = SqlServerSecretsStoreMigrator.ComputeBackoff(attempt, random).TotalMilliseconds; Assert.InRange(ms, minMs, maxMs); } } /// Different draws give different delays (the lockstep-retry defect was a constant schedule). [Fact] public void Backoff_is_jittered_not_constant() { var random = new Random(42); var delays = Enumerable.Range(0, 8) .Select(_ => SqlServerSecretsStoreMigrator.ComputeBackoff(3, random)) .Distinct() .ToList(); Assert.True(delays.Count > 1); } /// A fixed jitter source yields an exactly predictable delay. [Fact] public void Backoff_is_deterministic_for_a_seeded_source() { Assert.Equal( SqlServerSecretsStoreMigrator.ComputeBackoff(3, new Random(7)), SqlServerSecretsStoreMigrator.ComputeBackoff(3, new Random(7))); } }