test: A32 TERMINÉ — 6 routes Gitea stubbées + bug prod fd_icon (v7.20.0)
Les 6 dernières routes d'A32 (api.py, gitea) avec stub de transport — zéro
réseau réel :
- _stub_gitea() : stubs manuels sur gitea_client.gitea (create_issue,
update_issue, update_issue_labels, get_issue, get_issue_comments) avec
ÉTAT MUTABLE PARTAGÉ — le handler PATCH re-fetch l'issue via get_issue,
un canevas figé aurait masqué la mise à jour.
- POST /issues : carte INSÉRÉE sur le board (board seedé par endpoint) ;
PATCH : colonne recalculée sans perdre la carte.
- GET /issues JSON + HTML : ?format=html requis (le segment /html ne fixe pas
le paramètre, le handler le lit dans la query) ; stub qui lève → 404.
- POST /checklists + POST /checklist-items : lignes vérifiées en base,
404 sans board ; cleanup (items → checklists).
BUG PROD corrigé (trouvé par le smoke HTML) : card_detail.html utilisait la
macro fd_icon SANS l'importer → UndefinedError → 500 systématique sur
GET /api/issues/...?format=html (seul rendu du template dans le code).
Fix : {% from '_icons.html' import fd_icon %}.
A32 COMPLET : plus aucun router « 0 test » (webhooks, notes, sidebar_config,
github_routes, library, api, dashboard, api_v2 tous couverts).
test_smoke_uncovered.py : 52 tests. suite **1089/1089** · ruff OK · docs à jour
This commit is contained in:
@@ -728,6 +728,160 @@ def test_dashboard_convert_to_database(client):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user