`backend/tests/` follows a house rule of copying production SQL character-for-
character rather than calling `src/`, because the crate is a binary and nothing
in it is importable from an integration test. For pinning behaviour that already
existed that is a defensible trade. Applied to a NEW fix whose only coverage is
the copy, it proves nothing: the fix and its test become two independent
implementations, and deleting the fix leaves the test green.
`audit_names.rs` did exactly that. It never called `audit::record` — it
reimplemented `resolve_names` and the INSERT inside the test file, down to a
hardcoded `.bind("host")`, and then asserted `actor_role == "host"` against its
own literal. That assertion could not fail for any change to the code it named,
and grep confirmed there was no other coverage of the audit-name work anywhere.
Moved into `#[cfg(test)]` inside `services/audit.rs`, where the real function IS
callable. CI already runs `cargo test --all-features` with a live DATABASE_URL,
so `#[sqlx::test]` works there; verified all four run and pass. The role
assertion now compares against `UserRole::as_str()` itself rather than a literal,
so it tracks a rename instead of pretending to, plus an explicit `assert_ne!`
against the Debug spelling.
Also:
- `retry-after-release.spec.ts` filtered the feed on `u.id === original.id` to
prove "no second row was created". A duplicate gets a fresh uuid and could
never match, so the filter yielded exactly 1 whether the gallery held one copy
or five. Counts by uploader now, with the original's identity asserted
separately. (The rest of that spec is sound — its 403 control and replay-id
check both fail if the header fast-path is reverted.)
- `upload_after_release_commits_sees_the_lock_and_is_rejected` claimed the
handler answers `UploadsLocked`. It answers `GalleryReleased` since the check
order was inverted on this branch, and the test asserts no variant at all.
Documented what it actually covers (the locked READ) and where the ordering IS
covered (two e2e specs).
- Two `// SRC:` pointers had drifted ~130 lines into unrelated code, which is how
a hand-copied fixture silently stops matching its original. Now named, not
numbered.
- `emptyOutDir: false` claimed a failed viewer build "leaves the last good
artifact in place". True for the `generateBundle` error, false for the newer
`writeBundle` assertion, which fires after Vite has already written the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last-operator guard ran on the pool, before the transaction opened. Two
hosts deleting themselves at the same moment each saw the other, both passed,
and the event was left with nobody who can moderate, nobody who can release
the gallery, and no way to appoint anyone — because appointing requires a
host. Not recoverable from inside the app.
The fix is a transaction-scoped ADVISORY lock, and the two obvious
alternatives are both worse:
* `FOR UPDATE` on the other operators' rows DEADLOCKS. Each deleter locks the
other's row and then tries to delete its own, so Postgres resolves it by
killing one. The invariant survives; the loser gets a 500 instead of the
sentence explaining what to do. My first attempt did exactly this, and the
test caught it.
* Locking the `event` row serialises cleanly but inverts the lock order every
moderation path takes (upload/user rows first, event last). That is an ABBA
against a path that runs constantly during the event, traded for one that
runs approximately never.
An advisory lock is a separate lock space, so it cannot interact with the
row-lock graph at all, and it is released when the transaction ends. The loser
waits, counts zero once the winner's row is gone, and is refused with the
sentence it should have got.
The test is a genuine concurrency test — it spawns the second deleter and
asserts it unblocks to see no remaining operator. It fails against the
pre-check-outside-the-transaction version and against the FOR UPDATE version.
The backend had never been run through rustfmt. Doing it in one mechanical pass (134 files)
so no future functional diff is buried under formatting churn, then gating `cargo fmt
--check` in checks.yml so it stays clean.
Formatting only — no logic, SQL, or behaviour changed. Verified after the reformat:
cargo test 56 passed, clippy --all-targets -D warnings clean, cargo fmt --check clean.
This is the deferred cleanup noted when CI's Format step was first left out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All 40 backend tests were pure-function tests — not one line of SQL ran under `cargo test`,
even though the riskiest code in the repo is SQL. Add 12 DB-backed `#[sqlx::test]` tests
(fresh throwaway DB per test, real migrations) pinning the invariants that code is
load-bearing for, each mutation-verified to fail on broken code:
export_epoch.rs (8): epoch is post-increment on release (a worker born pre-increment is
inert); epoch strictly monotonic across reopen; a retired-epoch worker's writes are
no-ops; export_current is EXACTLY the invariant (8-case table); the ViewerOnly
carry-forward carries a done ZIP forward AND matches nothing when the ZIP is unfinished
(the af997a8 strand bug); boot recovery doesn't clobber a live ZIP.
upload_concurrency.rs (4): the atomic quota UPDATE stops two stale-snapshot uploads from
both committing (sequential AND concurrent-tx via EPQ); the FOR SHARE upload lock
serializes against release, blocking a photo from committing after the export snapshot.
Deleting FOR SHARE, or the carry-forward's `status='done'`, each makes a test fail.
Test isolation: TRUNCATE now also resets disk_cache and sse_tickets. The stale disk
reading was harmless only while quotas were globally off in e2e (they no longer are — see
the new quota spec), i.e. two holes were masking each other.
clippy: `cargo clippy --all-targets -- -D warnings` now passes (it did not). Deleted the
dead jobs.rs (an unused BackgroundJob sketch) and unused model methods; `#[allow(dead_code)]`
+ comment on the sqlx FromRow field-sets that ARE populated by the DB. No SQL or logic
changed. `cargo test` requires DATABASE_URL by design.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>