Files
RepoPulse-Public/backend/tests/test_api.py
T

123 lines
4.7 KiB
Python

from pathlib import Path
from threading import BoundedSemaphore
import pytest
from fastapi.testclient import TestClient
from app import main as main_module
from app.store import ScanStore
def _repository(root: Path) -> Path:
repository = root / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / "LICENSE").write_text("MIT\n", encoding="utf-8")
(repository / ".gitignore").write_text(".env\n", encoding="utf-8")
(repository / "app.py").write_text("print('hello')\n", encoding="utf-8")
return repository
def test_scan_api_respects_root_and_exports_reports(tmp_path: Path) -> None:
repository = _repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
first_response = client.post("/api/v1/scans", json={"path": "repo"})
assert first_response.status_code == 201
first = first_response.json()
assert first["repository_path"] == str(repository.resolve())
html = client.get(f"/api/v1/scans/{first['id']}/export?format=html")
assert html.status_code == 200
assert "RepoPulse report" in html.text
assert "repo" in html.text
blocked = client.post("/api/v1/scans", json={"path": str(tmp_path.parent)})
assert blocked.status_code == 422
def test_scan_api_comparison_delta_and_trend(tmp_path: Path) -> None:
repository = _repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
first = client.post("/api/v1/scans", json={"path": "repo"}).json()
(repository / "tests").mkdir()
(repository / "tests" / "test_app.py").write_text("def test_ok():\n assert True\n", encoding="utf-8")
second = client.post("/api/v1/scans", json={"path": "repo"}).json()
comparison = client.get(f"/api/v1/scans/{second['id']}/comparison")
assert comparison.status_code == 200
assert comparison.json()["previous_scan_id"] == first["id"]
delta = client.get(f"/api/v1/scans/{second['id']}/delta")
assert delta.status_code == 200
assert delta.json()["previous_health_score"] == first["health_score"]
assert delta.json()["previous_finding_count"] == len(first["findings"])
trend = client.get(f"/api/v1/trend?path={repository}")
assert trend.status_code == 200
assert [item["scan_id"] for item in trend.json()] == [first["id"], second["id"]]
def test_scan_api_rejects_when_scan_capacity_is_busy(tmp_path: Path) -> None:
_repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
assert main_module.SCAN_SLOTS.acquire(blocking=False)
try:
response = client.post("/api/v1/scans", json={"path": "repo"})
finally:
main_module.SCAN_SLOTS.release()
assert response.status_code == 429
assert response.headers["retry-after"] == "2"
def test_scan_api_rejects_symlinked_repository_outside_root(tmp_path: Path) -> None:
outside = tmp_path.parent / f"{tmp_path.name}-outside-repository"
outside.mkdir()
(outside / "app.py").write_text("print('outside')\n", encoding="utf-8")
linked = tmp_path / "linked-repository"
try:
linked.symlink_to(outside, target_is_directory=True)
except (NotImplementedError, OSError):
pytest.skip("directory symlinks are unavailable in this environment")
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
response = client.post("/api/v1/scans", json={"path": "linked-repository"})
assert response.status_code == 422
def test_mutating_api_requires_operator_token_only_when_configured(tmp_path: Path, monkeypatch) -> None:
_repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
monkeypatch.setenv("REPOPULSE_OPERATOR_TOKEN", "test-operator-token")
client = TestClient(main_module.app)
assert client.get("/api/v1/meta").status_code == 200
denied = client.post("/api/v1/scans", json={"path": "repo"})
assert denied.status_code == 401
allowed = client.post(
"/api/v1/scans",
json={"path": "repo"},
headers={"X-RepoPulse-Token": "test-operator-token"},
)
assert allowed.status_code == 201