test: A32 phase 2h — dashboard bloqué : 44/44 routes à 0 ref (v7.19.0)
+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:
@@ -1,5 +1,24 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.19.0 (2026-10-01) — Audit : A32 : dashboard bloqué (44/44)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- +5 routes `dashboard.py` — `test_smoke_uncovered.py` : 49 tests :
|
||||||
|
· **Members** (POST/PUT/DELETE) : invitation de soi-même dans un workspace
|
||||||
|
dédié, rôle relu en base, membre supprimé (`COUNT=0`) ; **quirk documenté**
|
||||||
|
: les retours `(..., 400)` de ces routes sont sérialisés FastAPI en
|
||||||
|
tableau + 200 (`[{"error": "Invalid role"}, 400]`)
|
||||||
|
· `upload-folder` : **validations seules** (structure absente → 400,
|
||||||
|
JSON cassé → 400) — zéro fichier écrit sur disque, 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** (page avant collection)
|
||||||
|
- **Recoupement final** : scan des 44 routes strictement à 0 ref de
|
||||||
|
`dashboard.py` → **toutes exercées** (19 faux positifs résiduels =
|
||||||
|
paths en f-string dans les tests, rapprochés manuellement)
|
||||||
|
- Suite complète : **1086/1086**
|
||||||
|
|
||||||
## v7.18.0 (2026-10-01) — Audit : A32 phase 2g (dashboard +13)
|
## v7.18.0 (2026-10-01) — Audit : A32 phase 2g (dashboard +13)
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.18.0 (audit — A32 phase 2g : dashboard +13, cycle items/tags) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.19.0 (audit — A32 : dashboard 44/44 routes à 0 ref couvertes) | **Statut**: EN COURS 🔄
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.18.0",
|
version="7.19.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.18.0"
|
"version": "7.19.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
@@ -596,6 +596,138 @@ def test_dashboard_settings_account_update(client):
|
|||||||
conn.commit()
|
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):
|
def test_dashboard_gitea_workspace_page(client):
|
||||||
r = client.get("/gitea-workspace")
|
r = client.get("/gitea-workspace")
|
||||||
assert r.status_code in (200, 302), r.status_code
|
assert r.status_code in (200, 302), r.status_code
|
||||||
|
|||||||
Reference in New Issue
Block a user