Make the smoke test sign in; enforce SQLite foreign keys
Closes known issue 3. server/smoketest.py predated the login portal and had no
login step at all, so auth_gate refused every route after /api/health and the
documented way to verify a deploy reported a wall of failures against a healthy
stack.
- Signs in first, holding the session in an http.cookiejar on a shared opener.
urlopen() has no cookie support, which is why the session was dropped.
- Credentials from WP_SMOKE_USER / WP_SMOKE_PASSWORD, or --user/--password, so
a password need not land in shell history. Refuses to start without them
rather than running headlong into 401s.
- Checks the signed-in role up front and warns when it cannot archive or delete
a project, instead of failing six checks later for an unexplained reason.
- New exit code 2 for "could not run" (unreachable, or credentials missing or
rejected), kept distinct from 1 "ran and found problems".
- Also asserts the session is accepted on an authenticated route and refused
after sign-out; signs out at the end so a run on a shared host leaves none.
The working smoke test immediately caught a real bug: SQLite ships with foreign
keys disabled and the pragma is per-connection, so every ondelete="CASCADE" was
silently a no-op on dev while working on Postgres. Deleting a project orphaned its
SOPs, work packages and membership rows; deleting a user orphaned theirs. db.py
now sets PRAGMA foreign_keys=ON for SQLite, so dev matches production.
Enforcing them exposed two things that had been getting away with it:
- create_user adds an account and its ProjectMember rows in one flush, and the
ORM takes flush order from relationship() declarations. models.py has none by
design, so it emitted the child INSERT first and the database rejected it.
Fixed with a db.flush() after the account, and documented at the top of
models.py so the next same-flush pair does not rediscover it. The other three
call sites already commit the parent first.
- A write aimed at a since-deleted project used to leave an orphan row; with FKs
enforced it would have been an IntegrityError surfacing as a 500, which the
browser outbox retries forever (it only retires 4xx). require_project_writable
now refuses a vanished project with 409, like the archived case beside it.
Verified: smoke test 27/27 exit 0 against a live server (the cascade assertion now
passes on SQLite, which is what used to fail); credentials missing and credentials
rejected both abort cleanly with exit 2 and no stray PASS lines; a project_user run
warns up front and fails as described. Scope tests 93/93, live HTTP checks 29/29,
static JS checks 33/33. No orphan rows left in the database afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,7 @@ Close an entry by deleting it in the same commit that fixes it.
|
||||
|---|-------|----------|--------|--------|
|
||||
| 1 | XSS via SOP discipline names in the WP creator | Medium (internal), High if externally reachable | 2026-08-05 | Open |
|
||||
| 2 | Archived projects: the two big apps don't grey out their own controls | Low | 2026-08-05 | Open |
|
||||
| 3 | `server/smoketest.py` cannot authenticate — every run fails with 401 | Medium | 2026-08-05 | Open |
|
||||
| 4 | User Directory and nav drawer have not been run in a browser | Low (verification gap, not a known defect) | 2026-08-05 | Open |
|
||||
| 3 | User Directory and nav drawer have not been run in a browser | Low (verification gap, not a known defect) | 2026-08-05 | Open |
|
||||
|
||||
---
|
||||
|
||||
@@ -166,72 +165,7 @@ app bar — and therefore the banner — is deliberately skipped.
|
||||
|
||||
---
|
||||
|
||||
## 3. `server/smoketest.py` cannot authenticate — every run fails with 401
|
||||
|
||||
**Files:** `server/smoketest.py` · documented in `DEPLOYMENT.md` §"Automated smoke
|
||||
test" (line ~147)
|
||||
**Predates:** the login portal. The script was written against an open API and was
|
||||
never updated when authentication landed.
|
||||
|
||||
### What is wrong
|
||||
|
||||
The script has no login step — no call to `/api/auth/login`, no cookie jar, no
|
||||
credential arguments. `auth_gate` (`server/app.py`) refuses every `/api/` route
|
||||
without a session cookie, so every check after the first fails:
|
||||
|
||||
```
|
||||
- create project (status=401)
|
||||
- fetch project by id (status=401)
|
||||
...
|
||||
Result: FAIL
|
||||
```
|
||||
|
||||
Only `/api/health` passes, because it is on `auth._EXEMPT_EXACT`.
|
||||
|
||||
### What it costs
|
||||
|
||||
**The documented end-to-end verification path does not work, and hasn't for some
|
||||
time.** `DEPLOYMENT.md` presents this as the way to prove "NGINX → FastAPI →
|
||||
PostgreSQL all work", including a `docker compose exec` invocation for use inside the
|
||||
api container. Anyone following it after a deploy gets a wall of 401s and has to work
|
||||
out for themselves whether the stack is broken or the script is.
|
||||
|
||||
The stack itself is fine, and there is a working equivalent: the **Admin Console →
|
||||
End-to-end smoke test** card runs the same sequence from the browser, where the
|
||||
session cookie already exists. `html/admin.html` describes it as mirroring
|
||||
`smoketest.py`, which is now the only place that sequence actually runs.
|
||||
|
||||
Rated Medium rather than Low because it is a verification tool that reports failure
|
||||
on a healthy system — the failure mode most likely to be believed and acted on.
|
||||
|
||||
### Why it is still open
|
||||
|
||||
It was found while testing unrelated work (the User Directory) and is not a defect in
|
||||
the product. Fixing it means choosing how the script gets credentials, which is a
|
||||
small design decision about a deploy-time tool rather than a code fix, and it should
|
||||
not ride along inside a feature branch.
|
||||
|
||||
### What closing it takes
|
||||
|
||||
Small — under an hour, stdlib only, matching the script's existing constraint.
|
||||
|
||||
1. Add `--user` / `--password` arguments, defaulting to `WP_SMOKE_USER` /
|
||||
`WP_SMOKE_PASSWORD` from the environment so the runbook does not put a password on
|
||||
a command line.
|
||||
2. Install an `http.cookiejar.CookieJar` on the opener in `call()`, then POST
|
||||
`/api/auth/login` before the first check and assert it returned 200. There is a
|
||||
working reference for both steps in the throwaway harness written for this branch
|
||||
(`Client` in the scratch `http_check.py`), or in `html/admin.js`'s in-browser
|
||||
version of the same flow.
|
||||
3. Note in `DEPLOYMENT.md` that the account needs access to the project the script
|
||||
creates — simplest is an admin account, since a `project_user` cannot create a
|
||||
project.
|
||||
4. Decide whether a missing credential is a hard failure or a skip with a clear
|
||||
message. A silent 401 wall is what caused this entry.
|
||||
|
||||
---
|
||||
|
||||
## 4. User Directory and nav drawer have not been run in a browser
|
||||
## 3. User Directory and nav drawer have not been run in a browser
|
||||
|
||||
**Files:** `html/users.html`, `html/users.js`, `html/wp-sidenav.js`,
|
||||
`html/wp-sidenav.css`, `html/console.css`
|
||||
|
||||
Reference in New Issue
Block a user