using System.Net.Sockets; using MailKit; using MailKit.Net.Smtp; namespace ScadaLink.NotificationService.Tests; /// /// NS-002/NS-003: 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 /// between and the central outbox's /// EmailNotificationDeliveryAdapter, so it deserves direct coverage. /// public class SmtpErrorClassifierTests { [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)); } [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)); } [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)); } [Fact] public void Classify_SmtpProtocolException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new SmtpProtocolException("protocol error"), CancellationToken.None)); } [Fact] public void Classify_ServiceNotConnectedException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new ServiceNotConnectedException(), CancellationToken.None)); } [Fact] public void Classify_SocketException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new SocketException(), CancellationToken.None)); } [Fact] public void Classify_TimeoutException_IsTransient() { Assert.Equal( SmtpErrorClass.Transient, SmtpErrorClassifier.Classify(new TimeoutException(), CancellationToken.None)); } [Fact] public void Classify_RequestedCancellation_IsUnknown() { using var cts = new CancellationTokenSource(); cts.Cancel(); Assert.Equal( SmtpErrorClass.Unknown, SmtpErrorClassifier.Classify(new OperationCanceledException(), cts.Token)); } [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)); } [Fact] public void Classify_UnrecognisedException_IsUnknown() { Assert.Equal( SmtpErrorClass.Unknown, SmtpErrorClassifier.Classify(new InvalidOperationException("bad credential triple"), CancellationToken.None)); } [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)); } [Fact] public void IsTransient_RequestedCancellation_IsFalse() { using var cts = new CancellationTokenSource(); cts.Cancel(); Assert.False(SmtpErrorClassifier.IsTransient(new OperationCanceledException(), cts.Token)); } }