fix(e2e): harden hard_delete against non-cascade user FKs (BUNYIP-248) #270

Merged
nrupard merged 1 commit from fix/bunyip-248-fk-hardening into main 2026-06-29 17:02:03 +02:00
Owner

What

Hardens the e2e disposable-account hard delete (BUNYIP-246) against the non-cascade users(id) foreign keys a future disposable spec could populate. Implements BUNYIP-248.

Why

UserRepository::hard_delete* cleared only audit_logs before DELETE FROM users. Other tables reference users(id) without ON DELETE CASCADE: oauth_authorization_codes.user_id (an OIDC authorize), admin_notifications.user_id / read_by (a feedback submission), and the admin *_by columns. The current three specs populate only audit_logs, but a future disposable spec exercising those flows would FK-block the delete - and because the reaper deletes in bulk, one blocking row would roll back and stall the whole sweep. Non-production / e2e only; production never hard-deletes (it soft-deletes), so there is no production exposure.

Change (hybrid, per the issue decision)

  • Migration 20260629000010_oauth_auth_code_user_cascade.sql: recreate oauth_authorization_codes.user_id as ON DELETE CASCADE - a pending code is the user's own ephemeral artifact, so it disappears with the user. Behavior-preserving for production (the CASCADE only fires on a hard delete, which production never does).
  • Repository: route hard_delete, hard_delete_by_email, and hard_delete_stale_disposable through one clear_deps_and_delete_users helper that clears admin_notifications (user_id rows deleted, read_by nulled) alongside audit_logs, then deletes the users by id. The reaper resolves eligible ids once (transaction-timestamp cutoff) rather than re-evaluating NOW() across two statements.
  • Doc: the remaining admin *_by non-cascade refs are recorded as out of scope (only admin actions populate them; a subscriber disposable cannot), to be handled if an admin-disposable spec ever lands.

Verification

just check-container green: fmt + clippy -D warnings + cargo test --workspace --lib (api 25, domain 222, oci 9, oidc 14). The FK / cleanup behavior runs against a live DB on api startup + e2e, not in --lib tests; no .sqlx regen needed (the methods are runtime queries; the migration is plain DDL). No production behavior change.

#BUNYIP-248

🤖 Generated with Claude Code

## What Hardens the e2e disposable-account hard delete (BUNYIP-246) against the non-cascade `users(id)` foreign keys a future disposable spec could populate. Implements BUNYIP-248. ## Why `UserRepository::hard_delete*` cleared only `audit_logs` before `DELETE FROM users`. Other tables reference `users(id)` without `ON DELETE CASCADE`: `oauth_authorization_codes.user_id` (an OIDC authorize), `admin_notifications.user_id` / `read_by` (a feedback submission), and the admin `*_by` columns. The current three specs populate only `audit_logs`, but a future disposable spec exercising those flows would FK-block the delete - and because the reaper deletes in bulk, one blocking row would roll back and stall the whole sweep. Non-production / e2e only; production never hard-deletes (it soft-deletes), so there is no production exposure. ## Change (hybrid, per the issue decision) - Migration `20260629000010_oauth_auth_code_user_cascade.sql`: recreate `oauth_authorization_codes.user_id` as `ON DELETE CASCADE` - a pending code is the user's own ephemeral artifact, so it disappears with the user. Behavior-preserving for production (the CASCADE only fires on a hard delete, which production never does). - Repository: route `hard_delete`, `hard_delete_by_email`, and `hard_delete_stale_disposable` through one `clear_deps_and_delete_users` helper that clears `admin_notifications` (`user_id` rows deleted, `read_by` nulled) alongside `audit_logs`, then deletes the users by id. The reaper resolves eligible ids once (transaction-timestamp cutoff) rather than re-evaluating `NOW()` across two statements. - Doc: the remaining admin `*_by` non-cascade refs are recorded as out of scope (only admin actions populate them; a subscriber disposable cannot), to be handled if an admin-disposable spec ever lands. ## Verification `just check-container` green: fmt + clippy `-D warnings` + `cargo test --workspace --lib` (api 25, domain 222, oci 9, oidc 14). The FK / cleanup behavior runs against a live DB on api startup + e2e, not in `--lib` tests; no `.sqlx` regen needed (the methods are runtime queries; the migration is plain DDL). No production behavior change. #BUNYIP-248 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(e2e): harden hard_delete against non-cascade user FKs (BUNYIP-248)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 24s
Check / fmt + clippy + build + tests (pull_request) Successful in 8m38s
Create release / Create release from merged PR (pull_request) Has been skipped
e5a36c497a
BUNYIP-246's hard_delete* cleared only audit_logs before DELETE FROM users, but other tables reference users(id) without ON DELETE cascade, so a future disposable spec that performed an OIDC authorize or submitted feedback would FK-block the delete (and a single blocking row would stall the bulk reaper). Hybrid fix:

- migration: oauth_authorization_codes.user_id (a user-owned ephemeral OIDC artifact) gets ON DELETE CASCADE, so it disappears with the user. Production never hard-deletes a user, so this is behavior-preserving there.
- repository: factor the three hard-delete entry points (hard_delete, hard_delete_by_email, hard_delete_stale_disposable) through one clear_deps_and_delete_users helper that also clears admin_notifications (user_id rows deleted, read_by nulled) alongside audit_logs, then deletes the user rows by id. The reaper now resolves eligible ids once (transaction-timestamp cutoff) instead of re-evaluating NOW() across two statements.
- doc: the remaining admin *_by non-cascade refs are recorded as out of scope (only admin actions populate them; a subscriber disposable cannot), to be handled if an admin-disposable spec ever lands.

just check-container green (fmt + clippy + lib tests). No production behavior change.

#BUNYIP-248

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-29 16:55:15 +02:00
nrupard deleted branch fix/bunyip-248-fk-hardening 2026-06-29 17:02:03 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
psa-systems/bunyip!270
No description provided.