From 74d5aa17eb1fa29d82bed03387642dff73f4edb4 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 20:01:58 -0300 Subject: [PATCH] fix(auth): trace a reset email the provider rejects as an error A send that the mail provider did not accept finished its background transaction as ok, so rejected reset emails looked delivered in tracing. It now finishes as an error with a fixed message that names no recipient. --- .../PasswordResetEmailSenderTracingTests.cs | 55 +++++++++++++++++++ .../PasswordResetEmailDelivery.cs | 16 ++++-- 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 Api.SeaHavenIndustries.Tests/PasswordResetEmailSenderTracingTests.cs 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; } } }