using System.Text; using MailKit.Security; namespace ZB.MOM.WW.ScadaBridge.NotificationService.Tests; /// /// must never /// silently skip authentication for a misconfigured SMTP config — a missing /// credential, an unrecognised auth type, or an unparseable Basic credential /// must be a hard, surfaced error rather than an unauthenticated send. /// The OAuth2 (XOAUTH2) branch must carry a non-empty user identity /// (the SMTP From address) — an empty user is rejected by Microsoft 365 with `535 5.7.3`. /// public class MailKitSmtpClientWrapperTests { /// Verifies that authenticating with empty credentials throws. /// A task that represents the asynchronous operation. [Fact] public async Task Authenticate_EmptyCredentials_Throws() { // An AuthType of "basic"/"oauth2" with a null/empty Credentials value is a // misconfigured row; the wrapper used to "return" and send unauthenticated. var wrapper = new MailKitSmtpClientWrapper(); await Assert.ThrowsAsync( () => wrapper.AuthenticateAsync("basic", null)); await Assert.ThrowsAsync( () => wrapper.AuthenticateAsync("oauth2", "")); } /// Verifies that authenticating with an unknown auth type throws. /// A task that represents the asynchronous operation. [Fact] public async Task Authenticate_UnknownAuthType_Throws() { // The switch had cases only for "basic"/"oauth2" and no default — any other // value (typo, future "ntlm") fell through and sent unauthenticated. var wrapper = new MailKitSmtpClientWrapper(); await Assert.ThrowsAsync( () => wrapper.AuthenticateAsync("ntlm", "user:pass")); } /// Verifies that a basic credential without a colon separator throws. /// A task that represents the asynchronous operation. [Fact] public async Task Authenticate_BasicCredentialWithoutColon_Throws() { // A "basic" credential string that does not split into exactly two parts was // silently skipped — the connection then sent unauthenticated. var wrapper = new MailKitSmtpClientWrapper(); await Assert.ThrowsAsync( () => wrapper.AuthenticateAsync("basic", "nocolon")); } /// Verifies that OAuth2 authentication without a user name throws. /// A task that represents the asynchronous operation. [Fact] public async Task Authenticate_OAuth2WithoutUserName_Throws() { // Passing an OAuth2 access token but no user identity (FromAddress) // used to construct `new SaslMechanismOAuth2("", credentials)`, which Microsoft 365 // rejects with `535 5.7.3`. The wrapper now refuses upfront so the caller // sees a clean configuration error rather than a confusing server reject. var wrapper = new MailKitSmtpClientWrapper(); await Assert.ThrowsAsync( () => wrapper.AuthenticateAsync("oauth2", "access-token", oauth2UserName: null)); await Assert.ThrowsAsync( () => wrapper.AuthenticateAsync("oauth2", "access-token", oauth2UserName: "")); } /// Verifies that the XOAUTH2 initial response carries both the user and the bearer token. [Fact] public void XOAuth2InitialResponse_CarriesUserAndBearer() { // Regression guard: independent of the wrapper, prove that MailKit's // SaslMechanismOAuth2 puts `user=` into the initial-response bytes // — i.e. wiring the wrapper to pass `FromAddress` is sufficient to fix the // Microsoft 365 handshake. If MailKit ever changes the framing this test will catch it. var sasl = new SaslMechanismOAuth2("noreply@example.com", "tok-xyz"); var initial = sasl.Challenge(string.Empty); var asString = Encoding.UTF8.GetString(Convert.FromBase64String(initial)); Assert.Contains("user=noreply@example.com", asString); Assert.Contains("auth=Bearer tok-xyz", asString); } }