fix(notifications): deterministic lowest-Id SMTP config selection (+ multi-row warning); atomic OAuth2 token cache entry

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 04:14:14 -04:00
parent eb7a913d22
commit 39e1ca3eae
4 changed files with 74 additions and 11 deletions
@@ -264,4 +264,37 @@ public class EmailNotificationDeliveryAdapterTests
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => adapter.DeliverAsync(MakeNotification(), cts.Token));
}
[Fact]
public async Task MultipleSmtpConfigs_LowestIdWins()
{
// Arch-review S7: when more than one SMTP configuration row exists the pick
// must be deterministic (lowest Id), regardless of repository row order.
var list = new NotificationList("ops-team") { Id = 1 };
_repository.GetListByNameAsync("ops-team").Returns(list);
_repository.GetRecipientsByListIdAsync(1).Returns(new List<NotificationRecipient>
{
new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 }
});
// Repository returns the higher-Id row FIRST — FirstOrDefault would pick it.
_repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration>
{
new("smtp-seven.example.com", "basic", "noreply@example.com")
{
Id = 7, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
},
new("smtp-three.example.com", "basic", "noreply@example.com")
{
Id = 3, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
}
});
var adapter = CreateAdapter();
await adapter.DeliverAsync(MakeNotification());
// The lowest-Id row (3) must be the one connected to.
await _smtpClient.Received().ConnectAsync(
"smtp-three.example.com", Arg.Any<int>(), Arg.Any<SmtpTlsMode>(),
Arg.Any<int>(), Arg.Any<CancellationToken>());
}
}