from __future__ import annotations from collections.abc import Mapping, Sequence from dataclasses import dataclass from pathlib import Path from fastapi.testclient import TestClient from game_data_capture.config import ServerConfig from game_data_capture.web.app import ( CaptureRecord, CaptureSessionStatus, ControlOutcome, MediaDescriptor, MutationOutcome, WebConfig, create_app, ) from game_data_capture.web.security import new_bootstrap_token _ID = "capture-00000000000000000000000000000002" @dataclass class CaptureControl: armed: bool = False def status(self) -> CaptureSessionStatus: return CaptureSessionStatus(self.armed, 1, 64) def start_session(self) -> ControlOutcome: self.armed = True return "started" def stop_session(self) -> ControlOutcome: self.armed = False return "stopped" class Corpus: def __init__(self) -> None: self.record = CaptureRecord(_ID, "stored", "2026-07-31T00:00:00Z", None) self.rejected = False self.deleted = False def list_captures(self, *, limit: int) -> Sequence[CaptureRecord]: return (self.record,) def get_capture(self, capture_id: str) -> CaptureRecord | None: return self.record if capture_id == _ID else None def open_media(self, capture_id: str) -> MediaDescriptor | None: return MediaDescriptor("image/png", 3, [b"png"]) if capture_id == _ID else None def label(self, capture_id: str, classification: Mapping[str, object]) -> MutationOutcome: if capture_id != _ID: return "not_found" self.record = CaptureRecord(_ID, "stored", "2026-07-31T00:00:00Z", dict(classification)) return "updated" def reject_preserving_evidence(self, capture_id: str) -> MutationOutcome: if capture_id != _ID: return "not_found" self.rejected = True return "updated" def delete_with_retention(self, capture_id: str) -> MutationOutcome: if capture_id != _ID: return "not_found" self.deleted = True return "updated" def test_private_capture_labeling_flow_preserves_rejection_before_retention_delete() -> None: token = new_bootstrap_token() control = CaptureControl() corpus = Corpus() client = TestClient( create_app( config=WebConfig(ServerConfig()), capture_control=control, corpus=corpus, bootstrap_token=token, ), base_url="http://127.0.0.1:8765", ) origin = "http://127.0.0.1:8765" csrf = str( client.post("/api/bootstrap", headers={"Origin": origin}, json={"token": token}).json()[ "csrf_token" ] ) label = { "family": "gear", "view": "detail", "gear_slot": "Mask", "record_types": ["gear", "brand_set"], "confidence": 1.0, "abstention_reason": None, } response = client.post( f"/api/captures/{_ID}/label", headers={"Origin": origin, "X-CSRF-Token": csrf}, json=label ) assert response.status_code == 200 csrf = str(response.json()["csrf_token"]) assert client.get(f"/api/captures/{_ID}").json()["capture"]["classification"] == label response = client.post( f"/api/captures/{_ID}/reject", headers={"Origin": origin, "X-CSRF-Token": csrf}, json={"confirmation": "REJECT"}, ) assert response.status_code == 200 csrf = str(response.json()["csrf_token"]) assert corpus.rejected is True response = client.post( f"/api/captures/{_ID}/delete", headers={"Origin": origin, "X-CSRF-Token": csrf}, json={"confirmation": "DELETE"}, ) assert response.status_code == 200 assert corpus.deleted is True def test_mod_labels_round_trip_through_the_reviewed_label_route() -> None: token = new_bootstrap_token() client = TestClient( create_app( config=WebConfig(ServerConfig()), capture_control=CaptureControl(), corpus=Corpus(), bootstrap_token=token, ), base_url="http://127.0.0.1:8765", ) origin = "http://127.0.0.1:8765" csrf = str( client.post("/api/bootstrap", headers={"Origin": origin}, json={"token": token}).json()[ "csrf_token" ] ) for family, record_type in (("weapon_mod", "weapon_mod"), ("gear_mod", "gear_mod")): label = { "family": family, "view": "detail", "gear_slot": None, "record_types": [record_type], "confidence": 1.0, "abstention_reason": None, } response = client.post( f"/api/captures/{_ID}/label", headers={"Origin": origin, "X-CSRF-Token": csrf}, json=label, ) assert response.status_code == 200 csrf = str(response.json()["csrf_token"]) assert client.get(f"/api/captures/{_ID}").json()["capture"]["classification"] == label def test_review_ui_uses_local_external_assets_and_keeps_guardrails_in_static_source() -> None: web_root = Path(__file__).parents[2] / "src" / "game_data_capture" / "web" template = (web_root / "templates" / "index.html").read_text(encoding="utf-8") script = (web_root / "static" / "review.js").read_text(encoding="utf-8") stylesheet = (web_root / "static" / "review.css").read_text(encoding="utf-8") assert 'href="/static/review.css"' in template assert 'src="/static/review.js"' in template assert "" not in template assert "http://" not in template + script + stylesheet assert "https://" not in template + script + stylesheet assert "innerHTML" not in script assert "Origin" not in script assert ( 'history.replaceState(null, "", `${window.location.pathname}${window.location.search}`)' in script ) assert '"/api/session/start"' in script assert "JSON.stringify({ confirmation })" in script assert "confidence: 1.0" in script assert 'credentials: "same-origin"' in script assert "localStorage" not in script assert "sessionStorage" not in script assert "document.cookie" not in script for expected in ( "weapon", "gear", "skill_chip", "os_protocol", "specialization", "weapon_mod", "gear_mod", "unknown", "Mask", "Gloves", "Backpack", "Body Armor", "Holster", "Knee Pads", "prefers-reduced-motion", ): assert expected in script + stylesheet def test_review_page_and_local_static_assets_serve_with_security_headers() -> None: client = TestClient( create_app( config=WebConfig(ServerConfig()), capture_control=CaptureControl(), corpus=Corpus(), bootstrap_token=new_bootstrap_token(), ), base_url="http://127.0.0.1:8765", ) page = client.get("/") stylesheet = client.get("/static/review.css") script = client.get("/static/review.js") for response in (page, stylesheet, script): assert response.status_code == 200 assert response.headers["content-security-policy"].startswith("default-src 'self'") assert response.headers["referrer-policy"] == "no-referrer" assert response.headers["x-content-type-options"] == "nosniff" assert response.headers["x-frame-options"] == "DENY" assert "http://" not in response.text assert "https://" not in response.text assert 'href="/static/review.css"' in page.text assert 'src="/static/review.js"' in page.text assert stylesheet.headers["content-type"].startswith("text/css") assert "csrfPattern" in script.text assert "setCheckedMembers" in script.text assert "announce: false" in script.text assert 'id="hide-reviewed"' in page.text