"""Endpoint tests via FastAPI TestClient. The lazy singletons ``app.main.gh`` / ``app.main.reviewer`` are monkeypatched to return fakes, so no network and no secrets are required. TestClient uses the already-installed httpx2, so no extra test deps. """ from __future__ import annotations from typing import Any import pytest from fastapi.testclient import TestClient from app import main as main_mod from app.github_client import GitHubError @pytest.fixture def client() -> TestClient: return TestClient(main_mod.app) class FakeGH: def __init__(self) -> None: self.posted: dict[str, Any] | None = None def search_prs(self) -> list[dict[str, Any]]: return [{"owner": "o", "repo": "r", "number": 1, "title": "t"}] def whoami(self) -> str: return "octocat" def pr_diff(self, owner: str, repo: str, number: int) -> str: return "diff --git a/x b/x\n+hi\n" def submit_review(self, owner, repo, number, body, event) -> dict[str, Any]: self.posted = {"event": event, "body": body} return {"html_url": "https://github.com/o/r/pull/1#review"} class FakeReviewer: def review(self, pr: dict[str, Any], diff: str) -> dict[str, Any]: return { "summary": "ok", "recommended_event": "COMMENT", "_body_markdown": "**Summary:** ok", "_diff_seen": diff, } def _patch(monkeypatch, gh=None, reviewer=None): if gh is not None: monkeypatch.setattr(main_mod, "gh", lambda: gh) if reviewer is not None: monkeypatch.setattr(main_mod, "reviewer", lambda: reviewer) # --------------------------------------------------------------------------- # # /api/config # --------------------------------------------------------------------------- # def test_api_config(client: TestClient) -> None: r = client.get("/api/config") assert r.status_code == 200 data = r.json() assert set(data) == {"org", "filter", "model", "mention_authors"} # --------------------------------------------------------------------------- # # /api/prs # --------------------------------------------------------------------------- # def test_api_prs_ok(client: TestClient, monkeypatch) -> None: _patch(monkeypatch, gh=FakeGH()) r = client.get("/api/prs") assert r.status_code == 200 data = r.json() assert data["me"] == "octocat" assert data["prs"][0]["number"] == 1 def test_api_prs_github_error_maps_to_400(client: TestClient, monkeypatch) -> None: class BoomGH(FakeGH): def search_prs(self): raise GitHubError("GitHub 401: Bad credentials") _patch(monkeypatch, gh=BoomGH()) r = client.get("/api/prs") assert r.status_code == 400 assert "Bad credentials" in r.json()["detail"] # --------------------------------------------------------------------------- # # /api/review # --------------------------------------------------------------------------- # def _review_body() -> dict[str, Any]: return { "owner": "o", "repo": "r", "number": 1, "title": "t", "author": "octocat", "body": "b", } def test_api_review_ok(client: TestClient, monkeypatch) -> None: _patch(monkeypatch, gh=FakeGH(), reviewer=FakeReviewer()) r = client.post("/api/review", json=_review_body()) assert r.status_code == 200 assert r.json()["review"]["summary"] == "ok" def test_api_review_github_error_maps_to_400(client: TestClient, monkeypatch) -> None: class BoomGH(FakeGH): def pr_diff(self, *a): raise GitHubError("GitHub 404: Not Found") _patch(monkeypatch, gh=BoomGH(), reviewer=FakeReviewer()) r = client.post("/api/review", json=_review_body()) assert r.status_code == 400 assert "Not Found" in r.json()["detail"] def test_api_review_generic_error_maps_to_500(client: TestClient, monkeypatch) -> None: class BoomReviewer: def review(self, pr, diff): raise ValueError("fireworks exploded") _patch(monkeypatch, gh=FakeGH(), reviewer=BoomReviewer()) r = client.post("/api/review", json=_review_body()) assert r.status_code == 500 assert "Review failed" in r.json()["detail"] # --------------------------------------------------------------------------- # # /api/revise # --------------------------------------------------------------------------- # def test_api_revise_folds_notes_into_diff(client: TestClient, monkeypatch) -> None: reviewer = FakeReviewer() _patch(monkeypatch, gh=FakeGH(), reviewer=reviewer) body = _review_body() body["notes"] = "be harsher on error handling" r = client.post("/api/revise", json=body) assert r.status_code == 200 seen = r.json()["review"]["_diff_seen"] assert "REVIEWER REVISION NOTES" in seen assert "be harsher on error handling" in seen def test_api_revise_generic_error_maps_to_500(client: TestClient, monkeypatch) -> None: class BoomReviewer: def review(self, pr, diff): raise RuntimeError("nope") _patch(monkeypatch, gh=FakeGH(), reviewer=BoomReviewer()) body = _review_body() body["notes"] = "x" r = client.post("/api/revise", json=body) assert r.status_code == 500 assert "Revision failed" in r.json()["detail"] # --------------------------------------------------------------------------- # # /api/post # --------------------------------------------------------------------------- # def test_api_post_ok(client: TestClient, monkeypatch) -> None: fake = FakeGH() _patch(monkeypatch, gh=fake) r = client.post( "/api/post", json={ "owner": "o", "repo": "r", "number": 1, "body": "lgtm", "event": "APPROVE", }, ) assert r.status_code == 200 assert r.json()["ok"] is True assert fake.posted == {"event": "APPROVE", "body": "lgtm"} def test_api_post_github_error_maps_to_400(client: TestClient, monkeypatch) -> None: class BoomGH(FakeGH): def submit_review(self, *a): raise GitHubError("Invalid event: LGTM") _patch(monkeypatch, gh=BoomGH()) r = client.post( "/api/post", json={"owner": "o", "repo": "r", "number": 1, "body": "b", "event": "LGTM"}, ) assert r.status_code == 400 assert "Invalid event" in r.json()["detail"]