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:
2026-08-14 15:19:36 -05:00
parent c99ef08cf1
commit 64a5fd5612
6 changed files with 209 additions and 95 deletions

View File

@@ -146,20 +146,37 @@ docker compose exec db psql -U wpsuite -d wpsuite -c "select id, name from proje
### Automated smoke test
`server/smoketest.py` exercises the whole stack end-to-end (health → project
SOP → Work Package → the AWP issue gate → status → metrics → comments → cascade
cleanup). Stdlib only — no pip/jq.
`server/smoketest.py` exercises the whole stack end-to-end (health → sign-in
project → SOP → Work Package → the AWP issue gate → status → metrics → comments →
archive round trip → cascade cleanup → sign-out). Stdlib only — no pip/jq.
It **signs in first**, because every `/api/` route except `/api/health` requires a
session. Credentials come from the environment so a password stays out of shell
history, and the account must be an **admin**: the run creates a project and deletes
it again, and archiving or deleting one takes Project Admin on it. The script checks
the signed-in role up front and warns if it is too low rather than letting you find
out in the cleanup step.
```bash
export WP_SMOKE_USER=<admin-account>
export WP_SMOKE_PASSWORD='…'
# Through the proxy (use --insecure for a self-signed internal cert):
python3 server/smoketest.py https://wp-suite.company.local --insecure
# Or from inside the api container (hits FastAPI directly):
docker compose exec api python /app/server/smoketest.py http://localhost:8000
# Or from inside the api container (hits FastAPI directly). Pass the vars through:
docker compose exec -e WP_SMOKE_USER -e WP_SMOKE_PASSWORD api \
python /app/server/smoketest.py http://localhost:8000
# Add --keep to leave a demo project in the DB so you can open it in the UI.
# --user / --password override the environment if you'd rather be explicit.
```
Exit codes: **0** all checks passed · **1** one or more checks failed · **2** the run
could not start (host unreachable, or credentials missing or rejected). The last is
kept separate on purpose — "I could not test this" is a different answer from "this is
broken", and automation should not treat them alike.
Exit code 0 and "ALL PASS" means the API, the Python logic, and SQL are all
working. It cleans up after itself (the test project and its SOP/WPs are
deleted via cascade); a single tagged test comment remains (there's no comment