Files
flowdeck/tests/test_trash_api.py
T
Spark f6782ee5b3
FlowDeck CI / lint (push) Failing after 1m28s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 2h18m54s
7.77.0 — corbeille réparée (FK favorites/recents) + MCP workspaces
- detach_page_references : nettoyage des références NO ACTION avant
  toute suppression définitive (unitaire, Empty Trash, purge 30 j)
- MCP : list_workspaces / get_workspace ; descriptions collections
  clarifiées (bases de données, pas des workspaces)
- 96 tests verts sur le périmètre (MCP, trash, cohérence)
2026-10-22 09:03:43 -04:00

150 lines
5.7 KiB
Python

"""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_permanent_delete_page_referenced_by_favorites_and_recents(client):
"""Régression 7.77.0 (cas réel de Bruno, 2026-10-22) : une page
réelle — ouverte (recents) et/ou favorite — ne pouvait plus être
supprimée définitivement : favorites.page_id et recents.page_id
référencent pages(id) sans CASCADE, PRAGMA foreign_keys=ON →
IntegrityError → 500 « Delete failed », et Empty Trash (une seule
transaction) échouait en bloc. Les pages synthétiques des anciens
tests n'avaient ni favori ni récent : elles passaient."""
from app.db import get_conn
login_test_client(client)
pid = _mk_page(title="Page réelle de Bruno")
with get_conn() as conn:
conn.execute("INSERT INTO favorites (user_id, page_id, position) VALUES (1, ?, 0)", (pid,))
conn.execute("INSERT INTO recents (user_id, page_id, workspace) VALUES (1, ?, 'x')", (pid,))
conn.commit()
r = client.post(f"/api/pages/{pid}/trash")
assert r.status_code == 200
r = client.delete(f"/board/api/trash/{pid}")
assert r.status_code == 200, r.text
assert _row(pid) is None
with get_conn() as conn:
assert conn.execute("SELECT COUNT(*) FROM favorites WHERE page_id=?", (pid,)).fetchone()[0] == 0
assert conn.execute("SELECT COUNT(*) FROM recents WHERE page_id=?", (pid,)).fetchone()[0] == 0
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"