fix(migrations): dropping the demo also takes its reset job's interval-change audit rows
0098 deleted the reset job's run rows but not the scheduler_interval rows that changing its interval wrote; with the job's label gone they rendered as a raw id. The delete now takes every job-targeted row for it, and a test seeds both kinds below 0098 and upgrades (red on the old filter). The test has its own file so the removed-features guard can name it.
This commit is contained in:
@@ -5,9 +5,9 @@ schedule — goes with its code. Prod held the demo user and 10 whitelist rows.
|
||||
|
||||
The upgrade deletes the demo user first (its playlists, states and subscriptions cascade; audit and
|
||||
quota rows keep their history with the actor nulled, as for any deleted account), then the traces
|
||||
that only the demo code could render: its audit rows (whitelist edits, manual resets, the scheduled
|
||||
reset's job runs), the reset job's interval override and its completion notices. Nothing is
|
||||
migrated anywhere; the nightly backup holds the rows.
|
||||
that only the demo code could render: its audit rows (whitelist edits, manual resets, and every row
|
||||
about the reset job — its runs and its interval changes), the reset job's interval override and its
|
||||
completion notices. Nothing is migrated anywhere; the nightly backup holds the rows.
|
||||
|
||||
The downgrade restores the schema as it stood (0014), empty: the demo user is created lazily on its
|
||||
first sign-in, so an empty `demo_whitelist` is the whole of its state.
|
||||
@@ -26,7 +26,7 @@ def upgrade() -> None:
|
||||
op.execute("DELETE FROM users WHERE is_demo")
|
||||
op.execute(
|
||||
"DELETE FROM audit_log WHERE action IN ('demo_add', 'demo_remove', 'demo_reset')"
|
||||
" OR (action = 'job_run' AND target_id = 'demo_reset')"
|
||||
" OR (target_type = 'job' AND target_id = 'demo_reset')"
|
||||
)
|
||||
op.execute("DELETE FROM scheduler_settings WHERE job_id = 'demo_reset'")
|
||||
op.execute("DELETE FROM notifications WHERE data->>'job_id' = 'demo_reset'")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Migration 0098's data cleanup, DB-lane.
|
||||
|
||||
The schema half of every migration is checked by test_migrations.py; this one also deletes rows, and
|
||||
a delete that misses a row is invisible there. Its own file so the removed-features guard can name it.
|
||||
"""
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
|
||||
|
||||
def test_dropping_the_demo_takes_every_audit_row_about_its_reset_job(db):
|
||||
# The reset job's interval could be changed from the scheduler page like any job's; once its
|
||||
# name is gone from the UI, a left-behind row would render as a raw job id.
|
||||
cfg = Config("alembic.ini")
|
||||
command.downgrade(cfg, "0097_drop_messages")
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO audit_log (action, target_type, target_id, created_at) VALUES"
|
||||
" ('job_run', 'job', 'demo_reset', now()),"
|
||||
" ('scheduler_interval', 'job', 'demo_reset', now()),"
|
||||
" ('scheduler_interval', 'job', 'sync_feed', now())"
|
||||
)
|
||||
)
|
||||
finally:
|
||||
command.upgrade(cfg, "head")
|
||||
with engine.connect() as conn:
|
||||
left = conn.execute(
|
||||
text("SELECT action, target_id FROM audit_log WHERE target_type = 'job' ORDER BY id")
|
||||
).all()
|
||||
assert [tuple(r) for r in left] == [("scheduler_interval", "sync_feed")]
|
||||
@@ -9,7 +9,8 @@ import { describe, expect, it } from "vitest";
|
||||
//
|
||||
// Not scanned: the Alembic migrations (0014 added the account and 0098 removed it, and both stay true
|
||||
// as history) and the release notes, which record what shipped when. `audit_metrics.py` counts the
|
||||
// footprint by this same word, so it names it on purpose.
|
||||
// footprint by this same word, and test_migration_0098.py tests the removal itself, so both name it
|
||||
// on purpose.
|
||||
|
||||
const REPO = new URL("../../../", import.meta.url);
|
||||
const ROOTS = ["backend/app", "backend/tests", "frontend/src", "frontend/e2e", "scripts"];
|
||||
@@ -18,6 +19,7 @@ const EXTENSIONS = /\.(py|ts|tsx|js|mjs|json|css|md|sh)$/;
|
||||
const ALLOWED = new Set([
|
||||
"frontend/src/lib/removedFeatures.contract.test.ts",
|
||||
"scripts/audit_metrics.py",
|
||||
"backend/tests/test_migration_0098.py",
|
||||
]);
|
||||
// "demo" as a word or an identifier part (`is_demo`, `DemoBanner`, `tryDemo`) — not "demote" (a role
|
||||
// change) and not "Demokratikus" (a country's Hungarian name in the vocab).
|
||||
|
||||
Reference in New Issue
Block a user