using System.Text; using MailKit.Security; namespace ScadaLink.NotificationService.Tests; /// /// NS-016: 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. /// NS-021: the OAuth2 (XOAUTH2) branch must carry a non-empty user identity /// (the SMTP From address) — an empty user is rejected by M365 with `535 5.7.3`. /// public class MailKitSmtpClientWrapperTests { [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", "")); } [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")); } [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")); } [Fact] public async Task Authenticate_OAuth2WithoutUserName_Throws() { // NS-021: passing an OAuth2 access token but no user identity (FromAddress) // used to construct `new SaslMechanismOAuth2("", credentials)`, which M365 // 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: "")); } [Fact] public void XOAuth2InitialResponse_CarriesUserAndBearer() { // NS-021 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 // M365 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); } }