Browser E2E for the admin OAuth2-clients screen
CI / full-gate (push) Successful in 2m50s

This commit is contained in:
2026-08-02 16:03:30 +02:00
parent 3f30f889c3
commit f5f4455b81
4 changed files with 45 additions and 7 deletions
+12 -4
View File
@@ -1,17 +1,21 @@
# Full browser E2E — the real Playwright UI flow against the live stack: password +
# mocked-SSO login, menu filtering by role, users/groups/roles CRUD, a plugin page, logout. A tiny
# mocked-SSO login, menu filtering by role, users/groups/roles/OAuth2-clients CRUD, a plugin page,
# logout. A tiny
# same-origin gateway (proxy, e2e-tests/proxy.ts) fronts web + Kratos on one host so the browser's cookies
# round-trip (ory/kratos/e2e-proxy.yml points Kratos at it); a mock OIDC provider backs the SSO test.
# docker compose -f compose.yml -f e2e-tests/compose.full.yml run --build --rm e2e
# docker compose -f compose.yml -f e2e-tests/compose.full.yml down -v # tear down after
services:
web:
# First-party + SSO flows need Kratos + Keto + bootstrap, not Hydra — drop it so the stack is
# leaner. SSO is enabled here only (clean clone stays password-only): the mock provider's whole
# array is the env-settable form Kratos offers, mapped through the committed claims jsonnet.
# Kratos + Keto + bootstrap back the login/role flows; Hydra backs the admin plugin's
# OAuth2-clients screen. SSO is enabled here only (clean clone stays password-only): the mock
# provider's whole array is the env-settable form Kratos offers, mapped through the committed
# claims jsonnet.
depends_on: !override
bootstrap:
condition: service_completed_successfully
hydra:
condition: service_healthy
kratos:
condition: service_healthy
keto:
@@ -52,6 +56,10 @@ services:
SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS: >-
[{"id":"mock","provider":"generic","label":"Mock SSO","client_id":"plainpages-e2e","client_secret":"e2e-secret","issuer_url":"http://mock-oidc:9000","scope":["openid","email"],"mapper_url":"file:///etc/config/kratos/oidc/claims.jsonnet"}]
# --dev permits the http issuer (the base file drops it for an https prod issuer).
hydra:
command: serve all --dev -c /etc/config/hydra/hydra.yml
# The reference plugin's upstream (examples/shifts-upstream) so /scheduling/shifts shows real rows.
shifts-upstream:
image: node:24.18.1-alpine3.24
+29
View File
@@ -85,6 +85,35 @@ test.describe.serial("authenticated admin journey", () => {
await expect(page.locator("main")).toContainText(role);
});
test("OAuth2 clients CRUD: register a client (writes go to Hydra), see the one-time secret once, then delete it via the confirm step", async () => {
const name = `e2e-client-${suffix}`;
await page.goto("/admin/clients");
await page.getByRole("link", { name: "Register client" }).click();
await page.fill('input[name="name"]', name);
await page.fill('textarea[name="redirectUris"]', "https://app.example.com/callback");
await page.locator('.form-card button[type="submit"]').click();
// Hydra returns the secret exactly once, so the POST renders the detail directly (no PRG).
await expect(page.locator("h1")).toHaveText("Client registered");
const clientId = await page.locator("#cid").inputValue();
expect(clientId).toBeTruthy();
await expect(page.locator("#csecret")).not.toHaveValue("");
// Listed; the row header links to the plain detail, which never shows the secret again.
await page.goto("/admin/clients");
const row = page.locator("tr", { hasText: name });
await expect(row).toBeVisible();
await row.getByRole("link", { name }).click();
await expect(page).toHaveURL(new RegExp(`/admin/clients/${clientId}`));
await expect(page.locator("#csecret")).toHaveCount(0);
// Delete through the confirm interstitial (danger link on the detail → confirm form's button).
await page.getByRole("link", { name: "Delete client" }).click();
await page.getByRole("button", { name: "Delete client" }).click();
await expect(page).toHaveURL(/\/admin\/clients(\?|$)/);
await expect(page.locator("tr", { hasText: name })).toHaveCount(0);
});
test("plugin page: the reference plugin renders its upstream shifts inside the native shell", async () => {
await page.goto("/scheduling/shifts");
await expect(page.locator("h1")).toHaveText("Shifts");