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:
@@ -9,6 +9,21 @@ The full client document for a SOP or WP is kept verbatim in a JSON `data`
|
||||
column, with the most-queried fields promoted to real columns for listing and
|
||||
filtering. IDs are short strings (client- or server-generated) so the browser
|
||||
can upsert without round-tripping a sequence.
|
||||
|
||||
NO relationship() DECLARATIONS, ON PURPOSE — and one consequence to know about.
|
||||
Every link here is a plain column plus a ForeignKey; nothing is navigable as
|
||||
`project.work_packages`. Queries are explicit selects, which suits an API that
|
||||
mostly reads one scoped list at a time and never wants a lazy load firing inside
|
||||
a response.
|
||||
|
||||
The consequence: SQLAlchemy's unit of work derives FLUSH ORDER from relationships,
|
||||
not from ForeignKey metadata. With none declared it has no dependency edge to
|
||||
follow, so if you add a parent and its child in the SAME flush it may emit the
|
||||
child's INSERT first and the database will reject it. Both engines enforce foreign
|
||||
keys (Postgres always; SQLite since db.py sets `PRAGMA foreign_keys=ON`), so this
|
||||
is a real error, not a dev-only quirk. Call `db.flush()` after adding the parent —
|
||||
see `create_user` in app.py, which creates an account and its ProjectMember rows
|
||||
together.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
Reference in New Issue
Block a user