|
|
|
@@ -596,6 +596,420 @@ 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── api.py : les 6 routes Gitea (stub de transport — zéro réseau) ────────────
|
|
|
|
|
|
|
|
|
|
_CANNED_ISSUE = {
|
|
|
|
|
"number": 42, "title": "Issue A32", "body": "description a32",
|
|
|
|
|
"state": "open", "labels": [], "assignee": None, "due_date": "",
|
|
|
|
|
"created_at": "2026-10-01T00:00:00Z", "user": {"login": "giteux"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stub_gitea(monkeypatch):
|
|
|
|
|
from app.services import gitea_client
|
|
|
|
|
|
|
|
|
|
# état mutable partagé : le PATCH est re-fetché par le handler via
|
|
|
|
|
# get_issue, donc update doit modifier ce que get renverra.
|
|
|
|
|
state = dict(_CANNED_ISSUE)
|
|
|
|
|
|
|
|
|
|
async def _create(owner, repo, title, body, **_k):
|
|
|
|
|
state.update({"title": title, "body": body})
|
|
|
|
|
return dict(state)
|
|
|
|
|
|
|
|
|
|
async def _update(owner, repo, issue_id, **kwargs):
|
|
|
|
|
state.update(kwargs)
|
|
|
|
|
return dict(state)
|
|
|
|
|
|
|
|
|
|
async def _labels(owner, repo, issue_id, label_ids):
|
|
|
|
|
return [{"id": i} for i in label_ids]
|
|
|
|
|
|
|
|
|
|
async def _get(owner, repo, issue_id):
|
|
|
|
|
return dict(state)
|
|
|
|
|
|
|
|
|
|
async def _comments(owner, repo, issue_id):
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(gitea_client.gitea, "create_issue", _create)
|
|
|
|
|
monkeypatch.setattr(gitea_client.gitea, "update_issue", _update)
|
|
|
|
|
monkeypatch.setattr(gitea_client.gitea, "update_issue_labels", _labels)
|
|
|
|
|
monkeypatch.setattr(gitea_client.gitea, "get_issue", _get)
|
|
|
|
|
monkeypatch.setattr(gitea_client.gitea, "get_issue_comments", _comments)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_gitea_issue_create_update_with_board(client, monkeypatch):
|
|
|
|
|
"""POST create → carte sur le board ; PATCH update → colonne recalculée."""
|
|
|
|
|
from app.db import get_conn
|
|
|
|
|
|
|
|
|
|
_stub_gitea(monkeypatch)
|
|
|
|
|
_seed_board(client) # board (OWNER, REPO) via l'endpoint
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
board_id = conn.execute(
|
|
|
|
|
"SELECT id FROM boards WHERE project_owner=? AND project_name=?",
|
|
|
|
|
(OWNER, REPO),
|
|
|
|
|
).fetchone()["id"]
|
|
|
|
|
conn.execute("DELETE FROM cards WHERE board_id=?", (board_id,))
|
|
|
|
|
conn.commit()
|
|
|
|
|
try:
|
|
|
|
|
created = client.post(
|
|
|
|
|
f"/api/issues/{OWNER}/{REPO}", params={"title": "Issue A32"}
|
|
|
|
|
)
|
|
|
|
|
assert created.status_code == 200, created.text
|
|
|
|
|
assert created.json()["status"] == "ok"
|
|
|
|
|
assert created.json()["issue"]["number"] == 42
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
card = conn.execute(
|
|
|
|
|
"SELECT column_name FROM cards WHERE board_id=? AND gitea_issue_id=42",
|
|
|
|
|
(board_id,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
assert card is not None # carte créée localement à partir de l'issue
|
|
|
|
|
|
|
|
|
|
patched = client.patch(
|
|
|
|
|
f"/api/issues/{OWNER}/{REPO}/42", params={"title": "Renommé A32"}
|
|
|
|
|
)
|
|
|
|
|
assert patched.status_code == 200
|
|
|
|
|
assert patched.json()["issue"]["title"] == "Renommé A32"
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
card2 = conn.execute(
|
|
|
|
|
"SELECT column_name FROM cards WHERE board_id=? AND gitea_issue_id=42",
|
|
|
|
|
(board_id,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
assert card2 is not None # colonne recalculée (UPDATE) sans perdre la carte
|
|
|
|
|
finally:
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
conn.execute("DELETE FROM cards WHERE board_id=?", (board_id,))
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_gitea_issue_detail_json_and_html(client, monkeypatch):
|
|
|
|
|
"""GET détail (2 decorators, 1 handler) : JSON + rendu HTML."""
|
|
|
|
|
_stub_gitea(monkeypatch)
|
|
|
|
|
r = client.get(f"/api/issues/{OWNER}/{REPO}/42")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
d = r.json()
|
|
|
|
|
assert d["issue"]["title"] == "Issue A32"
|
|
|
|
|
assert d["comments"] == [] and d["checklists"] == [] and d["card"] is None
|
|
|
|
|
|
|
|
|
|
# le handler lit le paramètre `format` (le segment /html ne le fixe pas)
|
|
|
|
|
html = client.get(f"/api/issues/{OWNER}/{REPO}/42/html", params={"format": "html"})
|
|
|
|
|
assert html.status_code == 200
|
|
|
|
|
assert "text/html" in html.headers["content-type"]
|
|
|
|
|
assert "Issue A32" in html.text # card_detail.html rendu avec le canevas
|
|
|
|
|
|
|
|
|
|
# inconnu (le stub lève) → 404 propre
|
|
|
|
|
from app.services import gitea_client
|
|
|
|
|
|
|
|
|
|
async def _boom(*_a, **_k):
|
|
|
|
|
raise RuntimeError("pas trouvé")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(gitea_client.gitea, "get_issue", _boom)
|
|
|
|
|
assert client.get(f"/api/issues/{OWNER}/{REPO}/999").status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_gitea_checklists_create_with_board(client):
|
|
|
|
|
"""POST checklist + POST item : création vérifiée en base (DB seul)."""
|
|
|
|
|
from app.db import get_conn
|
|
|
|
|
|
|
|
|
|
_seed_board(client)
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
board_id = conn.execute(
|
|
|
|
|
"SELECT id FROM boards WHERE project_owner=? AND project_name=?",
|
|
|
|
|
(OWNER, REPO),
|
|
|
|
|
).fetchone()["id"]
|
|
|
|
|
cl_id = item_id = None
|
|
|
|
|
try:
|
|
|
|
|
r = client.post(
|
|
|
|
|
f"/api/checklists/{OWNER}/{REPO}/42", params={"title": "À faire A32"}
|
|
|
|
|
)
|
|
|
|
|
assert r.status_code == 200 and r.json()["status"] == "ok"
|
|
|
|
|
cl_id = r.json()["checklist_id"]
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
row = conn.execute(
|
|
|
|
|
"SELECT board_id, title FROM checklists WHERE id=?", (cl_id,)
|
|
|
|
|
).fetchone()
|
|
|
|
|
assert row["board_id"] == board_id and row["title"] == "À faire A32"
|
|
|
|
|
|
|
|
|
|
r2 = client.post(
|
|
|
|
|
f"/api/checklist-items/{OWNER}/{REPO}/42/{cl_id}",
|
|
|
|
|
params={"content": "étape 1"},
|
|
|
|
|
)
|
|
|
|
|
assert r2.status_code == 200 and r2.json()["status"] == "ok"
|
|
|
|
|
item_id = r2.json()["item_id"]
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
it = conn.execute(
|
|
|
|
|
"SELECT content FROM checklist_items WHERE id=?", (item_id,)
|
|
|
|
|
).fetchone()
|
|
|
|
|
assert it["content"] == "étape 1"
|
|
|
|
|
# board absent → 404 (validation côté route)
|
|
|
|
|
assert client.post("/api/checklists/absent/repo/1").status_code == 404
|
|
|
|
|
finally:
|
|
|
|
|
with get_conn() as conn:
|
|
|
|
|
if item_id is not None:
|
|
|
|
|
conn.execute("DELETE FROM checklist_items WHERE id=?", (item_id,))
|
|
|
|
|
if cl_id is not None:
|
|
|
|
|
conn.execute("DELETE FROM checklists WHERE id=?", (cl_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
|
|
|
|
|
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
|
|
|
|
|