From b52f7281aaaa4bb8572cf2381df17aec135793fc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 6 Jun 2026 12:21:56 -0400 Subject: [PATCH] test(e2e): Transport import wrong-passphrase shows error and stays on passphrase step --- .../Transport/TransportImportTests.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs index df66bc83..29353784 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportImportTests.cs @@ -156,4 +156,86 @@ public class TransportImportTests try { File.Delete(bundlePath); } catch { /* best-effort */ } } } + + /// + /// Negative path: feed a real encrypted bundle, then submit the WRONG + /// passphrase at Step 2. Per + /// TransportImport.razor.cs::SubmitPassphraseAsync, the importer throws + /// , which + /// increments _failedUnlockAttempts to 1 (below the configured + /// MaxUnlockAttemptsPerSession of 3, so no lockout / no re-upload), + /// sets _errorMessage = "Wrong passphrase. Please try again.", clears + /// the passphrase field, and leaves _step on Passphrase. The wizard + /// therefore renders the [data-testid='error-message'] alert, keeps + /// #import-passphrase visible, and never reaches the Diff step — so + /// [data-testid='diff-summary'] stays absent. + /// + /// + /// We never reach the diff/apply, so the source template is deliberately NOT + /// deleted before import; teardown drops it by name prefix. + /// + /// + [SkippableFact] + public async Task ImportWithWrongPassphrase_ShowsErrorAndStaysOnPassphraseStep() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var tmplName = CliRunner.UniqueName("wrongpass"); + var bundlePath = Path.Combine(Path.GetTempPath(), tmplName + ".scadabundle"); + + try + { + // ── ARRANGE: build + export a synthetic single-template encrypted bundle ── + int tmplId = await CliRunner.CreateTemplateAsync(tmplName); + await CliRunner.AddAttributeAsync(tmplId, "Value", "Double"); + await CliRunner.BundleExportAsync(bundlePath, tmplId, "correct-passphrase-1", "src-env"); + + var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); + + // ── STEP 1: Upload ──────────────────────────────────────────────────────── + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/import"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await page.Locator("#bundle-input").SetInputFilesAsync(bundlePath); + await Assertions.Expect(page.Locator("[data-testid='encrypted-bundle-notice']")) + .ToBeVisibleAsync(new() { Timeout = 15_000 }); + + await page.Locator("button.btn.btn-primary:has-text('Next')").ClickAsync(); + + // ── STEP 2: Passphrase (wrong) ──────────────────────────────────────────── + await Assertions.Expect(page.Locator("#import-passphrase")) + .ToBeVisibleAsync(new() { Timeout = 10_000 }); + await page.Locator("#import-passphrase").FillAsync("WRONG-passphrase-xyz"); + await page.Locator("button.btn.btn-primary:has-text('Unlock')").ClickAsync(); + + // The wrong passphrase surfaces the typed error, keeps the passphrase + // input visible (still on Step 2), and never reveals the diff summary. + await Assertions.Expect(page.Locator("[data-testid='error-message']")) + .ToContainTextAsync("Wrong passphrase. Please try again.", new() { Timeout = 10_000 }); + await Assertions.Expect(page.Locator("#import-passphrase")).ToBeVisibleAsync(); + await Assertions.Expect(page.Locator("[data-testid='diff-summary']")).ToBeHiddenAsync(); + + // Secondary indicator: one failed attempt recorded (1 of MaxUnlockAttempts). + await Assertions.Expect(page.Locator("[data-testid='unlock-attempts']")) + .ToContainTextAsync("Failed unlock attempts: 1 of"); + } + finally + { + // The source template was never deleted (we never reached apply), so + // teardown drops it by name prefix and removes the staged bundle. + try + { + foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(tmplName)) + { + await CliRunner.DeleteTemplateAsync(id); + } + } + catch + { + // Best-effort — never mask the test's own failure. + } + + try { File.Delete(bundlePath); } catch { /* best-effort */ } + } + } }