diff --git a/README.md b/README.md index 22f57f9..dfbb82f 100644 --- a/README.md +++ b/README.md @@ -1118,8 +1118,9 @@ docker compose -f compose.yml -f e2e-tests/compose.oauth.yml down -v **Full browser flow** (`full-flow.spec.ts`) — the real Playwright UI against the live stack: the themed **password login** and a **mocked-SSO** login (an in-network mock OIDC provider, -`e2e-tests/mock-oidc.ts`), **menu filtering by role**, the **users/groups/roles** admin CRUD, a -permission-gated **plugin page**, and **logout**. Because the themed form posts straight to +`e2e-tests/mock-oidc.ts`), **menu filtering by role**, the **users/groups/roles** admin CRUD, the +**OAuth2-clients** admin screen (register → one-time secret → delete; Hydra is part of this +stack for it), a permission-gated **plugin page**, and **logout**. Because the themed form posts straight to Kratos and cookies are host-scoped, a tiny same-origin gateway (`e2e-tests/proxy.ts`) fronts web + Kratos on one host (`ory/kratos/e2e-proxy.yml` points Kratos at it) — exactly as a production reverse proxy would. diff --git a/e2e-tests/compose.full.yml b/e2e-tests/compose.full.yml index 28fd88d..9c4e6cc 100644 --- a/e2e-tests/compose.full.yml +++ b/e2e-tests/compose.full.yml @@ -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 diff --git a/e2e-tests/full-flow.spec.ts b/e2e-tests/full-flow.spec.ts index b976d67..ab971ff 100644 --- a/e2e-tests/full-flow.spec.ts +++ b/e2e-tests/full-flow.spec.ts @@ -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"); diff --git a/todo.md b/todo.md index cd348c2..1627ef4 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,7 @@ - [x] CI/CD - Setup renovate bot. Check how other repos on this Gitea is setup you can get access to, there should be a number of renovate bot activated ones. (`renovate.yml` runs the self-hosted `renovate/renovate` image nightly against `renovate.json` — this repo only, via the shared `renovate@larvit.se` bot + `RENOVATE_TOKEN` secret, mirroring the `pwrpln/core` pattern; standard managers cover npm/Dockerfiles/compose/gitea-action pins, two custom regex managers cover the image tags embedded in workflow `run:` steps, the Ory + Playwright lockstep sets are grouped, every bump stays an exact pin, and each PR automerges once the gate is green; documented in README → CI/CD.) - [x] CI/CD - Renovate: set a read-only `GITHUB_COM_TOKEN` env in `renovate.yml` so Renovate stops hitting github.com rate limits when resolving github-hosted deps (Playwright, lucide, `actions/checkout`) and can fetch changelogs. Non-blocking refinement; needs a read-only GitHub PAT stored as an Actions secret. (The renovate job forwards the `RENOVATE_GITHUB_TOKEN` secret — a scopeless read-only github.com PAT; Gitea rejects `GITHUB_`-prefixed secret names — into the container as `GITHUB_COM_TOKEN`; documented in README → CI/CD.) - [x] CI/CD - When renovate updates a dependency - also release a new version of plainpages based on what got updated with Renovate. Major typescript? New apiVersion + new major. A tiny patch to ejs? Only patch release etc. Before implementing, explain in detail how you will solve this. (`renovate.yml` gains an `auto-release` job (`needs: renovate`) that cuts one `vX.Y.Z` tag per run for what Renovate merged; level = highest `Release-Bump:` trailer Renovate stamps via `commitBody`, any dep's major/minor/patch mapped straight through (default patch). Decoupled from `apiVersion` (tag-only, `HOST_API_VERSION` untouched — a "major" is just a bigger image tag, never a plugin break); pre-1.0 shifts down so nothing auto-crosses into 1.0.0. Pure `auto-release/next-version.ts` + unit tests; tag pushed with renovate-bot's PAT so `release.yml` fires; documented in README → CI/CD.) -- [ ] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen. The full-flow e2e suite runs without Hydra (compose.full.yml), so /admin/clients register/detail/delete is only unit-covered (src/http/app.test.ts); wire Hydra into an e2e stack and drive the screen in the browser. +- [x] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen. The full-flow e2e suite runs without Hydra (compose.full.yml), so /admin/clients register/detail/delete is only unit-covered (src/http/app.test.ts); wire Hydra into an e2e stack and drive the screen in the browser. (compose.full.yml now includes Hydra (`serve all --dev`) and full-flow.spec.ts drives /admin/clients register → one-time secret → list → detail → delete in the browser; documented in README → Testing.) - [ ] Build and publish docker image as CI/CD. - [ ] The human developer understands the security model in the auth in this project. - [ ] Add i18n support.