"""A32 — smoke tests : un par route des 4 routers à 0 test. `webhooks.py` (3), `notes.py` (2), `sidebar_config.py` (2), `github_routes.py` (2). Aucun accès réseau réel : le secret webhook est piloté par monkeypatch et `gitea.list_webhooks` est stubbé. """ from app.config import settings OWNER, REPO = "smoke-a32", "repo-x" # ── webhooks.py (0/3) ──────────────────────────────────────────────────────── def test_webhook_receive_ok_without_secret(client, monkeypatch): monkeypatch.setattr(settings, "gitea_webhook_secret", "") r = client.post("/api/webhook", json={"zen": "smoke"}) assert r.status_code == 200 assert r.json() == {"status": "ok"} def test_webhook_receive_rejects_bad_signature(client, monkeypatch): monkeypatch.setattr(settings, "gitea_webhook_secret", "s3cret") r = client.post("/api/webhook", json={}) # pas d'en-tête X-Gitea-Signature assert r.status_code == 401 assert r.json()["detail"] == "Invalid signature" def test_webhook_register_fails_fast_without_secret(client, monkeypatch): # secret vide → 400 AVANT tout appel réseau (gitea.list_webhooks) monkeypatch.setattr(settings, "gitea_webhook_secret", "") r = client.post(f"/api/webhook/register/{OWNER}/{REPO}") assert r.status_code == 400 assert "not configured" in r.json()["detail"] def test_webhook_status_stubbed(client, monkeypatch): from app.services import gitea_client async def _no_hooks(*_a, **_k): return [] monkeypatch.setattr(gitea_client.gitea, "list_webhooks", _no_hooks) r = client.get(f"/api/webhook/status/{OWNER}/{REPO}") assert r.status_code == 200 assert r.json() == {"registered": False} # ── notes.py (0/2) ─────────────────────────────────────────────────────────── def test_notes_get_renders_html(client): r = client.get(f"/notes/{OWNER}/{REPO}") assert r.status_code == 200 assert "text/html" in r.headers["content-type"] def test_notes_save_roundtrip(client): payload = "note de smoke A32" html_payload = "&co" r = client.post( f"/notes/{OWNER}/{REPO}", data={"content": f"{payload} {html_payload}"} ) assert r.status_code == 200 assert payload in r.text # contenu rendu assert "<b>&co</b>" in r.text # échappé (A10 — pas d'HTML cru) again = client.get(f"/notes/{OWNER}/{REPO}") assert payload in again.text # persistée en base (upsert) # ── sidebar_config.py (0/2) ────────────────────────────────────────────────── def test_sidebar_config_get(client): r = client.get("/api/sidebar/config") assert r.status_code == 200 cfg = r.json()["config"] assert isinstance(cfg, dict) and "meetings" in cfg def test_sidebar_config_put_persists_and_validates(client): r = client.put( "/api/sidebar/config", json={"config": {"meetings": {"visible": False, "order": 2, "show_count": 5}}}, ) assert r.status_code == 200 back = client.get("/api/sidebar/config").json()["config"] assert back["meetings"]["visible"] is False # relu depuis users.sidebar_config bad = client.put("/api/sidebar/config", json={}) assert bad.status_code == 400 # config dict requis # remise en état pour les autres tests restore = client.put( "/api/sidebar/config", json={"config": {"meetings": {"visible": True, "order": 2, "show_count": 5}}}, ) assert restore.status_code == 200 # ── github_routes.py (0/2) ─────────────────────────────────────────────────── def test_github_status_unlinked(client): r = client.get("/api/github/status") assert r.status_code == 200 assert r.json() == {"linked": False} def test_github_disconnect_ok(client): r = client.delete("/api/github/disconnect") assert r.status_code == 200 assert r.json() == {"status": "ok"} # ── library.py (1/10 → 10/10) ──────────────────────────────────────────────── def _seed_page(title: str, **cols) -> int: """Insert minimale (title/content/format + colonnes surnuméraires), id renvoyé.""" from app.db import get_conn columns = ["workspace", "title", "content", "content_format", *cols] placeholders = ", ".join("?" for _ in columns) values = ["", title, "", "markdown", *cols.values()] with get_conn() as conn: cur = conn.execute( f"INSERT INTO pages ({', '.join(columns)}) VALUES ({placeholders})", values ) pid = cur.lastrowid conn.commit() return pid def test_library_all_lists_return_items(client): """Les 5 listes de la bibliothèque répondent {"items": [...]}.""" for path in ("/recents", "/favorites", "/published", "/private", "/workspace"): r = client.get(f"/api/library{path}") assert r.status_code == 200, (path, r.status_code) assert isinstance(r.json()["items"], list), path def test_library_private_lists_seeded_page(client): from app.db import get_conn pid = _seed_page("A32 private page", parent_section="Private") try: items = client.get("/api/library/private").json()["items"] assert "A32 private page" in [i["title"] for i in items] finally: with get_conn() as conn: conn.execute("DELETE FROM pages WHERE id=?", (pid,)) conn.commit() def test_library_children_lists_child(client): from app.db import get_conn parent = _seed_page("A32 parent") child = _seed_page("A32 child", parent_id=parent) try: items = client.get(f"/api/library/children/{parent}").json()["items"] assert [i["title"] for i in items] == ["A32 child"] finally: with get_conn() as conn: conn.execute("DELETE FROM pages WHERE id IN (?, ?)", (parent, child)) conn.commit() def test_library_local_workspace_routes_removed(client): """A32 → A30 : ces 2 routes lisaient `local_workspace_items`, une table inexistante nulle part dans le codebase (500 systématique côté children, 500 d?s qu'un workspace existe côté local-workspace) et AUCUNE référence front → supprimées avec `_format_size` devenu mort.""" assert client.get("/api/library/local-workspace-children/1").status_code == 404 assert client.get("/api/library/local-workspace").status_code == 404 def test_library_repository_empty_and_keyed(client): empty = client.get("/api/library/repository") assert empty.status_code == 200 and empty.json() == {"items": []} keyed = client.get("/api/library/repository?gitea_owner=acme&gitea_repo=flowdeck") assert keyed.status_code == 200 assert isinstance(keyed.json()["items"], list) def test_library_lists_still_available_after_removal(client): """Les listes restantes (celles qu'utilise le front) répondent toujours.""" r = client.get("/api/library/recents") assert r.status_code == 200 and isinstance(r.json()["items"], list)