diff --git a/Api.SeaHavenIndustries.Tests/PasswordResetEmailSenderTracingTests.cs b/Api.SeaHavenIndustries.Tests/PasswordResetEmailSenderTracingTests.cs new file mode 100644 index 0000000..93c912a --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/PasswordResetEmailSenderTracingTests.cs @@ -0,0 +1,55 @@ +using Api.SeaHavenIndustries.HostedServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Sentry; +using SeaHaven.Services.Interfaces; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class PasswordResetEmailSenderTracingTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Each_send_finishes_its_transaction_as_ok_only_when_the_provider_accepts_it(bool accepted) + { + var transaction = new Mock(); + var hub = new Mock(); + hub.Setup(h => h.PushScope()).Returns(Mock.Of()); + hub.Setup(h => h.StartTransaction(It.IsAny(), It.IsAny>())) + .Returns(transaction.Object); + + var sender = new Mock(); + sender.Setup(s => s.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(accepted); + var services = new ServiceCollection().AddSingleton(sender.Object).BuildServiceProvider(); + var channel = new PasswordResetEmailChannel(NullLogger.Instance); + var worker = new PasswordResetEmailSenderHostedService( + channel, + services.GetRequiredService(), + NullLogger.Instance, + hub.Object); + + await worker.StartAsync(CancellationToken.None); + Assert.True(channel.TryEnqueue("user@example.com", "subject", "Your code is 123456")); + var deadline = DateTime.UtcNow.AddSeconds(10); + while (channel.Pending > 0 && DateTime.UtcNow < deadline) + await Task.Delay(10); + await worker.StopAsync(CancellationToken.None); + + Assert.Equal(0, channel.Pending); + if (accepted) + { + transaction.Verify(t => t.Finish(SpanStatus.Ok), Times.Once); + transaction.Verify(t => t.Finish(It.IsAny(), It.IsAny()), Times.Never); + } + else + { + transaction.Verify(t => t.Finish(SpanStatus.Ok), Times.Never); + transaction.Verify(t => t.Finish( + It.Is(ex => !ex.Message.Contains("user@example.com") && !ex.Message.Contains("123456")), + SpanStatus.InternalError), Times.Once); + } + } +} diff --git a/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs b/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs index da87fdd..172f678 100644 --- a/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs +++ b/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs @@ -91,8 +91,10 @@ namespace Api.SeaHavenIndustries.HostedServices $"{nameof(PasswordResetEmailSenderHostedService)}.{nameof(SendAsync)}"); try { - await SendAsync(email); - transaction.FinishOk(); + if (await SendAsync(email)) + transaction.FinishOk(); + else + transaction.FinishError(new InvalidOperationException("The mail provider did not accept the password reset email.")); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { @@ -112,12 +114,16 @@ namespace Api.SeaHavenIndustries.HostedServices } } - private async Task SendAsync(PasswordResetEmail email) + /// True when the mail provider accepted the email. + private async Task SendAsync(PasswordResetEmail email) { await using var scope = _scopeFactory.CreateAsyncScope(); var sender = scope.ServiceProvider.GetRequiredService(); - if (!await sender.SendEmailAsync(email.EmailTo, email.Subject, email.Body)) - _logger.LogWarning("Password reset email was not accepted by the mail provider."); + if (await sender.SendEmailAsync(email.EmailTo, email.Subject, email.Body)) + return true; + + _logger.LogWarning("Password reset email was not accepted by the mail provider."); + return false; } } }