from __future__ import annotations import json import threading from collections.abc import Generator from http.client import HTTPConnection, HTTPResponse from pathlib import Path from typing import Any, cast import pytest from reviewer.server import ReviewArtifacts from reviewer.server.__main__ import create_server from tests.reviewer_fixture import ReviewFixture, create_review_run def request( port: int, method: str, path: str, *, headers: dict[str, str] | None = None, ) -> tuple[HTTPResponse, bytes]: connection = HTTPConnection("127.0.0.1", port, timeout=3) connection.request(method, path, headers=headers or {}) response = connection.getresponse() content = response.read() connection.close() return response, content @pytest.fixture def running_server(tmp_path: Path) -> Generator[tuple[int, ReviewFixture], None, None]: fixture = create_review_run(tmp_path) static_root = tmp_path / "static" (static_root / "assets").mkdir(parents=True) (static_root / "index.html").write_text("
reviewer
\n", encoding="utf-8") (static_root / "assets/app.js").write_text("export {};\n", encoding="utf-8") repository = ReviewArtifacts(fixture["run_directory"]) server = create_server(repository, static_root) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: yield server.server_address[1], fixture finally: server.shutdown() server.server_close() thread.join(timeout=3) def test_server_exposes_same_origin_api_and_static_build( running_server: tuple[int, ReviewFixture], ) -> None: port, fixture = running_server api_response, api_content = request(port, "GET", "/api/v1/run") page_response, page_content = request(port, "GET", "/") asset_response, _ = request(port, "HEAD", "/assets/app.js") payload = cast(dict[str, Any], json.loads(api_content)) assert api_response.status == 200 assert payload["run"]["run_id"] == "review-run" assert str(fixture["source_path"]) not in api_content.decode() assert api_response.getheader("Access-Control-Allow-Origin") is None assert api_response.getheader("Cache-Control") == "no-store" assert "default-src 'self'" in cast(str, api_response.getheader("Content-Security-Policy")) assert page_response.status == 200 assert page_content == b"
reviewer
\n" assert asset_response.status == 200 assert asset_response.getheader("Content-Type") == "text/javascript; charset=utf-8" @pytest.mark.parametrize( ("method", "path", "headers", "status", "code"), [ ("POST", "/api/v1/run", None, 405, "method_not_allowed"), ("GET", "/api/v1/run", {"Host": "example.com"}, 403, "invalid_origin"), ( "GET", "/api/v1/run", {"Origin": "http://example.com"}, 403, "invalid_origin", ), ("GET", "/api/v1/documents/missing", None, 404, "unknown_document"), ("GET", "/api/v1/documents/paper/components/99", None, 404, "unknown_component"), ("GET", "/assets/missing.js", None, 404, "not_found"), ("GET", "/..%2Fsecret.txt", None, 404, "not_found"), ], ) def test_server_rejects_unsafe_or_unknown_requests( running_server: tuple[int, ReviewFixture], method: str, path: str, headers: dict[str, str] | None, status: int, code: str, ) -> None: port, _ = running_server response, content = request(port, method, path, headers=headers) payload = cast(dict[str, Any], json.loads(content)) assert response.status == status assert payload["error"]["code"] == code