test: A32 phase 2h — dashboard bloqué : 44/44 routes à 0 ref (v7.19.0)
FlowDeck CI / test (push) Failing after 3h12m50s
FlowDeck CI / lint (push) Successful in 1m54s
FlowDeck CI / docker (push) Skipped

+5 routes dashboard (fichier test_smoke_uncovered.py à 49 tests) :

- Members POST/PUT/DELETE : invitation de soi-même dans un workspace dédié
  (_own_workspace), rôle admin relu en base, membre supprimé (COUNT=0).
  Quirk documenté : les retours tuple des routes (`{"error": ...}, 400`)
  sont sérialisés FastAPI en tableau + 200 → assert sur `[0]["error"]`.
- upload-folder : validations SEULES (structure absente → 400 « No
  structure provided », JSON cassé → 400 « Invalid structure JSON ») —
  zéro fichier écrit, workspace dédié nettoyé.
- convert-to-database : collection + propriété title + vue table + page en
  content_format='collection' VÉRIFIÉS en base, 404 page inconnue,
  cleanup dans l'ordre FK (pages avant collections — IntegrityError corrigée).

Recoupement final : scan des 44 routes strictement à 0 ref de dashboard.py →
TOUTES exercées. Les 19 résidus du scan sont des faux positifs (paths en
f-string dans les tests : /api/workspace/1/…, f"/api/pages/{id}/…", …)
rapprochés manuellement un par un.

Reste A32 : les 6 routes Gitea d'api.py (issues ×4, créations checklists)
→ stub de transport httpx (effort S).

suite **1086/1086** · `ruff check app tests` OK · docs à jour
This commit is contained in:
2026-10-01 15:24:38 -04:00
parent 0cb476e336
commit 113374e499
7 changed files with 157 additions and 6 deletions
+132
View File
@@ -596,6 +596,138 @@ def test_dashboard_settings_account_update(client):
conn.commit()
def _own_workspace(client) -> int:
"""Workspace appartenant à l'utilisateur de la SESSION (fallback actif)."""
from app.db import get_conn
uid = client.get("/api/users/me").json()["id"]
with get_conn() as conn:
cur = conn.execute(
"INSERT INTO workspaces (name, owner_id) VALUES ('Ws propre A32', ?)", (uid,)
)
ws_id = cur.lastrowid
conn.commit()
return ws_id
def test_dashboard_members_invite_role_remove(client):
"""POST invite → PUT role → DELETE : cycle complet vérifié en base."""
from app.db import get_conn
ws_id = _own_workspace(client)
me = client.get("/api/users/me").json()
try:
r = client.post(
f"/api/workspace/{ws_id}/members",
json={"email": me["login"], "role": "editor"},
)
assert r.status_code == 200 and r.json()["status"] == "ok"
assert r.json()["role"] == "editor"
# rôle invalide → quirk assumé (retour tuple FastAPI : tableau + 200)
inval = client.post(
f"/api/workspace/{ws_id}/members", json={"email": me["login"], "role": "hacker"}
)
assert inval.status_code == 200
assert inval.json()[0]["error"] == "Invalid role"
up = client.put(
f"/api/workspace/{ws_id}/members/{me['id']}", json={"role": "admin"}
)
assert up.status_code == 200 and up.json() == {"status": "ok"}
with get_conn() as conn:
row = conn.execute(
"SELECT role FROM workspace_members WHERE workspace_id=? AND user_id=?",
(ws_id, me["id"]),
).fetchone()
assert row["role"] == "admin"
rm = client.delete(f"/api/workspace/{ws_id}/members/{me['id']}")
assert rm.json() == {"status": "ok"}
with get_conn() as conn:
left = conn.execute(
"SELECT COUNT(*) AS n FROM workspace_members WHERE workspace_id=? AND user_id=?",
(ws_id, me["id"]),
).fetchone()["n"]
assert left == 0
finally:
with get_conn() as conn:
conn.execute("DELETE FROM workspace_members WHERE workspace_id=?", (ws_id,))
conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,))
conn.commit()
def test_dashboard_upload_folder_validation(client):
"""A22 : validations du dépôt de dossier SANS écrire de fichier."""
ws_id = _own_workspace(client)
try:
# structure absente → 400
r = client.post(
"/api/local-workspace/upload-folder", data={"parent_id": ""}
)
assert r.status_code == 400
assert r.json()["error"] == "No structure provided"
# structure JSON cassée → 400
bad = client.post(
"/api/local-workspace/upload-folder", data={"structure": "{pas du json"}
)
assert bad.status_code == 400
assert bad.json()["error"] == "Invalid structure JSON"
finally:
from app.db import get_conn
with get_conn() as conn:
conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,))
conn.commit()
def test_dashboard_convert_to_database(client):
"""Convertit une page en base : collection + propriété Name + vue Table."""
from app.db import get_conn
pid = _seed_page("À convertir A32")
collection_id = None
try:
r = client.post(f"/api/pages/{pid}/convert-to-database", json={})
assert r.status_code == 200, r.text
d = r.json()
assert d["status"] == "converted" and d["view_url"] == f"/pages/{pid}"
collection_id = d["collection_id"]
with get_conn() as conn:
page = conn.execute(
"SELECT content_format, collection_id FROM pages WHERE id=?", (pid,)
).fetchone()
assert page["content_format"] == "collection"
assert page["collection_id"] == collection_id
coll = conn.execute(
"SELECT name FROM collections WHERE id=?", (collection_id,)
).fetchone()
assert coll["name"] == d["name"]
prop = conn.execute(
"SELECT prop_type FROM collection_properties WHERE collection_id=? AND prop_type='title'",
(collection_id,),
).fetchone()
assert prop is not None
view = conn.execute(
"SELECT view_type FROM collection_views WHERE collection_id=?",
(collection_id,),
).fetchone()
assert view["view_type"] == "table"
# page inconnue → 404
assert client.post("/api/pages/999999/convert-to-database", json={}).status_code == 404
finally:
with get_conn() as conn:
# ordre FK : la page référence la collection (pages.collection_id)
conn.execute("DELETE FROM pages WHERE id=?", (pid,))
if collection_id is not None:
conn.execute("DELETE FROM collection_views WHERE collection_id=?", (collection_id,))
conn.execute("DELETE FROM collection_properties WHERE collection_id=?", (collection_id,))
conn.execute("DELETE FROM collections WHERE id=?", (collection_id,))
conn.commit()
def test_dashboard_gitea_workspace_page(client):
r = client.get("/gitea-workspace")
assert r.status_code in (200, 302), r.status_code