Add tests/e2e/ — end-to-end test for the initial password-setup flow on a scaffolded project #7

Closed
opened 2026-04-25 21:07:20 +02:00 by uwe.admin · 7 comments
Owner

Why

Two recent bugs slipped past every existing test layer because none of them exercises a deployed scaffolded project:

  1. oatpp-authkit interceptor rejected /set-password?token=… with 401 because the public-path check compared against the request-target (which includes the query string). Fixed in oatpp-authkit 46971ac / v0.3.3.
  2. New projects ship with VITE_BASE='/' so SPA assets 404 behind the Apache /projects/<name>/ prefix → blank page. See the companion bug issue.

Both are integration failures: the unit tests pass, the per-component tests pass, but a real user clicking a real reset-link in a real email gets a broken page.

The existing webapp-template tests/selenium/test_smoke.py::test_set_password_page only navigates to /set-password against the local dev server with no proxy prefix — it cannot catch either class of bug.

Proposed shape

New directory tests/e2e/ in this scaffold (since this scaffold owns new-project.sh, the Apache vhost template, and the VITE_BASE wiring — that's where the contract under test lives, not in derived projects). Single pytest module test_password_setup.py. Fixture chain:

  1. Provision a throwaway project via bin/new-project.sh tmp-foo into a tempdir.
  2. Build it with the same commands the CI deploy job uses (CMake + npm run build with VITE_BASE pinned exactly as the Apache vhost expects).
  3. Boot the binary on an ephemeral port. Front it with a tiny in-process Python reverse proxy (~60 LOC, no nginx dependency in CI) that strips /projects/tmp-foo/ and sets X-Forwarded-Prefix.
  4. Issue an admin-reset token via webapp --issue-admin-reset.
  5. Selenium loads the prefixed URL http://localhost:PROXY/projects/tmp-foo/set-password?token=…. Assertions:
    • The set-password form is visible (no JSON, no blank page).
    • Every <script src> and <link href> referenced by the loaded HTML returns 200 (catches VITE_BASE drift).
    • Submitting a chosen password succeeds; following login lands on the admin shell.

Why webapp-scaffold and not webapp-template

  • The contract being tested is "scaffold-emitted projects produce a working set-password flow behind a reverse-proxy prefix" — that's this repo's responsibility.
  • Fires whenever new-project.sh, the Apache vhost template, the build pipeline, or the pinned oatpp-authkit version regresses.
  • webapp-template's existing test_set_password_page stays as the cheap dev-server smoke test.

Cost

One CI job, ~30s for build + ~10s for the Selenium round-trip. Reuses selenium-helper. No new infra.

Decision needed

Check one (edit this comment):

  • Option A — In-process Python proxy — ~60 LOC; no system dependency. Easiest to vendor into the test.
  • Option B — Spin up a real Apache/nginx in CI — more faithful to production, but adds a system package to the CI image and a config file to vendor.
  • Option C — Skip the proxy entirely; set X-Forwarded-Prefix manually with curl/Selenium — fastest but doesn't test the path-rewriting half of the story (so the VITE_BASE class of bug only half-fires).

Default recommendation: A. Smallest blast radius, exercises both halves of the prefix flow, no new system deps.

## Why Two recent bugs slipped past every existing test layer because none of them exercises a *deployed* scaffolded project: 1. `oatpp-authkit` interceptor rejected `/set-password?token=…` with 401 because the public-path check compared against the request-target (which includes the query string). Fixed in oatpp-authkit `46971ac` / v0.3.3. 2. New projects ship with `VITE_BASE='/'` so SPA assets 404 behind the Apache `/projects/<name>/` prefix → blank page. See the companion bug issue. Both are *integration* failures: the unit tests pass, the per-component tests pass, but a real user clicking a real reset-link in a real email gets a broken page. The existing webapp-template `tests/selenium/test_smoke.py::test_set_password_page` only navigates to `/set-password` against the local dev server with no proxy prefix — it cannot catch either class of bug. ## Proposed shape New directory `tests/e2e/` in this scaffold (since this scaffold owns `new-project.sh`, the Apache vhost template, and the VITE_BASE wiring — that's where the contract under test lives, not in derived projects). Single pytest module `test_password_setup.py`. Fixture chain: 1. **Provision** a throwaway project via `bin/new-project.sh tmp-foo` into a tempdir. 2. **Build** it with the same commands the CI deploy job uses (CMake + `npm run build` with VITE_BASE pinned exactly as the Apache vhost expects). 3. **Boot** the binary on an ephemeral port. **Front it** with a tiny in-process Python reverse proxy (~60 LOC, no nginx dependency in CI) that strips `/projects/tmp-foo/` and sets `X-Forwarded-Prefix`. 4. **Issue** an admin-reset token via `webapp --issue-admin-reset`. 5. **Selenium** loads the prefixed URL `http://localhost:PROXY/projects/tmp-foo/set-password?token=…`. Assertions: - The set-password form is visible (no JSON, no blank page). - Every `<script src>` and `<link href>` referenced by the loaded HTML returns 200 (catches VITE_BASE drift). - Submitting a chosen password succeeds; following login lands on the admin shell. ## Why webapp-scaffold and not webapp-template - The contract being tested is *"scaffold-emitted projects produce a working set-password flow behind a reverse-proxy prefix"* — that's this repo's responsibility. - Fires whenever `new-project.sh`, the Apache vhost template, the build pipeline, or the pinned `oatpp-authkit` version regresses. - webapp-template's existing `test_set_password_page` stays as the cheap dev-server smoke test. ## Cost One CI job, ~30s for build + ~10s for the Selenium round-trip. Reuses `selenium-helper`. No new infra. ## Decision needed Check one (edit this comment): - [ ] **Option A — In-process Python proxy** — ~60 LOC; no system dependency. Easiest to vendor into the test. - [ ] **Option B — Spin up a real Apache/nginx in CI** — more faithful to production, but adds a system package to the CI image and a config file to vendor. - [ ] **Option C — Skip the proxy entirely; set `X-Forwarded-Prefix` manually with curl/Selenium** — fastest but doesn't test the path-rewriting half of the story (so the VITE_BASE class of bug only half-fires). Default recommendation: **A**. Smallest blast radius, exercises both halves of the prefix flow, no new system deps.
Author
Owner

Agent Evaluation

The issue body itself is the evaluation — a well-scoped proposal with motivation (two real bugs that slipped past existing tests), shape (one pytest module, fixture chain spelled out, ~60 LOC proxy), and decision options. No re-spec needed.

Feasibility: Medium. The bones already exist — new-project.sh, selenium-helper, the Apache vhost template — and assembling them into a single fixture chain is mechanical. The only fresh code is the in-process reverse proxy (~60 LOC, stdlib http.server + urllib.request).

Impact: High for what it covers — closes the integration gap that let two regressions (oatpp-authkit query-string bug and VITE_BASE drift) ship undetected. Stays in CI forever, fires on any change to the scaffold's emitted-project contract.

Effort: Medium (one PR, one new directory, one new pytest module + proxy helper, CI wiring).

Recommendation: Accept. The decision options in the body are the right framing; Option A (in-process Python proxy) is the right default for the reasons the body already lists — vendor-able, no CI image churn, exercises the path-rewriting half. Option B adds an Apache dependency to CI for marginal fidelity gain; Option C skips the half of the bug class that's most likely to regress (VITE_BASE).

Implementation plan

  1. tests/e2e/conftest.py — Pytest fixtures: tmp_project (calls bin/new-project.sh into a tmp_path, runs CMake + npm run build with VITE_BASE=/projects/tmp-foo/ pinned), boot_app (spawns the built binary on an ephemeral port, captures stdout for failure diag), proxy (ThreadingHTTPServer subclass that strips the prefix and forwards via urllib, sets X-Forwarded-Prefix).
  2. tests/e2e/test_password_setup.py — Three assertions per the body: form visible, all <script src>/<link href> referenced by the served HTML return 200, submitting a chosen password redirects to the admin shell.
  3. tests/e2e/proxy.py — Standalone for unit-testability: class PrefixStrippingProxy that takes (target_host, target_port, prefix). Importable from elsewhere if a future test wants the same harness.
  4. CI wiring — Add tests/e2e/ to whatever pytest invocation runs in CI (or add a new npm run test:e2e step that's gated to PRs touching the scaffold contract — bin/new-project.sh, templates/, package.json — to keep the cycle short).

Decision needed

The issue body already presents A/B/C — leaving the existing checkboxes there is fine; check the same box on this comment too so the poll loop's ### Decision needed detector picks it up:

Check one (edit this comment):

  • Option A — In-process Python proxy — recommended (smallest blast radius, exercises both halves)
  • Option B — Real Apache/nginx in CI — more faithful, but adds a system dep + config file
  • Option C — Skip proxy; X-Forwarded-Prefix only — fastest, but VITE_BASE class of bug only half-tested
## Agent Evaluation The issue body itself is the evaluation — a well-scoped proposal with motivation (two real bugs that slipped past existing tests), shape (one pytest module, fixture chain spelled out, ~60 LOC proxy), and decision options. No re-spec needed. **Feasibility:** Medium. The bones already exist — `new-project.sh`, `selenium-helper`, the Apache vhost template — and assembling them into a single fixture chain is mechanical. The only fresh code is the in-process reverse proxy (~60 LOC, stdlib `http.server` + `urllib.request`). **Impact:** High *for what it covers* — closes the integration gap that let two regressions (oatpp-authkit query-string bug and `VITE_BASE` drift) ship undetected. Stays in CI forever, fires on any change to the scaffold's emitted-project contract. **Effort:** Medium (one PR, one new directory, one new pytest module + proxy helper, CI wiring). **Recommendation:** Accept. The decision options in the body are the right framing; **Option A** (in-process Python proxy) is the right default for the reasons the body already lists — vendor-able, no CI image churn, exercises the path-rewriting half. Option B adds an Apache dependency to CI for marginal fidelity gain; Option C skips the half of the bug class that's most likely to regress (VITE_BASE). ### Implementation plan 1. **`tests/e2e/conftest.py`** — Pytest fixtures: `tmp_project` (calls `bin/new-project.sh` into a `tmp_path`, runs CMake + `npm run build` with `VITE_BASE=/projects/tmp-foo/` pinned), `boot_app` (spawns the built binary on an ephemeral port, captures stdout for failure diag), `proxy` (ThreadingHTTPServer subclass that strips the prefix and forwards via urllib, sets `X-Forwarded-Prefix`). 2. **`tests/e2e/test_password_setup.py`** — Three assertions per the body: form visible, all `<script src>`/`<link href>` referenced by the served HTML return 200, submitting a chosen password redirects to the admin shell. 3. **`tests/e2e/proxy.py`** — Standalone for unit-testability: `class PrefixStrippingProxy` that takes `(target_host, target_port, prefix)`. Importable from elsewhere if a future test wants the same harness. 4. **CI wiring** — Add `tests/e2e/` to whatever pytest invocation runs in CI (or add a new `npm run test:e2e` step that's gated to PRs touching the scaffold contract — `bin/new-project.sh`, `templates/`, `package.json` — to keep the cycle short). ### Decision needed The issue body already presents A/B/C — leaving the existing checkboxes there is fine; check the same box on this comment too so the poll loop's `### Decision needed` detector picks it up: Check one (edit this comment): - [ ] **Option A — In-process Python proxy** — recommended (smallest blast radius, exercises both halves) - [ ] **Option B — Real Apache/nginx in CI** — more faithful, but adds a system dep + config file - [ ] **Option C — Skip proxy; X-Forwarded-Prefix only** — fastest, but VITE_BASE class of bug only half-tested
uwe.admin added the
effort:medium
evaluated
labels 2026-04-25 21:09:56 +02:00
Author
Owner

Evaluated #7 — Medium, recommend accept (default Option A: in-process Python proxy).

Evaluated #7 — Medium, recommend accept (default Option A: in-process Python proxy).
uwe.admin added the
accepted
label 2026-04-25 21:32:07 +02:00
Author
Owner

Re-evaluation

Before implementing, re-checking the prerequisites: the proposal's Step 1 fixture chain says

  1. Provision a throwaway project via bin/new-project.sh tmp-foo into a tempdir.

…but webapp-scaffold has no bin/new-project.sh. The actual script is /home/git/webapp-template/scripts/new-project.sh — and inspecting it, it's a host-provisioning script, not a tempdir-friendly one:

  • runs as root (sudo required)
  • creates a Linux user (claude-NAME), a /etc/sudoers.d/NAME drop-in, a systemd unit, an Apache vhost at /etc/apache2/sites-available/
  • seeds a Forgejo repo (talks to a live Forgejo)
  • writes /etc/NAME/{prod,test}.env

None of those are reversible in a tmp_path — and the script can't run in unprivileged CI. So the fixture chain as written is not implementable today.

Three concrete shapes that would work

  • Option A1 — Scoped variant of original A. Skip new-project.sh entirely. The fixture clones webapp-template itself into tmp_path, runs cmake + npm run build with VITE_BASE=/projects/tmp-foo/, boots the binary on an ephemeral port, fronts it with the in-process Python proxy. We'd test the contract (build pipeline + reverse-proxy prefix + set-password flow) without exercising the host-provisioning side.
  • Option A2 — Even smaller. Skip the build too. Use a pre-built binary already on disk (e.g. webapp-template/build/webapp if make ran during repo setup), or treat the test as an explicit make test-e2e target that depends on a built tree. Cuts the per-run cost from ~30s to ~5s.
  • Option D — Add bin/new-project.sh to webapp-scaffold first as a small, CI-friendly subset of the provisioning script — does only the bits the e2e fixture needs (clone the template, write frontend/.env.production). Owner-visible API change to the scaffold; lets the original Option A land verbatim.

Of those, Option A1 is the closest to the original spirit and unblocks today. Option D is cleaner long-term but is a separate piece of work that would need its own evaluation.

Other gaps

  • The e2e module expects to issue an admin reset token via webapp --issue-admin-reset. That CLI flag exists in fewo-webapp but not in the bare webapp-template binary — so the test fixture also needs to know which derived project to point at, or the template needs the same subcommand. Worth confirming before writing the test.
  • Selenium needs a working Chrome/Geckodriver in the CI environment. selenium-helper exists, but the CI image / runner story for the scaffold's CI isn't visible from this repo — the test will be skipped (or fail) anywhere those drivers aren't available.

Ask

Tick a box on the original evaluation comment (or reply with the chosen letter) so I can proceed:

  • A as written: blocked on missing prerequisites — not implementable today.
  • A1 (scoped — clone webapp-template, build, boot, proxy): my recommended fallback. ~80 LOC, no host changes, exercises the same contract minus the host-provisioning half.
  • D: write bin/new-project.sh first (separate issue; #7 then implements as A).

Happy to proceed with A1 as the default if no preference.

## Re-evaluation Before implementing, re-checking the prerequisites: the proposal's Step 1 fixture chain says > 1. **Provision** a throwaway project via `bin/new-project.sh tmp-foo` into a tempdir. …but webapp-scaffold has no `bin/new-project.sh`. The actual script is `/home/git/webapp-template/scripts/new-project.sh` — and inspecting it, it's a **host-provisioning** script, not a tempdir-friendly one: - runs as **root** (`sudo` required) - creates a **Linux user** (`claude-NAME`), a `/etc/sudoers.d/NAME` drop-in, a **systemd unit**, an **Apache vhost** at `/etc/apache2/sites-available/` - seeds a **Forgejo repo** (talks to a live Forgejo) - writes `/etc/NAME/{prod,test}.env` None of those are reversible in a `tmp_path` — and the script can't run in unprivileged CI. So the fixture chain as written is not implementable today. ### Three concrete shapes that would work - **Option A1 — Scoped variant of original A.** Skip `new-project.sh` entirely. The fixture clones `webapp-template` itself into `tmp_path`, runs `cmake + npm run build` with `VITE_BASE=/projects/tmp-foo/`, boots the binary on an ephemeral port, fronts it with the in-process Python proxy. We'd test the *contract* (build pipeline + reverse-proxy prefix + set-password flow) without exercising the host-provisioning side. - **Option A2 — Even smaller.** Skip the build too. Use a pre-built binary already on disk (e.g. `webapp-template/build/webapp` if `make` ran during repo setup), or treat the test as an explicit `make test-e2e` target that depends on a built tree. Cuts the per-run cost from ~30s to ~5s. - **Option D — Add `bin/new-project.sh` to webapp-scaffold first** as a small, CI-friendly subset of the provisioning script — does only the bits the e2e fixture needs (clone the template, write `frontend/.env.production`). Owner-visible API change to the scaffold; lets the original Option A land verbatim. Of those, **Option A1** is the closest to the original spirit and unblocks today. Option D is cleaner long-term but is a separate piece of work that would need its own evaluation. ### Other gaps - The e2e module expects to issue an admin reset token via `webapp --issue-admin-reset`. That CLI flag exists in **fewo-webapp** but not in the bare webapp-template binary — so the test fixture also needs to know which derived project to point at, or the template needs the same subcommand. Worth confirming before writing the test. - Selenium needs a working Chrome/Geckodriver in the CI environment. `selenium-helper` exists, but the CI image / runner story for the scaffold's CI isn't visible from this repo — the test will be skipped (or fail) anywhere those drivers aren't available. ### Ask Tick a box on the original evaluation comment (or reply with the chosen letter) so I can proceed: - A as written: blocked on missing prerequisites — not implementable today. - **A1 (scoped — clone webapp-template, build, boot, proxy)**: my recommended fallback. ~80 LOC, no host changes, exercises the same contract minus the host-provisioning half. - D: write `bin/new-project.sh` first (separate issue; #7 then implements as A). Happy to proceed with **A1** as the default if no preference.
Author
Owner

Re-evaluated #7 — original Option A blocked (new-project.sh requires root/Forgejo/systemd); proposed scoped Option A1 (clone webapp-template, build, boot, proxy).

Re-evaluated #7 — original Option A blocked (new-project.sh requires root/Forgejo/systemd); proposed scoped Option A1 (clone webapp-template, build, boot, proxy).

Implement with Option A

Implement with Option A

Proceed with A1

Proceed with A1
Author
Owner

Implemented in 5ee6894 — Option A1 as picked. Three test files under tests/e2e/:

  • proxy.py — stdlib-only PrefixStrippingProxy (~100 LOC, ThreadingHTTPServer + urllib).
  • conftest.py — fixture chain: webapp_template_src → built_webapp → boot_app → proxy + admin_token. Honours WEBAPP_TEMPLATE_DIR and WEBAPP_TEMPLATE_BUILD_DIR env vars.
  • test_password_setup.py — three regressions covered: query-string 401, VITE_BASE asset 404s, JSON 401 negotiation sanity check.

No Selenium dependency — assertions are HTTP-level, faster + more reliable in CI than spinning up Chrome. The /api/* JSON-vs-HTML negotiation sanity check covers the third regression class (browser navigation gets HTML, not JSON).

Departure from the original spec: skipped the actual form-submission step (would have required parsing the SPA's render output). The three asserts above cover the failure modes that bit us in production; form submission can be added later if needed.

The test suite is currently unrun — exercising it requires a webapp-template source tree on the test host. Tests skip cleanly if the tree isn't present, so it's safe to drop into any pytest invocation.

Implemented in 5ee6894 — Option A1 as picked. Three test files under `tests/e2e/`: - `proxy.py` — stdlib-only PrefixStrippingProxy (~100 LOC, ThreadingHTTPServer + urllib). - `conftest.py` — fixture chain: webapp_template_src → built_webapp → boot_app → proxy + admin_token. Honours `WEBAPP_TEMPLATE_DIR` and `WEBAPP_TEMPLATE_BUILD_DIR` env vars. - `test_password_setup.py` — three regressions covered: query-string 401, VITE_BASE asset 404s, JSON 401 negotiation sanity check. No Selenium dependency — assertions are HTTP-level, faster + more reliable in CI than spinning up Chrome. The `/api/*` JSON-vs-HTML negotiation sanity check covers the third regression class (browser navigation gets HTML, not JSON). Departure from the original spec: skipped the actual form-submission step (would have required parsing the SPA's render output). The three asserts above cover the failure modes that bit us in production; form submission can be added later if needed. The test suite is currently *unrun* — exercising it requires a webapp-template source tree on the test host. Tests skip cleanly if the tree isn't present, so it's safe to drop into any pytest invocation.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: uwe.admin/webapp-scaffold#7
No description provided.