feat(notifications): SMS notifications retry under SmsConfiguration.MaxRetries/RetryDelay (SMTP policy remains the Email + fallback policy) — wires the dead fields
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
+74
@@ -684,4 +684,78 @@ public class NotificationOutboxActorDispatchTests : TestKit
|
||||
// Strictly sequential: never more than one delivery in flight at once.
|
||||
Assert.Equal(1, adapter.HighWater);
|
||||
}
|
||||
|
||||
// ── Task 18: per-type retry policy (SMS honors SmsConfiguration) ──────────
|
||||
|
||||
private void SetupSmsRetryPolicy(int maxRetries, TimeSpan retryDelay)
|
||||
{
|
||||
var config = new SmsConfiguration("ACtestaccountsid", "+15550000000")
|
||||
{
|
||||
MaxRetries = maxRetries,
|
||||
RetryDelay = retryDelay,
|
||||
};
|
||||
_notificationRepository.GetSmsConfigurationAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(config);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmsNotification_TransientFailure_UsesSmsRetryPolicy()
|
||||
{
|
||||
// Task 18: an SMS-typed notification resolves its retry policy from the SMS
|
||||
// configuration, NOT the shared SMTP policy. The SMTP policy (MaxRetries=10)
|
||||
// would keep this row Retrying; the SMS policy (MaxRetries=1) parks it after
|
||||
// this attempt (RetryCount 1 -> 2 >= 1). A pre-fix dispatcher (SMTP-for-all)
|
||||
// would leave it Retrying with RetryCount=2 — the assertion below fails.
|
||||
SetupSmtpRetryPolicy(maxRetries: 10, retryDelay: TimeSpan.FromMinutes(1));
|
||||
SetupSmsRetryPolicy(maxRetries: 1, retryDelay: TimeSpan.FromMinutes(5));
|
||||
var notification = MakeNotification(type: NotificationType.Sms, retryCount: 1);
|
||||
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new[] { notification });
|
||||
var adapter = new StubAdapter(() => DeliveryOutcome.Transient("twilio 503"),
|
||||
type: NotificationType.Sms);
|
||||
var actor = CreateActor([adapter]);
|
||||
|
||||
actor.Tell(InternalMessages.DispatchTick.Instance);
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
_outboxRepository.Received(1).UpdateAsync(
|
||||
Arg.Is<Notification>(n =>
|
||||
n.Status == NotificationStatus.Parked &&
|
||||
n.RetryCount == 2 &&
|
||||
n.LastError == "twilio 503"),
|
||||
Arg.Any<CancellationToken>());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmailNotification_StillUsesSmtpPolicy()
|
||||
{
|
||||
// Task 18: an Email row continues to resolve against the SMTP policy even when a
|
||||
// (tighter) SMS policy is present. SMTP MaxRetries=10 keeps it Retrying
|
||||
// (RetryCount 1 -> 2 < 10) and schedules the next attempt with the SMTP delay.
|
||||
SetupSmtpRetryPolicy(maxRetries: 10, retryDelay: TimeSpan.FromMinutes(3));
|
||||
SetupSmsRetryPolicy(maxRetries: 1, retryDelay: TimeSpan.FromMinutes(5));
|
||||
var before = DateTimeOffset.UtcNow;
|
||||
var notification = MakeNotification(type: NotificationType.Email, retryCount: 1);
|
||||
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new[] { notification });
|
||||
var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout"),
|
||||
type: NotificationType.Email);
|
||||
var actor = CreateActor([adapter]);
|
||||
|
||||
actor.Tell(InternalMessages.DispatchTick.Instance);
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
_outboxRepository.Received(1).UpdateAsync(
|
||||
Arg.Is<Notification>(n =>
|
||||
n.Status == NotificationStatus.Retrying &&
|
||||
n.RetryCount == 2 &&
|
||||
n.NextAttemptAt != null &&
|
||||
n.NextAttemptAt > before + TimeSpan.FromMinutes(2) &&
|
||||
n.LastError == "smtp timeout"),
|
||||
Arg.Any<CancellationToken>());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user