diff --git a/Api.SeaHavenIndustries.Tests/PasswordResetEmailChannelTests.cs b/Api.SeaHavenIndustries.Tests/PasswordResetEmailChannelTests.cs new file mode 100644 index 0000000..5366173 --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/PasswordResetEmailChannelTests.cs @@ -0,0 +1,43 @@ +using Api.SeaHavenIndustries.HostedServices; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class PasswordResetEmailChannelTests +{ + [Fact] + public void A_full_queue_refuses_the_email_instead_of_dropping_it_silently() + { + var channel = new PasswordResetEmailChannel(NullLogger.Instance); + for (var i = 0; i < PasswordResetEmailChannel.Capacity; i++) + channel.TryEnqueue($"user{i}@example.com", "subject", "body").Should().BeTrue(); + + var accepted = channel.TryEnqueue("late@example.com", "subject", "body"); + + accepted.Should().BeFalse(); + channel.Pending.Should().Be(PasswordResetEmailChannel.Capacity); + } + + [Fact] + public async Task A_refused_email_is_never_delivered_and_the_queue_accepts_again_once_drained() + { + var channel = new PasswordResetEmailChannel(NullLogger.Instance); + for (var i = 0; i < PasswordResetEmailChannel.Capacity; i++) + channel.TryEnqueue($"user{i}@example.com", "subject", "body"); + channel.TryEnqueue("late@example.com", "subject", "body").Should().BeFalse(); + + var delivered = new List(); + while (channel.Reader.TryRead(out var email)) + { + delivered.Add(email.EmailTo); + channel.MarkHandled(); + } + + delivered.Should().HaveCount(PasswordResetEmailChannel.Capacity).And.NotContain("late@example.com"); + channel.Pending.Should().Be(0); + channel.TryEnqueue("retry@example.com", "subject", "body").Should().BeTrue(); + (await channel.Reader.ReadAsync()).EmailTo.Should().Be("retry@example.com"); + } +} diff --git a/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs b/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs index 5b2b7af..da87fdd 100644 --- a/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs +++ b/Api.SeaHavenIndustries/HostedServices/PasswordResetEmailDelivery.cs @@ -29,7 +29,10 @@ namespace Api.SeaHavenIndustries.HostedServices private readonly Channel _channel = Channel.CreateBounded( new BoundedChannelOptions(Capacity) { - FullMode = BoundedChannelFullMode.DropWrite, + // Wait, not DropWrite: with DropWrite, TryWrite reports success and discards + // the email, so a full queue would still count the request. TryWrite never + // blocks; under Wait it returns false when the queue is full. + FullMode = BoundedChannelFullMode.Wait, SingleReader = true }); private readonly ILogger _logger;