using System.Net.Sockets; using MailKit; using MailKit.Net.Smtp; namespace ZB.MOM.WW.ScadaBridge.NotificationService.Tests; /// /// Tests for the shared SMTP error classification policy. This /// policy is correctness-relevant — it decides whether a delivery failure is /// retried (transient) or returned to the caller (permanent) — and is shared /// by the central outbox's EmailNotificationDeliveryAdapter, so it /// deserves direct coverage. /// public class SmtpErrorClassifierTests { /// Verifies that an SMTP 4xx command error is classified as transient. /// The SMTP status code. [Theory] [InlineData(421)] // service not available [InlineData(450)] // mailbox unavailable (busy) [InlineData(451)] // local error in processing [InlineData(452)] // insufficient system storage public void Classify_Smtp4xxCommand_IsTransient(int statusCode) { var ex = new SmtpCommandException( SmtpErrorCode.MessageNotAccepted, (SmtpStatusCode)statusCode, "rejected"); Assert.Equal(SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(ex, CancellationToken.None)); } /// Verifies that an SMTP 5xx command error is classified as permanent. /// The SMTP status code. [Theory] [InlineData(500)] // syntax error [InlineData(550)] // mailbox unavailable (rejected) [InlineData(553)] // mailbox name not allowed [InlineData(554)] // transaction failed public void Classify_Smtp5xxCommand_IsPermanent(int statusCode) { var ex = new SmtpCommandException( SmtpErrorCode.MessageNotAccepted, (SmtpStatusCode)statusCode, "rejected"); Assert.Equal(SmtpErrorClass.Permanent, SmtpErrorClassifier.Classify(ex, CancellationToken.None)); } /// Verifies that an SMTP command error with an unusual status code is classified as unknown. [Fact] public void Classify_SmtpCommandWithUnusualCode_IsUnknown() { // A status code outside the 4xx/5xx bands is not classifiable. var ex = new SmtpCommandException( SmtpErrorCode.UnexpectedStatusCode, (SmtpStatusCode)250, "ok-ish"); Assert.Equal(SmtpErrorClass.Unknown, SmtpErrorClassifier.Classify(ex, CancellationToken.None)); } /// Verifies that an SMTP protocol exception is classified as transient. [Fact] public void Classify_SmtpProtocolException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new SmtpProtocolException("protocol error"), CancellationToken.None)); } /// Verifies that a service-not-connected exception is classified as transient. [Fact] public void Classify_ServiceNotConnectedException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new ServiceNotConnectedException(), CancellationToken.None)); } /// Verifies that a socket exception is classified as transient. [Fact] public void Classify_SocketException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new SocketException(), CancellationToken.None)); } /// Verifies that a timeout exception is classified as transient. [Fact] public void Classify_TimeoutException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new TimeoutException(), CancellationToken.None)); } /// Verifies that a requested cancellation is classified as unknown. [Fact] public void Classify_RequestedCancellation_IsUnknown() { using var cts = new CancellationTokenSource(); cts.Cancel(); Assert.Equal( SmtpErrorClass.Unknown, SmtpErrorClassifier.Classify(new OperationCanceledException(), cts.Token)); } /// Verifies that an operation cancellation without a requested cancellation is classified as unknown. [Fact] public void Classify_OperationCanceledWithoutRequestedCancellation_IsUnknown() { // Not a recognised SMTP error, and cancellation was not requested. Assert.Equal( SmtpErrorClass.Unknown, SmtpErrorClassifier.Classify(new OperationCanceledException(), CancellationToken.None)); } /// Verifies that an unrecognised exception is classified as unknown. [Fact] public void Classify_UnrecognisedException_IsUnknown() { Assert.Equal( SmtpErrorClass.Unknown, SmtpErrorClassifier.Classify(new InvalidOperationException("bad credential triple"), CancellationToken.None)); } /// Verifies that the transient check matches the classification of the status code. /// The SMTP status code. /// Whether the status code is expected to be transient. [Theory] [InlineData(450, true)] [InlineData(550, false)] [InlineData(250, false)] public void IsTransient_MatchesClassification(int statusCode, bool expectedTransient) { var ex = new SmtpCommandException( SmtpErrorCode.MessageNotAccepted, (SmtpStatusCode)statusCode, "x"); Assert.Equal(expectedTransient, SmtpErrorClassifier.IsTransient(ex, CancellationToken.None)); } /// Verifies that a requested cancellation is not transient. [Fact] public void IsTransient_RequestedCancellation_IsFalse() { using var cts = new CancellationTokenSource(); cts.Cancel(); Assert.False(SmtpErrorClassifier.IsTransient(new OperationCanceledException(), cts.Token)); } }