"""Trash — v7.45.5. Trois régressions couvertes : 1. les routes trash n'avaient AUCUNE vérification de session (le CSRF seul ne protège pas : cookie lisible + en-tête forgé) ; 2. la suppression depuis l'éditeur réécrivait ``parent_section='Trash'`` et la restauration ne le remettait pas → page invisible en Library/Recents/Private ; 3. ``POST /board/api/trash/empty`` (purge totale) n'existait pas. """ from tests.conftest import anon_csrf, login_test_client def _mk_page(title="To trash", workspace="bruno/flowdeck", section="Private", parent_id=None): from app.db import get_conn with get_conn() as conn: cur = conn.execute( "INSERT INTO pages (workspace, title, content, content_format, parent_section, parent_id) " "VALUES (?, ?, '', 'blocks', ?, ?)", (workspace, title, section, parent_id), ) conn.commit() return cur.lastrowid def _row(page_id): from app.db import get_conn with get_conn() as conn: return conn.execute( "SELECT parent_section, deleted_at FROM pages WHERE id=?", (page_id,) ).fetchone() def test_trash_requires_session(client): pid = _mk_page() anon_csrf(client) # anonyme mais CSRF valide → on veut le 401, pas le 403 assert client.get("/board/api/trash").status_code == 401 assert client.post(f"/api/pages/{pid}/trash").status_code == 401 assert client.post(f"/board/api/trash/{pid}/restore").status_code == 401 assert client.delete(f"/board/api/trash/{pid}").status_code == 401 assert client.post("/board/api/trash/empty").status_code == 401 # rien n'a été touché row = _row(pid) assert row["deleted_at"] is None def test_soft_delete_then_restore_keeps_section(client): login_test_client(client) pid = _mk_page(title="Restore me") r = client.post(f"/api/pages/{pid}/trash") assert r.status_code == 200 row = _row(pid) assert row["deleted_at"], "la page doit être marquée supprimée" assert row["parent_section"] != "Trash", "parent_section ne doit plus être réécrit" items = client.get("/board/api/trash").json() hit = next((i for i in items if i["id"] == pid), None) assert hit, "la page doit apparaître dans la poubelle" assert hit["deleted_at"] and hit["path"] and hit["icon"] r = client.post(f"/board/api/trash/{pid}/restore") assert r.status_code == 200 row = _row(pid) assert row["deleted_at"] is None assert row["parent_section"] != "Trash" assert not any(i["id"] == pid for i in client.get("/board/api/trash").json()) def test_permanent_delete(client): login_test_client(client) pid = _mk_page(title="Gone for good") client.post(f"/api/pages/{pid}/trash") assert client.delete(f"/board/api/trash/{pid}").status_code == 200 from app.db import get_conn with get_conn() as conn: assert conn.execute("SELECT COUNT(*) FROM pages WHERE id=?", (pid,)).fetchone()[0] == 0 def test_empty_trash_purges_only_trashed(client): login_test_client(client) ids = [_mk_page(title=f"purge-{i}") for i in range(3)] keep = _mk_page(title="keep-me") for pid in ids: client.post(f"/api/pages/{pid}/trash") r = client.post("/board/api/trash/empty") assert r.status_code == 200 assert r.json()["deleted"] == 3 from app.db import get_conn with get_conn() as conn: placeholders = ",".join("?" * len(ids)) assert conn.execute( f"SELECT COUNT(*) FROM pages WHERE id IN ({placeholders})", ids ).fetchone()[0] == 0 assert conn.execute("SELECT COUNT(*) FROM pages WHERE id=?", (keep,)).fetchone()[0] == 1 def test_boot_repairs_restored_rows_marked_trash(client): """Le correctif de db.init_db() remet en 'Private' les pages restaurées que l'ancien soft-delete laissait marquées 'Trash' (donc invisibles partout).""" login_test_client(client) from app.db import get_conn, init_db with get_conn() as conn: cur = conn.execute( "INSERT INTO pages (workspace, title, content, content_format, parent_section) " "VALUES ('', 'ghost', '', 'blocks', 'Trash')" ) conn.commit() pid = cur.lastrowid init_db() with get_conn() as conn: row = conn.execute("SELECT parent_section FROM pages WHERE id=?", (pid,)).fetchone() assert row["parent_section"] == "Private"