"""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")]