test(e2e): API-key create→token reveal + name/method validation edges
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using Microsoft.Playwright;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// E2E coverage for the inbound API-key create form (<c>/admin/api-keys/create</c>,
|
||||
/// rendered by <c>ApiKeyForm.razor</c>). One happy-path test asserts the one-time token
|
||||
/// reveal after a successful create (and tears the key down via the CLI in a <c>finally</c>),
|
||||
/// plus two validation-edge tests that assert the exact inline messages the razor renders
|
||||
/// for an empty name and for a key with no method selected.
|
||||
///
|
||||
/// <para>
|
||||
/// The <see cref="ApiSurfaceFixture"/> provisions a single inbound api-method so the form
|
||||
/// renders at least one method checkbox (<c>#method-access-{MethodId}</c>); this fixture
|
||||
/// owns that method (cleaned at fixture dispose). Created keys are owned by the tests and
|
||||
/// deleted per-test.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Verified against <c>ApiKeyForm.razor</c>: the name input is the single
|
||||
/// <c>input[type='text'].form-control.form-control-sm</c> (editable on create); the Save
|
||||
/// button is the success button with text "Save"; the created-token panel exposes
|
||||
/// <c>[data-test='created-token']</c> with an adjacent "Copy" button; inline validation
|
||||
/// renders in <c>div.text-danger.small</c> with the literal messages
|
||||
/// <c>"Name is required."</c> and <c>"Select at least one API method for this key."</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Collection("Playwright")]
|
||||
public sealed class ApiKeyCrudTests : IClassFixture<ApiSurfaceFixture>
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
private readonly ApiSurfaceFixture _api;
|
||||
|
||||
public ApiKeyCrudTests(PlaywrightFixture fixture, ApiSurfaceFixture api)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_api = api;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Happy path: filling the name, checking the fixture method, and saving creates the key
|
||||
/// and reveals the one-time token panel (<c>[data-test='created-token']</c>) with its
|
||||
/// adjacent "Copy" button. MUTATES — the created key is deleted via the CLI in a
|
||||
/// <c>finally</c> (resolve by name, best-effort delete) so nothing leaks.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Create_RevealsOneTimeToken()
|
||||
{
|
||||
Skip.IfNot(_api.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password");
|
||||
var keyName = CliRunner.UniqueName("apikey");
|
||||
|
||||
try
|
||||
{
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/api-keys/create");
|
||||
|
||||
// Blazor Server page renders a LoadingSpinner while OnInitializedAsync loads the
|
||||
// method list; web-first wait for the name input before driving it.
|
||||
var nameInput = page.Locator("input[type='text'].form-control.form-control-sm").First;
|
||||
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
await nameInput.FillAsync(keyName);
|
||||
await page.Locator($"#method-access-{_api.MethodId}").CheckAsync();
|
||||
await page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
|
||||
|
||||
await Assertions.Expect(page.Locator("[data-test='created-token']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await Assertions.Expect(page.GetByRole(AriaRole.Button, new() { Name = "Copy" }))
|
||||
.ToBeVisibleAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
var keyId = await CliRunner.ResolveApiKeyIdByNameAsync(keyName);
|
||||
if (keyId is not null)
|
||||
{
|
||||
await CliRunner.DeleteApiKeyAsync(keyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validation edge: an empty name (with a method checked) blocks the save and renders the
|
||||
/// literal <c>"Name is required."</c> message in <c>div.text-danger</c>; no token panel
|
||||
/// appears, so nothing is created and there is no teardown.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Create_EmptyName_ShowsValidationError()
|
||||
{
|
||||
Skip.IfNot(_api.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password");
|
||||
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/api-keys/create");
|
||||
|
||||
// Web-first wait on the method checkbox guarantees the form (and its method list)
|
||||
// has rendered before we interact.
|
||||
var methodCheckbox = page.Locator($"#method-access-{_api.MethodId}");
|
||||
await Assertions.Expect(methodCheckbox).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
// Leave the name blank; check the method so only the name rule fires.
|
||||
await methodCheckbox.CheckAsync();
|
||||
await page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
|
||||
|
||||
var error = page.Locator("div.text-danger");
|
||||
await Assertions.Expect(error).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await Assertions.Expect(error).ToContainTextAsync("Name is required.");
|
||||
|
||||
// Nothing should have been created — the token panel must never appear.
|
||||
await Assertions.Expect(page.Locator("[data-test='created-token']")).ToHaveCountAsync(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validation edge: a named key with no method selected blocks the save and renders the
|
||||
/// literal <c>"Select at least one API method for this key."</c> message in
|
||||
/// <c>div.text-danger</c>; no token panel appears, so nothing is created and there is no
|
||||
/// teardown.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Create_NoMethods_ShowsValidationError()
|
||||
{
|
||||
Skip.IfNot(_api.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password");
|
||||
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/api-keys/create");
|
||||
|
||||
var nameInput = page.Locator("input[type='text'].form-control.form-control-sm").First;
|
||||
await Assertions.Expect(nameInput).ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
// Fill the name but leave ALL method checkboxes unchecked so only the method rule fires.
|
||||
// The name input uses Blazor @bind (onchange), which only commits on blur — blur the input
|
||||
// and let the change round-trip to the server (NetworkIdle) so _formName is populated before
|
||||
// Save, otherwise the name-required rule short-circuits first.
|
||||
await nameInput.FillAsync(CliRunner.UniqueName("apikey"));
|
||||
await nameInput.BlurAsync();
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
await page.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
|
||||
|
||||
var error = page.Locator("div.text-danger");
|
||||
await Assertions.Expect(error).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await Assertions.Expect(error).ToContainTextAsync("Select at least one API method for this key.");
|
||||
|
||||
// The name-only submit must not create a key — no token panel.
|
||||
await Assertions.Expect(page.Locator("[data-test='created-token']")).ToHaveCountAsync(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user