"""HTTP / WebSocket 接口:用 TestClient 起完整的应用,Hub 经进程内假 broker 连模拟板子。""" import time import pytest from fastapi.testclient import TestClient from prrack.api import create_app from prrack.bus import MemBroker, MemBus from prrack.db import Db, now_ms from prrack.hub import Hub from prrack.recovery import RecoveryManager from prrack.simboard import SimBoard B = "b81f3fc9c8bc" @pytest.fixture def env(tmp_path): broker = MemBroker() db = Db(str(tmp_path / "a.db")) hub = Hub(db, MemBus(broker), attempt_timeout=0.2) rec = RecoveryManager(hub, now_ms) sim = SimBoard(MemBus(broker), B, present=(True, True, True, False, True)) dist = tmp_path / "dist" (dist / "assets").mkdir(parents=True) (dist / "index.html").write_text("rack", encoding="utf-8") async def up(): await hub.start() sim.start() sim.tick(now_ms()) app = create_app(hub, rec, str(dist), startup=[up], shutdown=[hub.stop]) with TestClient(app) as c: deadline = time.time() + 2 while B not in hub.latest and time.time() < deadline: time.sleep(0.01) yield c, hub, sim, db def test_boards_and_snapshot(env): c, hub, sim, db = env boards = c.get("/api/boards").json() assert [b["id"] for b in boards] == [B] assert boards[0]["online"] is True and len(boards[0]["phones"]) == 5 assert c.get("/api/boards/nope").status_code == 404 def test_command_records_the_portal_user(env): c, hub, sim, db = env r = c.post("/api/boards/%s/cmd" % B, json={"cmd": "power", "rail": 1, "on": False}, headers={"X-Auth-User": "zhouyang"}) assert r.status_code == 200 and r.json()["ok"] is True assert sim.rails[0].en is False a = c.get("/api/audit").json()[0] assert a["user"] == "zhouyang" and a["request"]["cmd"] == "power" and a["ok"] == 1 # 没有门户头(服务器本机直接 curl)记作 local c.post("/api/boards/%s/cmd" % B, json={"cmd": "power", "rail": 1, "on": True}) assert c.get("/api/audit").json()[0]["user"] == "local" def test_bad_commands_never_reach_the_board(env): c, hub, sim, db = env n = len(sim.received) for body in ({"cmd": "wifi", "ssid": "x"}, {"cmd": "set_voltage", "rail": 1, "mv": 4300}, {"cmd": "key", "rail": 1, "keys": "pwr", "ms": 60000}, {"cmd": "rm -rf"}): r = c.post("/api/boards/%s/cmd" % B, json=body) assert r.status_code == 400, body assert len(sim.received) == n def test_board_rejection_is_passed_through(env): c, hub, sim, db = env r = c.post("/api/boards/%s/cmd" % B, json={"cmd": "power_cycle", "rail": 4}) assert r.status_code == 200 and r.json()["err"] == "empty" def test_timeout_is_504(env): c, hub, sim, db = env sim.drop_next_replies = 99 r = c.post("/api/boards/%s/cmd" % B, json={"cmd": "power", "rail": 2, "on": True}) assert r.status_code == 504 def test_phone_settings_toggle_auto_recovery(env): c, hub, sim, db = env r = c.patch("/api/boards/%s/rails/3" % B, json={"label": "A-03", "auto_recover": True}) assert r.status_code == 200 ph = [p for p in r.json()["phones"] if p["rail"] == 3][0] assert ph["label"] == "A-03" and ph["auto_recover"] == 1 rec = c.get("/api/recovery").json() assert rec["params"]["window_s"] == 300 r = c.put("/api/recovery/params", json={"window_s": 120, "flat_ua": -5, "bogus": 1}) assert r.json()["window_s"] == 120 and r.json()["flat_ua"] == 5000 # 负数不收 def test_history_after_telemetry(env): c, hub, sim, db = env for _ in range(3): sim.tick(now_ms()) time.sleep(0.1) hub.flush() h = c.get("/api/boards/%s/rails/1/history" % B, params={"start": now_ms() - 60000}).json() assert h["resolution"] == "raw" and len(h["points"]) >= 1 assert h["points"][0][2] is not None def test_websocket_hello_and_live_tele(env): c, hub, sim, db = env with c.websocket_connect("/api/ws", headers={"X-Auth-User": "zhouyang"}) as ws: hello = ws.receive_json() assert hello["t"] == "hello" and hello["user"] == "zhouyang" and hello["boards"][0]["id"] == B sim.tick(now_ms()) msg = ws.receive_json() while msg["t"] != "tele": msg = ws.receive_json() assert msg["board"] == B and len(msg["status"]["rails"]) == 5 def test_spa_fallback_is_not_cached(env): c, hub, sim, db = env r = c.get("/some/deep/route") assert r.status_code == 200 and "rack" in r.text assert r.headers["cache-control"] == "no-cache" def test_a_real_websocket_library_is_installed(): """TestClient 自带 WS 实现,上面那条 WS 用例在没装 websockets 的机器上也是绿的; 而真 uvicorn 缺了它会把 /api/ws 当普通 GET(落到 SPA 兜底,回 200 HTML)。 deploy.sh 在服务器上跑这套测试,所以缺库会在换上去之前被拦住。""" from prrack.main import ws_library_ok assert ws_library_ok()