# -*- coding: utf-8 -*- """只读 Web 服务测试(离线): uv run python tests/test_web_server.py""" import http.client import json import os import sys import tempfile import threading import urllib.parse from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.stdout.reconfigure(encoding="utf-8") from reaction_board.config import ConfigStore # noqa: E402 from reaction_board.database import Database # noqa: E402 from reaction_board.web.server import create_server # noqa: E402 from tests.web_fixtures import build_db, write_config # noqa: E402 class RunningServer: def __init__(self, tmp: str, *, token: str = "", preview: bool = False): self.config = write_config(tmp, token=token) build_db(tmp) self.db = Database(Path(tmp) / "app.db") self.db.initialize() if preview: self.db.write(lambda conn: conn.execute( "INSERT OR REPLACE INTO kv(k, v) VALUES ('preview:seeded', '1')" )) self.store = ConfigStore(self.config) self.server = create_server( self.store, self.db, port=0, preview=preview, clock=lambda: 1_793_459_000, ) self.port = self.server.server_address[1] self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() def request(self, path: str, *, method: str = "GET", headers=None, body=None): conn = http.client.HTTPConnection("127.0.0.1", self.port, timeout=5) conn.request(method, path, body=body, headers=headers or {}) response = conn.getresponse() payload = response.read() result = response.status, dict(response.getheaders()), payload conn.close() return result def json(self, path: str, **kwargs): status, headers, payload = self.request(path, **kwargs) return status, headers, json.loads(payload.decode("utf-8")) def close(self): self.server.shutdown() self.server.server_close() self.db.close() def test_readonly_routes_and_static(): with tempfile.TemporaryDirectory() as tmp: running = RunningServer(tmp) try: status, _, health = running.json("/api/health") assert status == 200 and health["ok"] is True assert health["data"]["preview"] is False status, _, payload = running.json("/api/session") assert status == 200 assert payload["data"]["auth_enabled"] is False assert payload["data"]["authenticated"] is True status, _, payload = running.json("/api/candidates") assert status == 200 and payload["ok"] is True status, _, payload = running.json("/api/system") assert status == 200 and payload["ok"] is True status, _, payload = running.json("/api/about") assert status == 200 assert payload["data"]["schema_version"] == 7 assert payload["data"]["analytics"]["min_duration_seconds"] == 600 status, _, overview = running.json("/api/overview") assert status == 200 and overview["ok"] is True assert overview["data"]["data_date"] == "2026-10-31" status, headers, payload = running.request("/") assert status == 200 assert "text/html" in headers["Content-Type"] assert b"= "2026-10-18" for point in fans["recent_points"] ) assert all("delta" in point for point in fans["recent_points"]) status, _, payload = running.request( "/api/fans?start=2026-10-29&end=2026-10-31" ) assert status == 200 data = json.loads(payload)["data"] assert data["mode"] == "daily" assert data["dates"] == ["2026-10-29", "2026-10-30", "2026-10-31"] running.db.write(lambda conn: conn.execute( "INSERT OR REPLACE INTO up_stat" "(stat_date, mid, follower, following, captured_at, ok) " "VALUES ('2025-01-01', 37754047, 500, NULL, 1, 1)" )) status, _, payload = running.request( "/api/fans?start=2025-01-01&end=2026-10-31" ) assert status == 200 assert json.loads(payload)["data"]["mode"] == "monthly" status, headers, payload = running.request("/api/videos", method="HEAD") assert status == 200 and payload == b"" assert "Content-Length" in headers # 只读:所有业务写方法都被拒绝,且数据库不变 before = running.db.read(lambda conn: conn.execute( "SELECT COUNT(*) FROM candidate" ).fetchone()[0]) for method in ("POST", "PUT", "PATCH", "DELETE"): status, _, _ = running.request("/api/videos/101", method=method) assert status == 405, method after = running.db.read(lambda conn: conn.execute( "SELECT COUNT(*) FROM candidate" ).fetchone()[0]) assert after == before status, _, error = running.json("/api/videos?sort=bad") assert status == 400 and error["ok"] is False status, _, error = running.json("/api/videos?page=0") assert status == 400 status, _, error = running.json("/api/videos/abc") assert status == 400 status, _, error = running.json("/api/nope") assert status == 404 status, _, payload = running.request("/api/export.csv?sort=view") assert status == 404 status, headers, _ = running.request("/api/overview") assert headers["X-Content-Type-Options"] == "nosniff" assert "default-src 'self'" in headers["Content-Security-Policy"] assert headers["X-Frame-Options"] == "DENY" finally: running.close() def test_token_auth_and_login(): with tempfile.TemporaryDirectory() as tmp: running = RunningServer(tmp, token="test-token") try: # 公共页面与公共 API 访客可见 status, _, body = running.json("/api/overview") assert status == 200 and body["ok"] is True status, _, body = running.json("/api/videos") assert status == 200 and body["ok"] is True status, _, body = running.json("/api/fans?days=7") assert status == 200 and body["ok"] is True status, _, payload = running.request("/") assert status == 200 and b"