test(sms): regression tests for code-review fixes

Lock the behaviors changed by the review-fix commit + the security invariants:

- ManagementActorTests: UpdateSms/SmtpConfig now require Administrator (updated the
  existing success cases from Designer); + UpdateSmsConfig_WithDesignerRole_Returns
  Unauthorized and _WithEmptyAuthToken_PreservesExistingToken regression tests.
- SecretEncryptionTests: SmsConfiguration.AuthToken stored-encrypted round-trip +
  null round-trip (AccountSid stays plaintext) — guards ApplySecretColumnEncryption.
- ArtifactDiffTests: CompareSmsConfiguration New/Identical/Modified + the secret
  presence-only invariant (value never echoed, presence-flip shows <present> only).
- UpdateCommandContractTests: notification sms update core fields Required, --auth-token optional.
- NotificationListsPageTests: SMS recipient badge shows phone, not "Name <>".
- NotificationOutboxActorDispatchTests: SMS-typed notification routes to the SMS
  adapter (StubAdapter.Type made configurable), not the Email adapter.
- NotificationRecipientTests (new): ForEmail/ForSms + public-ctor invariants.
This commit is contained in:
Joseph Doherty
2026-06-19 15:09:47 -04:00
parent cd8e4872f6
commit a9393c8913
7 changed files with 312 additions and 10 deletions
@@ -52,15 +52,17 @@ public class NotificationOutboxActorDispatchTests : TestKit
private readonly Func<DeliveryOutcome> _outcome;
private readonly TimeSpan _delay;
public StubAdapter(Func<DeliveryOutcome> outcome, TimeSpan? delay = null)
public StubAdapter(Func<DeliveryOutcome> outcome, TimeSpan? delay = null,
NotificationType type = NotificationType.Email)
{
_outcome = outcome;
_delay = delay ?? TimeSpan.Zero;
Type = type;
}
public int CallCount;
public NotificationType Type => NotificationType.Email;
public NotificationType Type { get; }
public async Task<DeliveryOutcome> DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
@@ -151,6 +153,35 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
[Fact]
public void Dispatch_SmsTypedNotification_RoutesToSmsAdapter_NotEmailAdapter()
{
// NotificationOutbox-NNN: the Type-keyed adapter selection must land an SMS-typed
// notification on the SMS adapter (not the Email one) and apply its outcome.
SetupSmtpRetryPolicy(maxRetries: 5, retryDelay: TimeSpan.FromMinutes(1));
var notification = MakeNotification(type: NotificationType.Sms);
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
var emailAdapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com"),
type: NotificationType.Email);
var smsAdapter = new StubAdapter(() => DeliveryOutcome.Success("+15551234567"),
type: NotificationType.Sms);
var actor = CreateActor([emailAdapter, smsAdapter]);
actor.Tell(InternalMessages.DispatchTick.Instance);
AwaitAssert(() =>
{
Assert.Equal(1, smsAdapter.CallCount);
Assert.Equal(0, emailAdapter.CallCount);
_outboxRepository.Received(1).UpdateAsync(
Arg.Is<Notification>(n =>
n.Status == NotificationStatus.Delivered &&
n.ResolvedTargets == "+15551234567"),
Arg.Any<CancellationToken>());
});
}
[Fact]
public void Success_MarksNotificationDelivered_WithResolvedTargets()
{