mirror of
https://github.com/Sea-Haven-Industries/pr-reviewer.git
synced 2026-09-30 09:13:15 +00:00
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.
205 lines
6.2 KiB
Python
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"]
|