Files
flowdeck/tests/test_smoke_uncovered.py
T
bruno df9a269d76
FlowDeck CI / lint (push) Successful in 1m53s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 8m41s
test: A32 phase 2a — library 10/10 + 2 routes fantômes supprimées (v7.12.0)
- tests/test_smoke_uncovered.py : +6 tests pour library.py (1/10 → 8 routes
  couvertes) : les 5 listes en boucle (recents/favorites/published/private/
  workspace → 200 + items), /private avec page seedée retrouvée, /children/{id}
  avec parent/enfant seedés, /repository vide ET clé (string, aucun réseau),
  non-régression 404 sur les routes supprimées.
- DÉCOUVERTE (les smokes l'ont prouvé) : `/api/library/local-workspace-children`
  renvoyait un 500 systématique (test vert → ASGI double response.start) et
  `/api/library/local-workspace` 500 dès qu'un workspace existe — les deux
  lisaient `local_workspace_items`, table AUCUNEMENT créée dans le codebase
  (0 CREATE TABLE) et sans 1 seule référence front. Supprimés avec
  `library._format_size` devenu mort (la copie de dashboard.py est inchangée).
  `local_workspace_items` : 0 occurrence restante dans app/.
- helper `_seed_page` minimal (workspace NOT NULL inclus) + nettoyage en
  finally (pas de pollution des autres tests).

suite **1053/1053** (229 s) · `ruff check app tests` OK · docs à jour
restent phase 2b : api.py 3/23, dashboard.py 17/63, api_v2.py 50/115
2026-10-01 12:11:12 -04:00

187 lines
7.3 KiB
Python

"""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 = "<b>&co</b>"
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 "&lt;b&gt;&amp;co&lt;/b&gt;" 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)