diff --git a/tests/test_capture.py b/tests/test_capture.py new file mode 100644 index 0000000..655bcc8 --- /dev/null +++ b/tests/test_capture.py @@ -0,0 +1,154 @@ +"""Capture éclair (feuille de route #12) : API à jeton, page web, ntfy.""" + +import pytest + +from app import models +from app.capture import parse_capture_text, parse_ntfy_message +from tests.conftest import csrf_from + +TOKEN = "jeton-de-capture-test" + + +@pytest.fixture +def capture_env(monkeypatch): + monkeypatch.setenv("KANBANTE_CAPTURE_TOKEN", TOKEN) + monkeypatch.setenv("KANBANTE_CAPTURE_REPO", "Projets/demo") + + +# ── Parsing ─────────────────────────────────────────────────────── + + +def test_parse_capture_text_first_line_is_title(): + title, body = parse_capture_text("Idée géniale\n\nDétails\nsur deux lignes") + assert title == "Idée géniale" + assert body == "Détails\nsur deux lignes" + + +def test_parse_capture_text_empty(): + assert parse_capture_text(" \n ") == ("", "") + + +def test_parse_ntfy_message_variants(): + assert parse_ntfy_message( + {"event": "message", "title": "Titre", "message": "Corps"} + ) == ("Titre", "Corps") + assert parse_ntfy_message( + {"event": "message", "message": "Ligne un\n\nLigne deux"} + ) == ("Ligne un", "Ligne deux") + assert parse_ntfy_message({"event": "open"}) is None + assert parse_ntfy_message( + {"event": "message", "message": "auto", "tags": ["kanbante"]} + ) is None # never ingest our own posts + + +# ── API ─────────────────────────────────────────────────────────── + + +def test_capture_api_disabled_without_token(client, fake_gitea): + r = client.post("/api/capture", json={"text": "Idée"}) + assert r.status_code == 503 + + +def test_capture_api_rejects_wrong_token(client, fake_gitea, capture_env): + r = client.post( + "/api/capture", + json={"text": "Idée"}, + headers={"X-Capture-Token": "mauvais"}, + ) + assert r.status_code == 401 + assert fake_gitea.created == [] + + +def test_capture_api_creates_issue_in_default_repo(client, fake_gitea, capture_env): + r = client.post( + "/api/capture", + json={"text": "Idée éclair\n\nLe détail qui compte"}, + headers={"X-Capture-Token": TOKEN}, + ) + assert r.status_code == 201 + data = r.json() + assert data["ok"] is True + number = data["issue"]["number"] + assert fake_gitea.created == [("Projets", "demo", "Idée éclair")] + state = models.get_board_state("Projets/demo") + assert number in [c["issue_number"] for c in state["backlog"]] + events = [ + e for e in models.get_events("Projets/demo") + if e["issue_number"] == number + ] + assert any( + e["event_type"] == "created" and e["source"] == "capture" for e in events + ) + + +def test_capture_api_text_plain_and_bearer(client, fake_gitea, capture_env): + r = client.post( + "/api/capture", + content="Titre en texte brut\nCorps", + headers={ + "Authorization": f"Bearer {TOKEN}", + "Content-Type": "text/plain", + }, + ) + assert r.status_code == 201 + assert fake_gitea.created == [("Projets", "demo", "Titre en texte brut")] + + +def test_capture_api_explicit_repo_and_column(client, fake_gitea, capture_env): + r = client.post( + "/api/capture", + json={"title": "Direct en todo", "repo": "Projets/demo", "column": "todo"}, + headers={"X-Capture-Token": TOKEN}, + ) + assert r.status_code == 201 + number = r.json()["issue"]["number"] + state = models.get_board_state("Projets/demo") + assert number in [c["issue_number"] for c in state["todo"]] + + +def test_capture_api_validates_input(client, fake_gitea, capture_env): + r = client.post( + "/api/capture", json={"text": " "}, + headers={"X-Capture-Token": TOKEN}, + ) + assert r.status_code == 400 + r = client.post( + "/api/capture", + json={"title": "X", "repo": "pas-de-slash"}, + headers={"X-Capture-Token": TOKEN}, + ) + assert r.status_code == 400 + + +def test_capture_api_no_repo_anywhere(client, fake_gitea, monkeypatch): + monkeypatch.setenv("KANBANTE_CAPTURE_TOKEN", TOKEN) + monkeypatch.delenv("KANBANTE_CAPTURE_REPO", raising=False) + r = client.post( + "/api/capture", json={"title": "Orpheline"}, + headers={"X-Capture-Token": TOKEN}, + ) + assert r.status_code == 400 + + +# ── Page web ────────────────────────────────────────────────────── + + +def test_capture_page_renders(client, capture_env): + r = client.get("/capture") + assert r.status_code == 200 + assert "Capture éclair" in r.text + assert "Projets/demo" in r.text + + +def test_capture_web_form_creates_issue(client, fake_gitea, capture_env): + token = csrf_from(client) + r = client.post( + "/capture", + data={ + "repo_full_name": "Projets/demo", "title": "Via le formulaire", + "body": "Détails", "column": "backlog", "csrf_token": token, + }, + ) + assert r.status_code == 200 + assert "Capturé" in r.text + assert fake_gitea.created == [("Projets", "demo", "Via le formulaire")]