feat(sms): export wizard SMS-config selection (S10c)
This commit is contained in:
@@ -129,6 +129,9 @@ public class TransportExportPageTests : BunitContext
|
||||
var db = new DatabaseConnectionDefinition("Hist", "Server=.;") { Id = 30 };
|
||||
var notifList = new NotificationList("Ops") { Id = 40 };
|
||||
var smtp = new SmtpConfiguration("smtp.example.com", "Basic", "no-reply@example.com") { Id = 50 };
|
||||
// S10c: an SMS provider config, mirroring the SMTP entry above. Labelled by
|
||||
// AccountSid; the AuthToken is a secret and must never reach the markup.
|
||||
var sms = new SmsConfiguration("AC123", "+15551230001") { Id = 60, AuthToken = "super-secret-token" };
|
||||
// Inbound API keys are not transported between environments (re-arch C4) — the
|
||||
// export page no longer offers a keys selection list, only API methods.
|
||||
var apiMethod = new ApiMethod("CreateOrder", "// noop") { Id = 70 };
|
||||
@@ -147,6 +150,8 @@ public class TransportExportPageTests : BunitContext
|
||||
.Returns(Task.FromResult<IReadOnlyList<NotificationList>>(new List<NotificationList> { notifList }));
|
||||
_notificationRepo.GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SmtpConfiguration>>(new List<SmtpConfiguration> { smtp }));
|
||||
_notificationRepo.GetAllSmsConfigurationsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SmsConfiguration>>(new List<SmsConfiguration> { sms }));
|
||||
_inboundApiRepo.GetAllApiMethodsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<ApiMethod>>(new List<ApiMethod> { apiMethod }));
|
||||
|
||||
@@ -163,6 +168,7 @@ public class TransportExportPageTests : BunitContext
|
||||
"group-db-connections",
|
||||
"group-notification-lists",
|
||||
"group-smtp-configs",
|
||||
"group-sms-configs",
|
||||
"group-api-methods",
|
||||
})
|
||||
{
|
||||
@@ -180,6 +186,9 @@ public class TransportExportPageTests : BunitContext
|
||||
Assert.Contains("Hist", cut.Markup);
|
||||
Assert.Contains("Ops", cut.Markup);
|
||||
Assert.Contains("smtp.example.com", cut.Markup);
|
||||
// S10c: the SMS config shows its AccountSid label, never its secret AuthToken.
|
||||
Assert.Contains("AC123", cut.Markup);
|
||||
Assert.DoesNotContain("super-secret-token", cut.Markup);
|
||||
Assert.Contains("CreateOrder", cut.Markup);
|
||||
|
||||
// Next button is disabled while no selection exists.
|
||||
@@ -427,6 +436,89 @@ public class TransportExportPageTests : BunitContext
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Test 8 (S10c): the Step 1 SMS section renders available SMS configs by
|
||||
// AccountSid (never AuthToken), and ticking one flows its entity id into
|
||||
// ExportSelection.SmsConfigurationIds — mirroring the SMTP-by-Host contract.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public async Task Step4_export_carries_selected_sms_configuration_ids()
|
||||
{
|
||||
var sms = new SmsConfiguration("AC777", "+15559990001") { Id = 77, AuthToken = "do-not-leak" };
|
||||
_notificationRepo.GetAllSmsConfigurationsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SmsConfiguration>>(new List<SmsConfiguration> { sms }));
|
||||
// The resolver fetches the selected SMS config by id when building the closure.
|
||||
_notificationRepo.GetSmsConfigurationByIdAsync(77, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<SmsConfiguration?>(sms));
|
||||
|
||||
_exporter
|
||||
.ExportAsync(
|
||||
Arg.Any<ExportSelection>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string?>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(_ => Task.FromResult<Stream>(new MemoryStream(new byte[] { 0x50, 0x4b, 0x03, 0x04 })));
|
||||
|
||||
var cut = Render<TransportExportPage>();
|
||||
cut.WaitForState(() => cut.Markup.Contains("AC777"));
|
||||
|
||||
// The SMS section renders the available config by AccountSid; its secret
|
||||
// AuthToken is never rendered.
|
||||
Assert.NotNull(cut.Find("[data-testid='group-sms-configs']"));
|
||||
Assert.DoesNotContain("do-not-leak", cut.Markup);
|
||||
|
||||
// Tick the SMS config checkbox (id pattern mirrors the SMTP flat-list wiring).
|
||||
cut.Find("#chk-SmsConfiguration-77").Change(true);
|
||||
|
||||
// Step 1 → 2 → 3 → export.
|
||||
await cut.FindAll("button").First(b => b.TextContent.Trim() == "Next").ClickAsync(new());
|
||||
cut.WaitForAssertion(() => Assert.Contains("Selected by you", cut.Markup));
|
||||
await cut.FindAll("button").First(b => b.TextContent.Trim() == "Next").ClickAsync(new());
|
||||
cut.WaitForAssertion(() => Assert.Contains("Passphrase", cut.Markup));
|
||||
|
||||
cut.Find("#passphrase").Input("hunter2hunter2");
|
||||
cut.Find("#passphrase-confirm").Input("hunter2hunter2");
|
||||
await cut.FindAll("button").First(b => b.TextContent.Trim() == "Export").ClickAsync(new());
|
||||
cut.WaitForAssertion(() => Assert.Contains("Bundle ready", cut.Markup));
|
||||
|
||||
await _exporter.Received(1).ExportAsync(
|
||||
Arg.Is<ExportSelection>(s => s.SmsConfigurationIds.Contains(77)),
|
||||
"alice",
|
||||
"test-cluster",
|
||||
"hunter2hunter2",
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Test 9 (S10c): CountSecrets counts a populated SMS AuthToken, mirroring
|
||||
// the SMTP Credentials contribution to the Step 3 secrets banner.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void CountSecrets_includes_sms_configuration_auth_token()
|
||||
{
|
||||
var smsWithToken = new SmsConfiguration("AC1", "+15551110000") { Id = 1, AuthToken = "tok" };
|
||||
var smsNoToken = new SmsConfiguration("AC2", "+15552220000") { Id = 2, AuthToken = null };
|
||||
|
||||
var resolved = new ResolvedExport(
|
||||
TemplateFolders: Array.Empty<TemplateFolder>(),
|
||||
Templates: Array.Empty<Template>(),
|
||||
SharedScripts: Array.Empty<SharedScript>(),
|
||||
ExternalSystems: Array.Empty<ExternalSystemDefinition>(),
|
||||
ExternalSystemMethods: Array.Empty<ExternalSystemMethod>(),
|
||||
DatabaseConnections: Array.Empty<DatabaseConnectionDefinition>(),
|
||||
NotificationLists: Array.Empty<NotificationList>(),
|
||||
SmtpConfigs: Array.Empty<SmtpConfiguration>(),
|
||||
ApiMethods: Array.Empty<ApiMethod>(),
|
||||
ContentManifest: Array.Empty<ManifestContentEntry>())
|
||||
{
|
||||
SmsConfigs = new List<SmsConfiguration> { smsWithToken, smsNoToken },
|
||||
};
|
||||
|
||||
// Only the config with a non-empty AuthToken counts.
|
||||
Assert.Equal(1, TransportExportPage.CountSecrets(resolved));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Static helpers — exercised directly so the file-naming + secret-count
|
||||
// contract is unit-pinned independently of the rendering surface.
|
||||
|
||||
Reference in New Issue
Block a user