From 0efbb66bc35f5482c8c44b88606d123e53dcf69f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 7 Jun 2026 03:50:05 -0400 Subject: [PATCH] test(playwright): LDAP missing-field + duplicate-group edge cases (Wave 4) --- .../Admin/LdapMappingCrudTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs index 71bc017f..7628fe41 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Admin/LdapMappingCrudTests.cs @@ -123,4 +123,97 @@ public class LdapMappingCrudTests } } } + + /// + /// The create form's SaveMapping() validates manually (no EditForm / + /// DataAnnotations): a blank LDAP Group Name renders "LDAP Group Name is + /// required." in div.text-danger.small.mt-2 and returns early; a present + /// group but blank role renders "Role is required." Both branches return before + /// any persistence, so nothing is created and no teardown is needed. The first + /// Save is a full circuit roundtrip — proving the Blazor circuit is live — which + /// makes the second-leg fill (group → blank role) safe from the prerender race. + /// + [SkippableFact] + public async Task Save_MissingGroupName_ShowsRequiredError() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var page = await _fixture.NewAuthenticatedPageAsync(); + + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/ldap-mappings/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // ── Leg 1: blank group, role selected → "LDAP Group Name is required." ───── + // Leave the group input blank; pick a role so the group-blank branch (checked + // FIRST in SaveMapping) is unambiguously the one that fires. + await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm") + .SelectOptionAsync("Designer"); + await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')"); + + await Assertions.Expect(page.Locator("div.text-danger.small.mt-2")) + .ToHaveTextAsync("LDAP Group Name is required."); + await Assertions.Expect(page) + .ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/admin/ldap-mappings/create")); + + // ── Leg 2: group filled, role blank → "Role is required." ────────────────── + // The first Save roundtrip proved the circuit live, so filling now is safe. + var group = $"zztest-grp-{Guid.NewGuid():N}"[..18]; + await page.Locator("label:has-text('LDAP Group Name') + input.form-control.form-control-sm") + .FillAsync(group); + await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm") + .SelectOptionAsync(new[] { "" }); + await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')"); + + await Assertions.Expect(page.Locator("div.text-danger.small.mt-2")) + .ToHaveTextAsync("Role is required."); + await Assertions.Expect(page) + .ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/admin/ldap-mappings/create")); + + // Both validations returned early → nothing persisted → no teardown. + } + + /// + /// Attempting to create a mapping whose LDAP group already exists violates the DB + /// unique index on LdapGroupName. The form has no friendly pre-check, so + /// the persistence exception is caught and surfaced as "Save failed: {message}" + /// in div.text-danger.small.mt-2, keeping the user on the create page. + /// The duplicate is CLI-seeded (and torn down) so this asserts the save-failure + /// branch without depending on the raw DB error text. + /// + [SkippableFact] + public async Task Create_DuplicateGroup_ShowsSaveFailedError() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var group = CliRunner.UniqueName("grp"); + var seededId = await CliRunner.CreateRoleMappingAsync(group, "Designer"); + + try + { + var page = await _fixture.NewAuthenticatedPageAsync(); + + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/ldap-mappings/create"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // Fill the SAME group name the CLI just seeded → unique-index violation. + await page.Locator("label:has-text('LDAP Group Name') + input.form-control.form-control-sm") + .FillAsync(group); + await page.Locator("div.mb-2:has(label:has-text('Role')) .form-select.form-select-sm") + .SelectOptionAsync("Viewer"); + await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')"); + + // The catch block prefixes the raw DB message with "Save failed:" — assert + // only on that stable prefix, never the provider-specific tail. + await Assertions.Expect(page.Locator("div.text-danger.small.mt-2")) + .ToContainTextAsync("Save failed"); + await Assertions.Expect(page) + .ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/admin/ldap-mappings/create")); + } + finally + { + // Best-effort: remove the CLI-seeded mapping; the UI never persisted a + // duplicate (the save failed), so only the seed needs cleanup. + await CliRunner.DeleteRoleMappingAsync(seededId); + } + } }