pr-reviewer/tests/test_endpoints.py
Adam Moussa 46cd856fb3
Add pr-reviewer local PR review tool
A single-user local dashboard that pulls open PRs from the
Sea-Haven-Industries org, reviews each with a Fireworks model in the
BLOCK/FIX/NIT/QUESTION format, and posts the review to GitHub as the
token owner. Runs only on localhost; secrets stay in a gitignored .env
and never reach the browser.

Structured as an app/ package plus a static/ frontend so the module
imports and static mount resolve. HTTP uses httpx2 (the runtime lib
starlette's TestClient now prefers), pinned in requirements.txt.

Includes a stdlib-only pytest suite (network mocked, no extra test
deps so CI needs only pytest) with a skip-by-default live Fireworks
test, and CI wired to the org ci-python-app reusable workflow to lint
app and tests and run the mocked suite fully offline. Dependabot covers
pip and github-actions.
2026-07-01 13:48:10 -04:00

205 lines
6.2 KiB
Python

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