using System.Data.Common; namespace ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests; /// /// M2.3 (#7): unit tests for the transient-vs-permanent SQL error-number /// classifier that DatabaseGateway uses to decide whether a failed /// cached write should be buffered (transient) or returned to the script /// synchronously / parked (permanent). /// public class SqlErrorClassifierTests { // The full transient set documented on SqlErrorClassifier — connection, // timeout, deadlock, and Azure throttle error numbers. A retry can plausibly // succeed for any of these, so they are buffered to store-and-forward. [Theory] [InlineData(-2)] // timeout expired [InlineData(-1)] // connection error [InlineData(2)] // network / instance not found [InlineData(53)] // network path not found [InlineData(64)] // connection terminated mid-session [InlineData(233)] // no process on the other end of the pipe [InlineData(1205)] // deadlock victim [InlineData(10053)] // transport-level abort [InlineData(10054)] // connection reset by peer [InlineData(10060)] // connection timed out [InlineData(40197)] // Azure SQL service error, retry [InlineData(40501)] // Azure SQL service busy [InlineData(40613)] // Azure SQL database unavailable [InlineData(49918)] // Azure SQL cannot process request (throttle) [InlineData(49919)] // Azure SQL too many create/update operations [InlineData(49920)] // Azure SQL too many operations (throttle) public void IsTransient_KnownTransientNumber_ReturnsTrue(int errorNumber) { Assert.True(SqlErrorClassifier.IsTransient(errorNumber)); } // Constraint, syntax, and permission errors are permanent — retrying the // identical statement cannot succeed and may cause duplicate side effects. [Theory] [InlineData(547)] // constraint violation (FK/CHECK) [InlineData(2627)] // primary-key / unique constraint violation [InlineData(2601)] // duplicate key in a unique index [InlineData(102)] // incorrect syntax [InlineData(156)] // incorrect syntax near a keyword [InlineData(207)] // invalid column name [InlineData(208)] // invalid object name [InlineData(229)] // permission denied on object [InlineData(230)] // permission denied on column [InlineData(262)] // permission denied (CREATE etc.) public void IsTransient_KnownPermanentNumber_ReturnsFalse(int errorNumber) { Assert.False(SqlErrorClassifier.IsTransient(errorNumber)); } [Theory] [InlineData(0)] // no error number captured [InlineData(99999)] // unknown / undocumented number [InlineData(12345)] [InlineData(int.MaxValue)] public void IsTransient_UnknownNumber_DefaultsToPermanent(int errorNumber) { // Fail-fast is the safer default: an unrecognised error number must NOT // be silently retried forever. Unknown => permanent => false. Assert.False(SqlErrorClassifier.IsTransient(errorNumber)); } // ── M2.3 (#7) code-review fix: IsTransient(Exception) — a live DB outage does // not always surface as a SqlException. Transport/connection/timeout/driver // exception types are transient (buffer+retry), mirroring the HTTP path's // ErrorClassifier.IsTransient(Exception). ── public static IEnumerable TransientExceptionTypes() { yield return new object[] { new InvalidOperationException("connection not open") }; yield return new object[] { new System.IO.IOException("transport reset") }; yield return new object[] { new System.Net.Sockets.SocketException(10060) }; yield return new object[] { new TimeoutException("timed out") }; yield return new object[] { new TaskCanceledException("driver-level cancellation") }; // Any DbException that is NOT a SqlException is a driver/transport error. yield return new object[] { new NonSqlDbException("provider transport error") }; } [Theory] [MemberData(nameof(TransientExceptionTypes))] public void IsTransient_Exception_TrueForTransportTypes(Exception ex) { Assert.True(SqlErrorClassifier.IsTransient(ex)); } [Fact] public void IsTransient_Exception_FalseForUnexpectedType() { // Authoring bugs are NOT a DB outage — they must propagate, exactly as the // HTTP path lets genuinely-unexpected exceptions escape its IsTransient filter. Assert.False(SqlErrorClassifier.IsTransient(new ArgumentException("authoring bug"))); Assert.False(SqlErrorClassifier.IsTransient(new NullReferenceException())); } /// A concrete that is not a SqlException, for the classifier unit test. private sealed class NonSqlDbException : DbException { public NonSqlDbException(string message) : base(message) { } } }