|
7 | 7 | import urllib.parse |
8 | 8 | import urllib.request |
9 | 9 | from functools import partial |
10 | | -from http.server import ThreadingHTTPServer |
| 10 | +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer |
11 | 11 | from pathlib import Path |
12 | 12 |
|
| 13 | +import pytest |
13 | 14 | from store_dashboard_helpers import ( |
14 | 15 | SESSION_ID, |
15 | 16 | _assert_contract, |
|
31 | 32 | ) |
32 | 33 |
|
33 | 34 |
|
| 35 | +def test_dashboard_server_forces_dashboard_asset_mime_types( |
| 36 | + tmp_path: Path, |
| 37 | + monkeypatch: pytest.MonkeyPatch, |
| 38 | +) -> None: |
| 39 | + from codex_usage_tracker.server import _UsageDashboardHandler |
| 40 | + |
| 41 | + def registry_text_plain_guess_type(self: SimpleHTTPRequestHandler, path: str) -> str: |
| 42 | + suffix = Path(path).suffix.lower() |
| 43 | + if suffix in {".css", ".js", ".json"}: |
| 44 | + return "text/plain" |
| 45 | + return "application/octet-stream" |
| 46 | + |
| 47 | + monkeypatch.setattr( |
| 48 | + SimpleHTTPRequestHandler, |
| 49 | + "guess_type", |
| 50 | + registry_text_plain_guess_type, |
| 51 | + ) |
| 52 | + |
| 53 | + asset_dir = tmp_path / "codex-usage-tracker-assets" |
| 54 | + locale_dir = asset_dir / "locales" |
| 55 | + locale_dir.mkdir(parents=True) |
| 56 | + (asset_dir / "dashboard.js").write_text("window.__dashboardLoaded = true;\n", encoding="utf-8") |
| 57 | + (asset_dir / "dashboard.css").write_text("body { color: black; }\n", encoding="utf-8") |
| 58 | + (locale_dir / "en.json").write_text('{"dashboard": "Usage"}\n', encoding="utf-8") |
| 59 | + |
| 60 | + handler = partial( |
| 61 | + _UsageDashboardHandler, |
| 62 | + directory=str(tmp_path), |
| 63 | + db_path=tmp_path / "usage.sqlite3", |
| 64 | + pricing_path=tmp_path / "pricing.json", |
| 65 | + allowance_path=tmp_path / "allowance.json", |
| 66 | + thresholds_path=tmp_path / "thresholds.json", |
| 67 | + projects_path=tmp_path / "projects.json", |
| 68 | + limit=5000, |
| 69 | + since=None, |
| 70 | + codex_home=tmp_path / ".codex", |
| 71 | + include_archived=False, |
| 72 | + dashboard_name="dashboard.html", |
| 73 | + context_chars=2000, |
| 74 | + api_token="test-token", |
| 75 | + context_api_enabled=True, |
| 76 | + refresh_lock=threading.Lock(), |
| 77 | + ) |
| 78 | + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) |
| 79 | + thread = threading.Thread(target=server.serve_forever, daemon=True) |
| 80 | + thread.start() |
| 81 | + responses = [] |
| 82 | + try: |
| 83 | + base_url = f"http://127.0.0.1:{server.server_port}/codex-usage-tracker-assets" |
| 84 | + expected_assets = { |
| 85 | + "dashboard.js": "text/javascript", |
| 86 | + "dashboard.css": "text/css", |
| 87 | + "locales/en.json": "application/json", |
| 88 | + } |
| 89 | + for asset_path, expected_content_type in expected_assets.items(): |
| 90 | + with urllib.request.urlopen( # noqa: S310 - local test server only |
| 91 | + f"{base_url}/{asset_path}", |
| 92 | + timeout=5, |
| 93 | + ) as response: |
| 94 | + content_type = response.headers.get("Content-Type") |
| 95 | + nosniff = response.headers.get("X-Content-Type-Options") |
| 96 | + responses.append((asset_path, expected_content_type, content_type, nosniff)) |
| 97 | + finally: |
| 98 | + server.shutdown() |
| 99 | + server.server_close() |
| 100 | + thread.join(timeout=5) |
| 101 | + |
| 102 | + assert len(responses) == len(expected_assets) |
| 103 | + for asset_path, expected_content_type, content_type, nosniff in responses: |
| 104 | + assert content_type is not None, asset_path |
| 105 | + assert content_type.split(";", 1)[0] == expected_content_type |
| 106 | + assert nosniff == "nosniff" |
| 107 | + |
| 108 | + |
34 | 109 | def test_dashboard_server_usage_api_refreshes_aggregate_rows(tmp_path: Path) -> None: |
35 | 110 | from codex_usage_tracker.server import _UsageDashboardHandler |
36 | 111 |
|
|
0 commit comments