using System.Net; using Api.SeaHavenIndustries.Helper; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moq; using SendGrid; using SendGrid.Helpers.Mail; using Xunit; namespace Api.SeaHavenIndustries.Tests; public sealed class SendMessageTests { private const string Recipient = "taylor@example.com"; private const string Subject = "You're invited to Seahaven"; private const string RejectionBody = "{\"errors\":[{\"message\":\"taylor@example.com does not exist\"}]}"; [Theory] [InlineData(HttpStatusCode.OK, true)] [InlineData(HttpStatusCode.Accepted, true)] [InlineData(HttpStatusCode.BadRequest, false)] [InlineData(HttpStatusCode.Unauthorized, false)] [InlineData(HttpStatusCode.TooManyRequests, false)] [InlineData(HttpStatusCode.InternalServerError, false)] public async Task EverySendPath_ReportsDeliveryOnlyForASuccessStatus(HttpStatusCode status, bool delivered) { var sender = new StubbedSendMessage(status, new CapturingLogger()); Assert.Equal(delivered, await sender.SendEMail(Recipient, Subject, "

Hi

")); Assert.Equal(delivered, await sender.SendEmailAsync(Recipient, Subject, "

Hi

")); Assert.Equal(delivered, await sender.SendEMailAttachment(Recipient, Subject, new byte[] { 1, 2, 3 })); Assert.Equal(delivered, await sender.SendDispatchEmail(Recipient, Subject, "

Hi

", null)); } [Fact] public async Task ARejectedSend_LogsOnlyTheStatusCode() { var logger = new CapturingLogger(); var sender = new StubbedSendMessage(HttpStatusCode.BadRequest, logger); Assert.False(await sender.SendEMail(Recipient, Subject, "

secret link

")); var entry = Assert.Single(logger.Entries); Assert.Contains("400", entry); Assert.DoesNotContain(Recipient, entry); Assert.DoesNotContain(Subject, entry); Assert.DoesNotContain("secret link", entry); Assert.DoesNotContain("errors", entry); } [Fact] public async Task AFailedSend_LogsOnlyTheExceptionType() { var logger = new CapturingLogger(); var sender = new StubbedSendMessage( new HttpRequestException($"could not reach SendGrid for {Recipient}"), logger); Assert.False(await sender.SendEMail(Recipient, Subject, "

Hi

")); var entry = Assert.Single(logger.Entries); Assert.Contains(nameof(HttpRequestException), entry); Assert.DoesNotContain(Recipient, entry); } private sealed class StubbedSendMessage : SendMessage { private readonly Mock _client = new(); public StubbedSendMessage(HttpStatusCode status, ILogger logger) : base(new ConfigurationBuilder().Build(), logger) { _client .Setup(client => client.SendEmailAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(() => new Response(status, new StringContent(RejectionBody), null)); } public StubbedSendMessage(Exception failure, ILogger logger) : base(new ConfigurationBuilder().Build(), logger) { _client .Setup(client => client.SendEmailAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(failure); } protected override ISendGridClient CreateClient(string? apiKey) => _client.Object; } private sealed class CapturingLogger : ILogger { public List Entries { get; } = new(); public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => Entries.Add($"{formatter(state, exception)} {exception}"); } }