test: A32 phase 2e — dashboard +9 routes, comptes A2/A3 (v7.16.0)
Cumul dashboard : 27 → 36 des 63 routes. 6 nouveaux tests (fichier à 38) :
- /accounts + /accounts/settings : 200 HTML et « password_hash » ABSENT du
rendu (whitelist A2 vérifiée côté page)
- PUT /api/user/profile : persistance relue en base, restauration finally
- PUT /api/user/password : 403 « current password is incorrect » (A3 — la
session seule ne change pas le mdp) + quirk assumé documenté : la longueur
est validée AVANT l'auth et répond 200 + message
- POST /api/user/token : format fd_ + 64 hex ; ligne user_tokens nettoyée
- DELETE /api/user/forge/{provider} : {"status": "ok"}
- PUT /api/settings/account : full_name/email persistés + 400 sur mdp court,
restauration finally
- POST /api/workspaces/1/select : Set-Cookie flowdeck_workspace vérifié ;
GET /api/local-workspace/breadcrumb : shape liste
Reste A32 : dashboard 27 routes (fichiers/avatars/local-workspace/collections)
+ 6 routes Gitea d'api.py (stub transport httpx).
suite **1075/1075** · `ruff check app tests` OK · docs à jour
This commit is contained in:
@@ -1,5 +1,26 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.16.0 (2026-10-01) — Audit : A32 phase 2e (dashboard +9, comptes)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- +9 routes `dashboard.py` (cumul **27→36 sur 63**) :
|
||||||
|
· `/accounts` + `/accounts/settings` : 200 HTML, **`password_hash` absent**
|
||||||
|
du rendu (whitelist A2 vérifiée côté page)
|
||||||
|
· `PUT /api/user/profile` : persistance **relue en base**, valeur d'origine
|
||||||
|
restaurée en `finally`
|
||||||
|
· `PUT /api/user/password` : **403 « current password is incorrect »** (A3 —
|
||||||
|
la session seule ne change pas le mdp) + quirk assumé et documenté :
|
||||||
|
longueur validée AVANT auth → 200 + message d'erreur
|
||||||
|
· `POST /api/user/token` : format `fd_` + 64 hex, ligne `user_tokens`
|
||||||
|
nettoyée en `finally`
|
||||||
|
· `DELETE /api/user/forge/{provider}` : `{"status": "ok"}`
|
||||||
|
· `PUT /api/settings/account` : full_name/email persistés + **400 sur mdp
|
||||||
|
court** (validateur), restauration en `finally`
|
||||||
|
· `POST /api/workspaces/{id}/select` : `Set-Cookie flowdeck_workspace`
|
||||||
|
vérifié ; `GET /api/local-workspace/breadcrumb` : shape liste
|
||||||
|
- `test_smoke_uncovered.py` : 38 tests ; suite complète **1075/1075**
|
||||||
|
|
||||||
## v7.15.0 (2026-10-01) — Audit : A32 phase 2d (dashboard +10 routes)
|
## v7.15.0 (2026-10-01) — Audit : A32 phase 2d (dashboard +10 routes)
|
||||||
|
|
||||||
### 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.15.0 (audit — A32 phase 2d : dashboard +10 routes) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.16.0 (audit — A32 phase 2e : dashboard +9 routes, comptes A2/A3) | **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.15.0",
|
version="7.16.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.15.0"
|
"version": "7.16.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
@@ -500,3 +500,109 @@ def test_dashboard_workspace_members_list(client):
|
|||||||
r = client.get("/api/workspace/1/members")
|
r = client.get("/api/workspace/1/members")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert isinstance(r.json()["members"], list)
|
assert isinstance(r.json()["members"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_account_pages_html(client):
|
||||||
|
"""Pages HTML /accounts et /accounts/settings (whitelist A2 vérifiée)."""
|
||||||
|
for path in ("/accounts", "/accounts/settings"):
|
||||||
|
r = client.get(path)
|
||||||
|
assert r.status_code == 200, path
|
||||||
|
assert "text/html" in r.headers["content-type"], path
|
||||||
|
# la page ne doit plus contenir de hash (colonnes whitelistées en A29)
|
||||||
|
assert "password_hash" not in client.get("/accounts").text
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_user_profile_update_persisted(client):
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
uid = client.get("/api/users/me").json()["id"]
|
||||||
|
with get_conn() as conn:
|
||||||
|
prev = conn.execute("SELECT full_name FROM users WHERE id=?", (uid,)).fetchone()[
|
||||||
|
"full_name"
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
r = client.put("/api/user/profile", json={"full_name": "Smoke A32"})
|
||||||
|
assert r.status_code == 200 and r.json() == {"status": "ok"}
|
||||||
|
with get_conn() as conn:
|
||||||
|
now = conn.execute(
|
||||||
|
"SELECT full_name FROM users WHERE id=?", (uid,)
|
||||||
|
).fetchone()["full_name"]
|
||||||
|
assert now == "Smoke A32"
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("UPDATE users SET full_name=? WHERE id=?", (prev, uid))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_password_change_gated_by_current(client):
|
||||||
|
"""A3 : sans mdp actuel correct, la session ne permet PAS de changer le mdp."""
|
||||||
|
court = client.put("/api/user/password", json={"password": "abc"})
|
||||||
|
# quirk assumé : la longueur est validée AVANT auth et répond 200 + erreur
|
||||||
|
assert "at least 6 characters" in court.json().get("error", "")
|
||||||
|
faux = client.put(
|
||||||
|
"/api/user/password",
|
||||||
|
json={"password": "nouveaumdp1", "current_password": "pas-le-bon"},
|
||||||
|
)
|
||||||
|
assert faux.status_code == 403
|
||||||
|
assert "incorrect" in faux.json()["detail"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_user_token_and_forge_disconnect(client):
|
||||||
|
import re as _re
|
||||||
|
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
uid = client.get("/api/users/me").json()["id"]
|
||||||
|
try:
|
||||||
|
r = client.post("/api/user/token")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert _re.fullmatch(r"fd_[0-9a-f]{64}", r.json()["token"])
|
||||||
|
r2 = client.delete("/api/user/forge/gitea")
|
||||||
|
assert r2.status_code == 200 and r2.json() == {"status": "ok"}
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM user_tokens WHERE gitea_user_id=?", (uid,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_settings_account_update(client):
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
uid = client.get("/api/users/me").json()["id"]
|
||||||
|
with get_conn() as conn:
|
||||||
|
prev = conn.execute(
|
||||||
|
"SELECT full_name, email FROM users WHERE id=?", (uid,)
|
||||||
|
).fetchone()
|
||||||
|
try:
|
||||||
|
r = client.put(
|
||||||
|
"/api/settings/account",
|
||||||
|
json={"full_name": "Compte A32", "email": "[email protected]"},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
with get_conn() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT full_name, email FROM users WHERE id=?", (uid,)
|
||||||
|
).fetchone()
|
||||||
|
assert row["full_name"] == "Compte A32" and row["email"] == "[email protected]"
|
||||||
|
# mdp trop court → 400 (validateur, pas de changement)
|
||||||
|
court = client.put("/api/settings/account", json={"password": "abc"})
|
||||||
|
assert court.status_code == 400
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE users SET full_name=?, email=? WHERE id=?",
|
||||||
|
(prev["full_name"], prev["email"], uid),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_workspace_select_and_breadcrumb(client):
|
||||||
|
# select : cookie positionné + shape
|
||||||
|
r = client.post("/api/workspaces/1/select")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"status": "ok", "workspace_id": 1}
|
||||||
|
assert "flowdeck_workspace" in r.headers.get("set-cookie", "")
|
||||||
|
# breadcrumb d'un dossier inexistant → liste (shape)
|
||||||
|
b = client.get("/api/local-workspace/breadcrumb", params={"folder": 999999})
|
||||||
|
assert b.status_code == 200
|
||||||
|
assert isinstance(b.json()["breadcrumb"], list)
|
||||||
|
|||||||
Reference in New Issue
Block a user