feat: 增加项目无关的本地清洗评审器
This commit is contained in:
@@ -362,7 +362,7 @@ def test_empty_document_and_empty_rule_set_are_no_ops() -> None:
|
||||
|
||||
|
||||
def test_installed_package_version_matches_delivery_candidate() -> None:
|
||||
assert distribution_version("mdpolish") == "0.6.1"
|
||||
assert distribution_version("mdpolish") == "0.7.0"
|
||||
|
||||
|
||||
def test_parameters_preserve_rule_order_and_record_all_options() -> None:
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps, loads
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import (
|
||||
DocumentSnapshot,
|
||||
Modifier,
|
||||
Pipeline,
|
||||
ProposedChange,
|
||||
RunStatus,
|
||||
TextEdit,
|
||||
TextSpan,
|
||||
)
|
||||
from mdpolish.review import (
|
||||
ReviewDetail,
|
||||
ReviewDocument,
|
||||
ReviewParseError,
|
||||
build_review_document,
|
||||
parse_json_report,
|
||||
render_json_report,
|
||||
)
|
||||
|
||||
|
||||
def _replace_modifier(
|
||||
needle: str,
|
||||
replacement: str,
|
||||
*,
|
||||
modifier_id: str,
|
||||
reason: str = "应用虚构测试替换",
|
||||
) -> Modifier:
|
||||
def propose(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
start = snapshot.markdown.find(needle)
|
||||
if start < 0:
|
||||
return ()
|
||||
return (
|
||||
ProposedChange(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
reason=reason,
|
||||
edits=(
|
||||
TextEdit(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
span=TextSpan(start, start + len(needle)),
|
||||
expected_text=needle,
|
||||
replacement=replacement,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return Modifier(
|
||||
modifier_id=modifier_id,
|
||||
version="1.0.0",
|
||||
parameters={"needle": needle, "replacement": replacement},
|
||||
applicability="只处理虚构 JSON reader 测试标记。",
|
||||
propose=propose,
|
||||
)
|
||||
|
||||
|
||||
def _noop_modifier(modifier_id: str = "test.noop") -> Modifier:
|
||||
return Modifier(
|
||||
modifier_id=modifier_id,
|
||||
version="1.0.0",
|
||||
parameters=(),
|
||||
applicability="不修改任何文本。",
|
||||
propose=lambda _snapshot: (),
|
||||
)
|
||||
|
||||
|
||||
def _success_review() -> ReviewDocument:
|
||||
input_markdown = "\ufeff首行\r\n🙂Cafe\u0301\r末行"
|
||||
result = Pipeline(
|
||||
(
|
||||
_noop_modifier(),
|
||||
_replace_modifier("🙂Cafe\u0301", "完成", modifier_id="test.unicode"),
|
||||
)
|
||||
).transform(input_markdown)
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
return build_review_document(input_markdown, result)
|
||||
|
||||
|
||||
def _unstable_review() -> ReviewDocument:
|
||||
residual_marker = "RESIDUAL_SECRET"
|
||||
|
||||
def residual(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
start = snapshot.markdown.find(residual_marker)
|
||||
if start < 0:
|
||||
return ()
|
||||
return (
|
||||
ProposedChange(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
reason="残留虚构候选",
|
||||
edits=(
|
||||
TextEdit(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
span=TextSpan(start, start + len(residual_marker)),
|
||||
expected_text=residual_marker,
|
||||
replacement="resolved",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
result = Pipeline(
|
||||
(
|
||||
Modifier(
|
||||
modifier_id="test.residual",
|
||||
version="1.0.0",
|
||||
parameters=(),
|
||||
applicability="只在最终复查产生候选。",
|
||||
propose=residual,
|
||||
),
|
||||
_replace_modifier("start", residual_marker, modifier_id="test.producer"),
|
||||
)
|
||||
).transform("start")
|
||||
assert result.status is RunStatus.UNSTABLE
|
||||
return build_review_document("start", result)
|
||||
|
||||
|
||||
def _error_reviews() -> tuple[ReviewDocument, ...]:
|
||||
preflight_result = Pipeline(cast(tuple[Modifier, ...], (object(),))).transform("preflight")
|
||||
|
||||
def explode(_snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
raise RuntimeError("DIAGNOSTIC_SECRET")
|
||||
|
||||
transform_result = Pipeline(
|
||||
(
|
||||
Modifier(
|
||||
modifier_id="test.transform-error",
|
||||
version="1.0.0",
|
||||
parameters=(),
|
||||
applicability="只测试执行错误。",
|
||||
propose=explode,
|
||||
),
|
||||
)
|
||||
).transform("transform")
|
||||
|
||||
def explode_on_done(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
if snapshot.markdown == "done":
|
||||
raise RuntimeError("FINAL_DIAGNOSTIC_SECRET")
|
||||
return ()
|
||||
|
||||
final_result = Pipeline(
|
||||
(
|
||||
Modifier(
|
||||
modifier_id="test.final-error",
|
||||
version="1.0.0",
|
||||
parameters=(),
|
||||
applicability="只测试最终复查错误。",
|
||||
propose=explode_on_done,
|
||||
),
|
||||
_replace_modifier("start", "done", modifier_id="test.final-producer"),
|
||||
)
|
||||
).transform("start")
|
||||
return (
|
||||
build_review_document("preflight", preflight_result),
|
||||
build_review_document("transform", transform_result),
|
||||
build_review_document("start", final_result),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("detail", tuple(ReviewDetail))
|
||||
def test_reader_accepts_every_official_detail(detail: ReviewDetail) -> None:
|
||||
review = _success_review()
|
||||
report = render_json_report(review, detail=detail)
|
||||
|
||||
parsed = parse_json_report(report, expected_detail=detail)
|
||||
|
||||
assert parsed == loads(report)
|
||||
assert parsed["detail"] == detail.value
|
||||
|
||||
|
||||
def test_reader_accepts_unstable_summary_without_materializing_residuals() -> None:
|
||||
parsed = parse_json_report(render_json_report(_unstable_review(), detail="summary"))
|
||||
|
||||
assert parsed["status"] == "unstable"
|
||||
assert "residual_proposals" not in parsed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("review", _error_reviews())
|
||||
@pytest.mark.parametrize("detail", tuple(ReviewDetail))
|
||||
def test_reader_accepts_each_failed_stage(review: ReviewDocument, detail: ReviewDetail) -> None:
|
||||
parsed = parse_json_report(render_json_report(review, detail=detail))
|
||||
|
||||
assert parsed["status"] == "failed"
|
||||
|
||||
|
||||
def test_reader_accepts_compatible_minor_and_unknown_object_field() -> None:
|
||||
payload = loads(render_json_report(_success_review(), detail="full"))
|
||||
payload["schema_version"] = "1.9"
|
||||
payload["future_summary"] = {"available": True}
|
||||
payload["hash_contract"]["future_hash_note"] = "compatible"
|
||||
payload["coordinate_contract"]["future_coordinate_note"] = "compatible"
|
||||
|
||||
parsed = parse_json_report(dumps(payload, ensure_ascii=False))
|
||||
|
||||
assert parsed["schema_version"] == "1.9"
|
||||
assert parsed["future_summary"] == {"available": True}
|
||||
|
||||
|
||||
def test_reader_rejects_stage_after_that_was_not_produced_by_changes() -> None:
|
||||
payload = loads(render_json_report(_success_review(), detail="full"))
|
||||
stage = payload["stages"][1]
|
||||
forged = "伪造结果"
|
||||
forged_hash = DocumentSnapshot(forged).sha256
|
||||
stage["after"] = {
|
||||
"sha256": forged_hash,
|
||||
"code_point_length": len(forged),
|
||||
"markdown": forged,
|
||||
}
|
||||
stage["changes"][0]["after_sha256"] = forged_hash
|
||||
payload["current"] = {
|
||||
"sha256": forged_hash,
|
||||
"code_point_length": len(forged),
|
||||
"markdown": forged,
|
||||
}
|
||||
|
||||
with pytest.raises(ReviewParseError, match="replayed changes do not produce"):
|
||||
parse_json_report(dumps(payload, ensure_ascii=False), expected_detail="full")
|
||||
|
||||
|
||||
def test_reader_rejects_changed_zero_change_stage() -> None:
|
||||
payload = loads(render_json_report(_success_review(), detail="full"))
|
||||
stage = payload["stages"][0]
|
||||
forged = "改变了零修改阶段"
|
||||
forged_hash = DocumentSnapshot(forged).sha256
|
||||
stage["after"] = {
|
||||
"sha256": forged_hash,
|
||||
"code_point_length": len(forged),
|
||||
"markdown": forged,
|
||||
}
|
||||
|
||||
with pytest.raises(ReviewParseError, match="zero-change stage"):
|
||||
parse_json_report(dumps(payload, ensure_ascii=False), expected_detail="full")
|
||||
|
||||
|
||||
def test_reader_rejects_conflicting_change_batch() -> None:
|
||||
payload = loads(render_json_report(_success_review(), detail="full"))
|
||||
stage = payload["stages"][1]
|
||||
duplicate = dict(stage["changes"][0])
|
||||
duplicate["edit_index"] = 1
|
||||
stage["changes"].append(duplicate)
|
||||
stage["change_count"] = 2
|
||||
payload["counts"]["change_count"] = 2
|
||||
|
||||
with pytest.raises(ReviewParseError, match="edit contract"):
|
||||
parse_json_report(dumps(payload, ensure_ascii=False), expected_detail="full")
|
||||
|
||||
|
||||
def test_reader_rejects_wrong_location_without_leaking_text() -> None:
|
||||
payload = loads(render_json_report(_success_review(), detail="full"))
|
||||
change = payload["stages"][1]["changes"][0]
|
||||
change["location"] = {"line": 99, "column": 99}
|
||||
|
||||
with pytest.raises(ReviewParseError) as error:
|
||||
parse_json_report(dumps(payload, ensure_ascii=False), expected_detail="full")
|
||||
|
||||
assert "🙂Cafe\u0301" not in str(error.value)
|
||||
assert "location does not match" in str(error.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("report", "message"),
|
||||
(
|
||||
('{"schema_name":"a","schema_name":"b"}', "keys must be unique"),
|
||||
('{"value":NaN}', "non-finite"),
|
||||
('{"schema_name":"mdpolish.review","schema_version":"2.0"}', "schema major"),
|
||||
(dumps({"schema_name": "\ud800"}), "valid Unicode"),
|
||||
("not JSON", "malformed JSON"),
|
||||
),
|
||||
)
|
||||
def test_reader_rejects_malformed_or_unsupported_json(report: str, message: str) -> None:
|
||||
with pytest.raises(ReviewParseError, match=message):
|
||||
parse_json_report(report)
|
||||
|
||||
|
||||
def test_reader_rejects_detail_mismatch_and_invalid_expected_detail() -> None:
|
||||
report = render_json_report(_success_review(), detail="summary")
|
||||
|
||||
with pytest.raises(ReviewParseError, match="does not match"):
|
||||
parse_json_report(report, expected_detail="full")
|
||||
with pytest.raises(ReviewParseError, match="expected_detail"):
|
||||
parse_json_report(report, expected_detail="unknown")
|
||||
|
||||
|
||||
def test_reader_enforces_body_exposure_boundaries() -> None:
|
||||
summary = loads(render_json_report(_success_review(), detail="summary"))
|
||||
summary["future"] = {"markdown": "SUMMARY_SECRET"}
|
||||
with pytest.raises(ReviewParseError, match="body-bearing"):
|
||||
parse_json_report(dumps(summary, ensure_ascii=False))
|
||||
|
||||
changes = loads(render_json_report(_success_review(), detail="changes"))
|
||||
changes["future"] = {"markdown": "CHANGES_SECRET"}
|
||||
with pytest.raises(ReviewParseError, match="full markdown"):
|
||||
parse_json_report(dumps(changes, ensure_ascii=False))
|
||||
@@ -0,0 +1,303 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import DocumentSnapshot, Modifier, Pipeline, ProposedChange, TextEdit, TextSpan
|
||||
from mdpolish.review import build_review_document, render_json_report
|
||||
from mdpolish.reviewer import ReviewDataError, ReviewRepository, create_server, main
|
||||
|
||||
|
||||
def _object(value: object) -> dict[str, object]:
|
||||
assert isinstance(value, dict)
|
||||
return cast(dict[str, object], value)
|
||||
|
||||
|
||||
def _array(value: object) -> list[object]:
|
||||
assert isinstance(value, list)
|
||||
return cast(list[object], value)
|
||||
|
||||
|
||||
def _full_review_json(source: str = "😀 old\r\nlast line\r\n") -> str:
|
||||
"""生成包含 emoji、CRLF、一次修改和一个零修改阶段的虚构 full JSON。"""
|
||||
|
||||
def replace_old(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
start = snapshot.markdown.find("old")
|
||||
if start < 0:
|
||||
return ()
|
||||
return (
|
||||
ProposedChange(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
reason="替换虚构测试词",
|
||||
edits=(
|
||||
TextEdit(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
span=TextSpan(start, start + 3),
|
||||
expected_text="old",
|
||||
replacement="new",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
pipeline = Pipeline(
|
||||
(
|
||||
Modifier(
|
||||
modifier_id="test.replace",
|
||||
version="1.0.0",
|
||||
parameters={"mode": "exact"},
|
||||
applicability="只替换虚构测试词。",
|
||||
propose=replace_old,
|
||||
),
|
||||
Modifier(
|
||||
modifier_id="test.zero",
|
||||
version="1.0.0",
|
||||
parameters={},
|
||||
applicability="用于验证零修改阶段。",
|
||||
propose=lambda _snapshot: (),
|
||||
),
|
||||
)
|
||||
)
|
||||
result = pipeline.transform(source)
|
||||
review = build_review_document(source, result)
|
||||
return render_json_report(review, detail="full") + "\n"
|
||||
|
||||
|
||||
def _review_directory(tmp_path: Path, content: str | None = None) -> Path:
|
||||
review_dir = tmp_path / "reviews"
|
||||
review_dir.mkdir(parents=True)
|
||||
(review_dir / "paper.review.json").write_text(
|
||||
_full_review_json() if content is None else content,
|
||||
encoding="utf-8",
|
||||
newline="",
|
||||
)
|
||||
return review_dir
|
||||
|
||||
|
||||
def test_repository_exposes_collection_document_and_modifier_stages(tmp_path: Path) -> None:
|
||||
repository = ReviewRepository(_review_directory(tmp_path))
|
||||
|
||||
collection = repository.collection_response()
|
||||
document = repository.document("paper").document_response()
|
||||
first_stage = repository.document("paper").stage_response(0)
|
||||
zero_stage = repository.document("paper").stage_response(1)
|
||||
|
||||
assert collection["collection"] == {"label": "reviews", "status": "success"}
|
||||
assert collection["summary"] == {
|
||||
"document_count": 1,
|
||||
"success_count": 1,
|
||||
"failed_count": 0,
|
||||
"unstable_count": 0,
|
||||
"change_count": 1,
|
||||
}
|
||||
assert "input_markdown" not in json.dumps(collection)
|
||||
assert document["input_markdown"] == "😀 old\r\nlast line\r\n"
|
||||
assert document["current_markdown"] == "😀 new\r\nlast line\r\n"
|
||||
modifiers = _array(document["modifiers"])
|
||||
assert [_object(item)["modifier_id"] for item in modifiers] == ["test.replace", "test.zero"]
|
||||
changes = _array(document["changes"])
|
||||
# Python 码点 2 位于 emoji 后; CodeMirror UTF-16 下标因此是 3。
|
||||
assert _object(changes[0])["span"] == {"start": 2, "end": 5}
|
||||
assert _object(changes[0])["editor_range"] == {"start": 3, "end": 6}
|
||||
assert first_stage["before_markdown"] == "😀 old\r\nlast line\r\n"
|
||||
assert first_stage["after_markdown"] == "😀 new\r\nlast line\r\n"
|
||||
assert zero_stage["before_markdown"] == zero_stage["after_markdown"]
|
||||
assert zero_stage["changes"] == []
|
||||
|
||||
|
||||
def test_repository_handles_unicode_empty_text_and_file_labels(tmp_path: Path) -> None:
|
||||
source = "中e\u0301😀\r\nold\rend"
|
||||
repository = ReviewRepository(_review_directory(tmp_path, _full_review_json(source)))
|
||||
document = repository.document("paper").document_response()
|
||||
changes = _array(document["changes"])
|
||||
|
||||
assert _object(changes[0])["location"] == {"line": 2, "column": 1}
|
||||
assert _object(changes[0])["span"] == {"start": 6, "end": 9}
|
||||
assert _object(changes[0])["editor_range"] == {"start": 7, "end": 10}
|
||||
assert document["current_markdown"] == "中e\u0301😀\r\nnew\rend"
|
||||
assert _object(document["document"])["source_label"] == "paper"
|
||||
|
||||
empty_directory = tmp_path / "empty"
|
||||
empty_directory.mkdir()
|
||||
(empty_directory / "empty.review.json").write_text(
|
||||
_full_review_json(""),
|
||||
encoding="utf-8",
|
||||
newline="",
|
||||
)
|
||||
empty_document = ReviewRepository(empty_directory).document("empty").document_response()
|
||||
assert empty_document["input_markdown"] == ""
|
||||
assert empty_document["current_markdown"] == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation",
|
||||
(
|
||||
lambda payload: payload.__setitem__("detail", "changes"),
|
||||
lambda payload: payload["input"].__setitem__(
|
||||
"markdown",
|
||||
payload["input"]["markdown"].replace("old", "bad"),
|
||||
),
|
||||
lambda payload: payload["stages"][0]["changes"][0]["span"].__setitem__("start", 0),
|
||||
),
|
||||
)
|
||||
def test_repository_rejects_untrusted_review_shapes(tmp_path: Path, mutation: object) -> None:
|
||||
payload = json.loads(_full_review_json())
|
||||
assert callable(mutation)
|
||||
mutation(payload)
|
||||
review_dir = _review_directory(tmp_path, json.dumps(payload, ensure_ascii=False) + "\n")
|
||||
|
||||
with pytest.raises(ReviewDataError) as raised:
|
||||
ReviewRepository(review_dir)
|
||||
|
||||
assert raised.value.code == "invalid_review"
|
||||
assert str(raised.value).startswith("paper.review.json:")
|
||||
|
||||
|
||||
def test_repository_accepts_compatible_schema_minor(tmp_path: Path) -> None:
|
||||
payload = json.loads(_full_review_json())
|
||||
payload["schema_version"] = "1.4"
|
||||
payload["future_summary"] = {"available": True}
|
||||
|
||||
repository = ReviewRepository(
|
||||
_review_directory(tmp_path, json.dumps(payload, ensure_ascii=False) + "\n")
|
||||
)
|
||||
|
||||
assert repository.document("paper").status == "success"
|
||||
|
||||
|
||||
def test_repository_rejects_bom_invalid_utf8_and_symlinks(tmp_path: Path) -> None:
|
||||
review_dir = _review_directory(tmp_path)
|
||||
review_path = review_dir / "paper.review.json"
|
||||
review_path.write_bytes(b"\xef\xbb\xbf" + review_path.read_bytes())
|
||||
with pytest.raises(ReviewDataError, match="BOM"):
|
||||
ReviewRepository(review_dir)
|
||||
|
||||
review_path.write_bytes(b'{"bad": "\xff"}')
|
||||
with pytest.raises(ReviewDataError, match="UTF-8"):
|
||||
ReviewRepository(review_dir)
|
||||
|
||||
review_path.unlink()
|
||||
target = tmp_path / "outside.review.json"
|
||||
target.write_text(_full_review_json(), encoding="utf-8", newline="")
|
||||
review_path.symlink_to(target)
|
||||
with pytest.raises(ReviewDataError) as raised:
|
||||
ReviewRepository(review_dir)
|
||||
assert raised.value.code == "unsafe_path"
|
||||
|
||||
review_path.unlink()
|
||||
review_path.write_text(_full_review_json(), encoding="utf-8", newline="")
|
||||
link = tmp_path / "review-link"
|
||||
link.symlink_to(review_dir, target_is_directory=True)
|
||||
with pytest.raises(ReviewDataError) as raised:
|
||||
ReviewRepository(link)
|
||||
assert raised.value.code == "unsafe_path"
|
||||
|
||||
|
||||
def test_repository_rejects_empty_directory_and_empty_label(tmp_path: Path) -> None:
|
||||
empty = tmp_path / "empty"
|
||||
empty.mkdir()
|
||||
with pytest.raises(ReviewDataError) as raised:
|
||||
ReviewRepository(empty)
|
||||
assert raised.value.code == "empty_review_directory"
|
||||
|
||||
(empty / ".review.json").write_text(_full_review_json(), encoding="utf-8", newline="")
|
||||
with pytest.raises(ReviewDataError, match="缺少文档标签"):
|
||||
ReviewRepository(empty)
|
||||
|
||||
|
||||
def test_console_entry_reports_expected_startup_failure_without_traceback(tmp_path: Path) -> None:
|
||||
empty = tmp_path / "empty"
|
||||
empty.mkdir()
|
||||
|
||||
with pytest.raises(SystemExit) as raised:
|
||||
main(["--review-dir", str(empty)])
|
||||
|
||||
assert str(raised.value) == "评审器启动失败:评审目录没有直属 full review JSON。"
|
||||
|
||||
|
||||
def test_http_server_is_loopback_read_only_and_does_not_leak_text(tmp_path: Path) -> None:
|
||||
repository = ReviewRepository(_review_directory(tmp_path))
|
||||
static_root = tmp_path / "static"
|
||||
static_root.mkdir()
|
||||
(static_root / "index.html").write_text("<!doctype html><title>review</title>", encoding="utf-8")
|
||||
server = create_server(repository, static_root)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
host = cast(str, server.server_address[0])
|
||||
port = server.server_address[1]
|
||||
|
||||
try:
|
||||
assert host == "127.0.0.1"
|
||||
connection = http.client.HTTPConnection(host, port)
|
||||
connection.request("GET", "/api/v1/collection")
|
||||
response = connection.getresponse()
|
||||
body = response.read().decode("utf-8")
|
||||
assert response.status == 200
|
||||
assert response.getheader("Cache-Control") == "no-store"
|
||||
assert response.getheader("Access-Control-Allow-Origin") is None
|
||||
assert "😀 old" not in body
|
||||
|
||||
connection.request("POST", "/api/v1/collection")
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
assert response.status == 405
|
||||
|
||||
connection.request("GET", "/api/v1/documents/missing")
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
assert response.status == 404
|
||||
|
||||
connection.request("GET", "/api/v1/documents/paper/modifiers/99")
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
assert response.status == 404
|
||||
|
||||
connection.request("HEAD", "/")
|
||||
response = connection.getresponse()
|
||||
assert response.status == 200
|
||||
assert response.read() == b""
|
||||
assert response.getheader("Content-Security-Policy") is not None
|
||||
assert response.getheader("X-Content-Type-Options") == "nosniff"
|
||||
|
||||
connection.putrequest("GET", "/api/v1/collection", skip_host=True)
|
||||
connection.putheader("Host", "example.com")
|
||||
connection.endheaders()
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
assert response.status == 403
|
||||
connection.close()
|
||||
|
||||
connection = http.client.HTTPConnection(host, port)
|
||||
connection.putrequest("GET", "/api/v1/collection")
|
||||
connection.putheader("Origin", "http://example.com")
|
||||
connection.endheaders()
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
assert response.status == 403
|
||||
connection.close()
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
def test_server_rejects_missing_or_symlink_static_root(tmp_path: Path) -> None:
|
||||
repository = ReviewRepository(_review_directory(tmp_path))
|
||||
missing = tmp_path / "missing-static"
|
||||
with pytest.raises(ReviewDataError) as raised:
|
||||
create_server(repository, missing)
|
||||
assert raised.value.code == "missing_build"
|
||||
|
||||
real = tmp_path / "real-static"
|
||||
real.mkdir()
|
||||
(real / "index.html").write_text("ok", encoding="utf-8")
|
||||
link = tmp_path / "static-link"
|
||||
link.symlink_to(real, target_is_directory=True)
|
||||
with pytest.raises(ReviewDataError) as raised:
|
||||
create_server(repository, link)
|
||||
assert raised.value.code == "missing_build"
|
||||
Reference in New Issue
Block a user