106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
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("<main>reviewer</main>\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"<main>reviewer</main>\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
|