from fastapi.testclient import TestClient from app.db import auth_db from app.db.operations_dashboard import _display_error_summary, get_operations_dashboard from app.web import web_server def _login_user(email: str, admin: bool = False) -> str: reg = auth_db.register_user(email, "StrongPass123") auth_db.verify_email(email, reg["verification_code"]) user = auth_db.get_user_by_email(email) auth_db.claim_free_trial(user["id"]) if admin: auth_db.set_user_admin(email, True) return auth_db.login_user(email, "StrongPass123")["token"] def test_operations_page_and_api_require_admin(): token = _login_user("normal-ops@example.com") client = TestClient(web_server.app) client.cookies.set("altcoin_session", token) page = client.get("/operations") api = client.get("/api/admin/operations-dashboard") assert page.status_code == 403 assert api.status_code == 403 def test_operations_admin_can_access_dashboard(): token = _login_user("admin-ops@example.com", admin=True) client = TestClient(web_server.app) client.cookies.set("altcoin_session", token) page = client.get("/operations") api = client.get("/api/admin/operations-dashboard?hours=24") assert page.status_code == 200 assert "运行大屏" in page.text assert api.status_code == 200 data = api.json() assert "overall" in data assert "scheduler" in data assert "data_sources" in data assert "funnel" in data assert "trading" in data assert "timeline" in data def test_operations_dashboard_read_model_shape(pg_conn): data = get_operations_dashboard(hours=24) assert data["hours"] == 24 assert data["overall"]["status"] in {"ok", "warn", "danger"} assert isinstance(data["scheduler"], list) assert isinstance(data["data_sources"], list) assert isinstance(data["funnel"], list) assert isinstance(data["trading"], dict) def test_operations_dashboard_sanitizes_external_provider_errors(): summary = _display_error_summary( "market:HTTPSConnectionPool(host='api.binance.com', port=443): " "Max retries exceeded with url: /v1/secret (Caused by NameResolutionError())" ) assert summary == "外部服务连接异常" assert "HTTPSConnectionPool" not in summary assert "/v1/" not in summary