Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/SqlErrorClassifierTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

119 lines
5.9 KiB
C#

using System.Data.Common;
namespace ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests;
/// <summary>
/// Unit tests for the transient-vs-permanent SQL error-number
/// classifier that <c>DatabaseGateway</c> uses to decide whether a failed
/// cached write should be buffered (transient) or returned to the script
/// synchronously / parked (permanent).
/// </summary>
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.
/// <summary>Verifies that a known transient error number is classified as transient.</summary>
/// <param name="errorNumber">The SQL error number.</param>
[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.
/// <summary>Verifies that a known permanent error number is not classified as transient.</summary>
/// <param name="errorNumber">The SQL error number.</param>
[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));
}
/// <summary>Verifies that an unknown error number defaults to permanent.</summary>
/// <param name="errorNumber">The SQL error number.</param>
[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));
}
// ── 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). ──
/// <summary>Provides exception instances of the transport types that count as transient.</summary>
/// <returns>Theory data with one exception instance per row.</returns>
public static IEnumerable<object[]> 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") };
}
/// <summary>Verifies that transport exception types are classified as transient.</summary>
/// <param name="ex">The exception to classify.</param>
[Theory]
[MemberData(nameof(TransientExceptionTypes))]
public void IsTransient_Exception_TrueForTransportTypes(Exception ex)
{
Assert.True(SqlErrorClassifier.IsTransient(ex));
}
/// <summary>Verifies that an unexpected exception type is not classified as transient.</summary>
[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()));
}
/// <summary>A concrete <see cref="DbException"/> that is not a SqlException, for the classifier unit test.</summary>
private sealed class NonSqlDbException : DbException
{
/// <summary>Initializes a database exception that is not a SQL exception.</summary>
/// <param name="message">The exception message.</param>
public NonSqlDbException(string message) : base(message) { }
}
}