diff --git a/backend/alembic/versions/0098_drop_demo_account.py b/backend/alembic/versions/0098_drop_demo_account.py index 7cbcd8df..d4b26a9b 100644 --- a/backend/alembic/versions/0098_drop_demo_account.py +++ b/backend/alembic/versions/0098_drop_demo_account.py @@ -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'") diff --git a/backend/tests/test_migration_0098.py b/backend/tests/test_migration_0098.py new file mode 100644 index 00000000..578f8ee0 --- /dev/null +++ b/backend/tests/test_migration_0098.py @@ -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")] diff --git a/frontend/src/lib/removedFeatures.contract.test.ts b/frontend/src/lib/removedFeatures.contract.test.ts index edbd1dd5..df509aff 100644 --- a/frontend/src/lib/removedFeatures.contract.test.ts +++ b/frontend/src/lib/removedFeatures.contract.test.ts @@ -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).