test: A32 phase 2g — dashboard +13 routes, cycles items/tags (v7.18.0)
Cumul dashboard : 43 → 56 des 63 routes. 4 nouveaux tests (fichier à 46) :
- GET /gitea-workspace : page HTML (200 ou redirection propre)
- workspace/projects GET+POST : shape {builtin, gitea, github} avec
github == [] ; projet créé RETROUVÉ dans builtin ; quirk « error » sans nom
; nettoyage (DELETE page)
- Cycle items local-workspace (5 routes) : POST création (titre relu),
PUT rename (relu en base), PUT move, DELETE soft-delete (deleted_at relu),
POST restore (deleted_at NULL relu) — nettoyage finally
- Cycle tags d'item (5 routes) : POST (urgenta32 lowercasé), tags de l'item,
liste workspace, search (shape), suppression vérifiée. Utilisateur DÉDIÉ +
workspace créé dans le test (le endpoint /api/local-workspace/tags exige un
workspace actif : fallback « premier workspace du user » — on n'attache pas
ce workspace à l'utilisateur fixture partagé), tout est nettoyé.
Reste A32 : dashboard 7 routes (members invite/role/unsubscribe,
upload-folder, convert-to-database) + 6 routes Gitea d'api.py (stub httpx).
suite **1083/1083** · `ruff check app tests` OK · docs à jour
This commit is contained in:
@@ -1,5 +1,25 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.18.0 (2026-10-01) — Audit : A32 phase 2g (dashboard +13)
|
||||
|
||||
### Tests
|
||||
|
||||
- +13 routes `dashboard.py` (cumul **43→56 sur 63**) —
|
||||
`test_smoke_uncovered.py` : 46 tests :
|
||||
· `/gitea-workspace` : page HTML (200 ou redirection propre)
|
||||
· `workspace/projects` GET+POST : shape `{builtin, gitea, github}` avec
|
||||
`github == []`, projet créé **retrouvé dans builtin**, quirk `error` sans
|
||||
nom, nettoyage
|
||||
· **Cycle items local-workspace** (5 routes) : création → renommage **relu en
|
||||
base** → move → soft-delete (`deleted_at` **relu**) → restore
|
||||
(`deleted_at IS NULL` **relu**), nettoyage
|
||||
· **Cycle tags d'item** (5 routes) : POST (nom lowercasé), tags de l'item,
|
||||
liste workspace, search (shape), suppression vérifiée. Utilisateur +
|
||||
workspace **créés dans le test** : `/api/local-workspace/tags` a besoin
|
||||
d'un workspace actif (fallback « premier workspace du user ») — on ne le
|
||||
fait pas dépendre de l'utilisateur fixture partagé, tout est nettoyé
|
||||
- Suite complète : **1083/1083**
|
||||
|
||||
## v7.17.0 (2026-10-01) — Audit : A32 phase 2f (dashboard +7, garde-fous A16)
|
||||
|
||||
### Tests
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
||||
# WORKLOAD — FlowDeck Notion Clone
|
||||
|
||||
> **Début**: 2026-07-08 | **Version**: v7.17.0 (audit — A32 phase 2f : dashboard +7, garde-fous A16) | **Statut**: EN COURS 🔄
|
||||
> **Début**: 2026-07-08 | **Version**: v7.18.0 (audit — A32 phase 2g : dashboard +13, cycle items/tags) | **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`
|
||||
|
||||
## Avancement Global
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.17.0",
|
||||
version="7.18.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FlowDeck",
|
||||
"version": "7.17.0"
|
||||
"version": "7.18.0"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
|
||||
@@ -596,6 +596,134 @@ def test_dashboard_settings_account_update(client):
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_dashboard_gitea_workspace_page(client):
|
||||
r = client.get("/gitea-workspace")
|
||||
assert r.status_code in (200, 302), r.status_code
|
||||
if r.status_code == 200:
|
||||
assert "text/html" in r.headers["content-type"]
|
||||
|
||||
|
||||
def test_dashboard_workspace_projects_crud(client):
|
||||
from app.db import get_conn
|
||||
|
||||
listing = client.get("/api/workspace/projects")
|
||||
assert listing.status_code == 200
|
||||
d = listing.json()
|
||||
assert isinstance(d["builtin"], list) and isinstance(d["gitea"], list)
|
||||
assert d["github"] == []
|
||||
created = client.post("/api/workspace/projects", json={"name": "Projet A32"})
|
||||
assert created.status_code == 200
|
||||
body = created.json()
|
||||
assert body["name"] == "Projet A32" and body["forge"] == "builtin"
|
||||
try:
|
||||
# la page créée est un projet racine → elle apparaît dans builtin
|
||||
names = [p["name"] for p in client.get("/api/workspace/projects").json()["builtin"]]
|
||||
assert "Projet A32" in names
|
||||
vide = client.post("/api/workspace/projects", json={"name": " "})
|
||||
assert "error" in vide.json() # quirk : 200 + message
|
||||
finally:
|
||||
with get_conn() as conn:
|
||||
conn.execute("DELETE FROM pages WHERE id=?", (body["id"],))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_dashboard_local_workspace_item_lifecycle(client):
|
||||
"""POST → rename → move → soft-delete → restore (5 routes, vérifié en base)."""
|
||||
from app.db import get_conn
|
||||
|
||||
created = client.post("/api/local-workspace/items", json={"name": "Fichier A32"})
|
||||
assert created.status_code == 200
|
||||
item = created.json()
|
||||
iid = item["id"]
|
||||
try:
|
||||
assert item["name"] == "Fichier A32" and item["type"] == "page"
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT title FROM pages WHERE id=?", (iid,)).fetchone()
|
||||
assert row["title"] == "Fichier A32"
|
||||
|
||||
assert client.put(
|
||||
f"/api/local-workspace/items/{iid}", json={"name": "Renommé A32"}
|
||||
).json() == {"status": "ok"}
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT title FROM pages WHERE id=?", (iid,)).fetchone()
|
||||
assert row["title"] == "Renommé A32"
|
||||
|
||||
assert client.put(
|
||||
f"/api/local-workspace/items/{iid}/move", json={"parent_id": None}
|
||||
).json() == {"status": "ok"}
|
||||
|
||||
assert client.delete(f"/api/local-workspace/items/{iid}").json() == {"status": "ok"}
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT deleted_at FROM pages WHERE id=?", (iid,)).fetchone()
|
||||
assert row["deleted_at"] is not None
|
||||
|
||||
assert client.post(f"/api/local-workspace/items/{iid}/restore").json() == {
|
||||
"status": "ok"
|
||||
}
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT deleted_at FROM pages WHERE id=?", (iid,)).fetchone()
|
||||
assert row["deleted_at"] is None
|
||||
finally:
|
||||
with get_conn() as conn:
|
||||
conn.execute("DELETE FROM pages WHERE id=?", (iid,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_dashboard_item_tags_lifecycle(client):
|
||||
"""POST tag item → lecture → liste workspace → search → suppression.
|
||||
|
||||
L'utilisateur et son workspace sont créés DANS le test : /tags a besoin
|
||||
d'un workspace actif (fallback « premier workspace du user ») et on ne
|
||||
veut pas que ce workspace appartienne à l'utilisateur fixture partagé.
|
||||
"""
|
||||
from app.db import get_conn
|
||||
|
||||
reg = client.post(
|
||||
"/auth/register",
|
||||
json={"email": "[email protected]", "password": "secret123", "name": "taguser"},
|
||||
)
|
||||
assert reg.status_code == 200, reg.text
|
||||
uid = client.get("/api/users/me").json()["id"]
|
||||
with get_conn() as conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO workspaces (name, owner_id) VALUES ('Ws A32', ?)", (uid,)
|
||||
)
|
||||
ws_id = cur.lastrowid
|
||||
conn.commit()
|
||||
iid = _seed_page("Item taggé A32")
|
||||
tag_id = None
|
||||
try:
|
||||
created = client.post(
|
||||
f"/api/local-workspace/items/{iid}/tags", json={"name": "UrgentA32"}
|
||||
)
|
||||
assert created.status_code == 200
|
||||
tag_id = created.json()["tag"]["id"]
|
||||
assert created.json()["tag"]["name"] == "urgenta32" # lowercasé
|
||||
|
||||
item_tags = client.get(f"/api/local-workspace/items/{iid}/tags").json()["tags"]
|
||||
assert any(t["id"] == tag_id for t in item_tags)
|
||||
|
||||
all_tags = client.get("/api/local-workspace/tags")
|
||||
assert all_tags.status_code == 200
|
||||
assert any(t["id"] == tag_id for t in all_tags.json()["tags"])
|
||||
|
||||
search = client.get("/api/local-workspace/tags/search", params={"tags": "urgentA32"})
|
||||
assert search.status_code == 200 and isinstance(search.json()["items"], list)
|
||||
|
||||
assert client.delete(
|
||||
f"/api/local-workspace/items/{iid}/tags/{tag_id}"
|
||||
).json() == {"status": "ok"}
|
||||
after = client.get(f"/api/local-workspace/items/{iid}/tags").json()["tags"]
|
||||
assert all(t["id"] != tag_id for t in after)
|
||||
finally:
|
||||
with get_conn() as conn:
|
||||
if tag_id is not None:
|
||||
conn.execute("DELETE FROM tags WHERE id=?", (tag_id,))
|
||||
conn.execute("DELETE FROM pages WHERE id=?", (iid,))
|
||||
conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_dashboard_files_traversal_denied_and_serve(client):
|
||||
"""A16 : /api/files refuse le traversal et sert les vrais fichiers."""
|
||||
import pathlib as _pathlib
|
||||
|
||||
Reference in New Issue
Block a user