"""HTTP API behaviour with a fake Gitea (audit C2/C3/I5/M6/M7).""" from app import models from tests.conftest import csrf_from def test_health_is_public_liveness(client): from app.config import APP_VERSION r = client.get("/api/health") assert r.status_code == 200 assert r.json()["status"] == "ok" assert r.json()["version"] == APP_VERSION def test_readiness_reflects_gitea(client, fake_gitea): assert client.get("/api/readiness").status_code == 200 fake_gitea.ping_result = False r = client.get("/api/readiness") assert r.status_code == 503 assert r.json()["checks"]["gitea"] is False assert r.json()["checks"]["database"] is True def test_dashboard_renders_three_categories(client): r = client.get("/") assert r.status_code == 200 assert "demo" in r.text and "vieux" in r.text and "archive" in r.text assert "Archivés" in r.text and "Inactifs" in r.text def test_board_hides_pull_requests_and_reconciles(client): models.move_issue("Projets/demo", 3, "todo") # closed in Gitea r = client.get("/board/Projets/demo") assert r.status_code == 200 assert "Tâche un" in r.text assert "PR déguisée" not in r.text # M6: PRs are not issues state = models.get_board_state("Projets/demo") assert [c["issue_number"] for c in state["done"]] == [3] # reconciled def test_focus_lists_in_progress_across_projects(client): models.move_issue("Projets/demo", 5, "in_progress") r = client.get("/focus") assert r.status_code == 200 assert "Tâche focus" in r.text def test_gitea_outage_is_a_clean_502_not_500(client, fake_gitea): fake_gitea.raise_on_search = True r = client.get("/") assert r.status_code == 502 assert "joindre Gitea" in r.text r = client.get("/api/projects") assert r.status_code == 502 assert r.json()["ok"] is False # ── CSRF + moves + write-back ────────────────────────────── def test_move_requires_csrf_token(client): r = client.post( "/api/move", data={"repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "done"}, ) assert r.status_code == 403 def test_move_to_done_closes_issue_in_gitea(client, fake_gitea): token = csrf_from(client) r = client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "done", "from_column": "backlog", "to_order": "1", "csrf_token": token, }, ) assert r.status_code == 200 assert r.json()["gitea_sync"] == "closed" assert fake_gitea.state_changes == [("Projets", "demo", 1, "closed")] state = models.get_board_state("Projets/demo") assert [c["issue_number"] for c in state["done"]] == [1] def test_move_out_of_done_reopens_issue(client, fake_gitea): token = csrf_from(client) client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "done", "from_column": "backlog", "csrf_token": token, }, ) r = client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "todo", "from_column": "done", "csrf_token": token, }, ) assert r.json()["gitea_sync"] == "reopened" assert fake_gitea.state_changes[-1] == ("Projets", "demo", 1, "open") def test_move_writeback_failure_is_reported_not_fatal(client, fake_gitea): fake_gitea.fail_state_change = True token = csrf_from(client) r = client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "done", "from_column": "backlog", "csrf_token": token, }, ) assert r.status_code == 200 assert r.json()["ok"] is True assert r.json()["gitea_sync"] == "failed" def test_move_invalid_column_is_400(client): token = csrf_from(client) r = client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 1, "to_column": "nimporte_quoi", "csrf_token": token, }, ) assert r.status_code == 400 assert r.json()["ok"] is False def test_move_persists_card_order(client): token = csrf_from(client) r = client.post( "/api/move", data={ "repo_full_name": "Projets/demo", "issue_number": 5, "to_column": "backlog", "from_column": "backlog", "to_order": "5,1", "csrf_token": token, }, ) assert r.status_code == 200 state = models.get_board_state("Projets/demo") assert [c["issue_number"] for c in state["backlog"]][:2] == [5, 1] # ── Notes + creation ─────────────────────────────────────── def test_notes_roundtrip_via_api(client): token = csrf_from(client) r = client.post( "/api/notes", data={"repo_full_name": "Projets/demo", "note": "à retenir", "csrf_token": token}, ) assert r.status_code == 200 assert models.get_notes("Projets/demo") == "à retenir" def test_notes_size_limit_is_413(client, monkeypatch): monkeypatch.setenv("KANBANTE_MAX_NOTE_CHARS", "10") token = csrf_from(client) r = client.post( "/api/notes", data={ "repo_full_name": "Projets/demo", "note": "x" * 50, "csrf_token": token, }, ) assert r.status_code == 413 def test_create_issue_goes_to_gitea_and_lands_on_board(client, fake_gitea): token = csrf_from(client) r = client.post( "/api/issues", data={ "repo_full_name": "Projets/demo", "title": "Bug à corriger", "column": "backlog", "csrf_token": token, }, ) assert r.status_code == 201 number = r.json()["issue"]["number"] assert fake_gitea.created == [("Projets", "demo", "Bug à corriger")] state = models.get_board_state("Projets/demo") assert number in [c["issue_number"] for c in state["backlog"]] def test_create_issue_validates_title(client): token = csrf_from(client) r = client.post( "/api/issues", data={"repo_full_name": "Projets/demo", "title": " ", "csrf_token": token}, ) assert r.status_code == 400