Files
gaokao-volunteer-system/admin/tests/test_app.py
Hermes Agent 1111bec32a
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled
feat(frontend): overhaul /dashboard layout, status badge, and empty states
- /dashboard 顶部加 system-status 徽章:尚未连接 / 已连接 / 异常 三态,背景渐变 + 圆点 + 元数据
- 顶部右侧加 quick-refresh / quick-logout 两个语义化动作
- 头部把原 3 个内嵌链接改成 quick-links pill 行,更专业
- 登录区加 id=login-form 与 aria-label,登录成功后自动折叠
- KPI 4 卡:订单数/收入/后台可登录用户/待处理订单
  - 每张卡片有 pill 标签与 data-card 钩子
  - 订单/收入两张卡加 spark 骨架线,登录后变 loaded
  - 空状态文案统一为「登录后查看 · 」
- 趋势图与 3 个分布图统一加 chart-empty 组件,未加载/无数据时显示占位
- JS:
  - 加 setStatus / setChartEmpty / markCardsLoading / markCardsEmpty
  - 失败或登出时统一清空所有图表并重置为占位
  - renderBarChart 在 dataMap 全为 0 时自动走空状态
  - 保留 sessionStorage,保留 localStorage 黑名单
  - 修复 pending_orders 的算式 bug
- 测试:test_app.py 加 system-status / quick-links / data-card / chart-empty / spark 等新结构断言,锁住产品化文案
2026-06-16 20:33:06 +08:00

230 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FastAPI app 启动 + Swagger 测试 (T6.1).
验证:
- app 工厂可用
- lifespan 触发 bootstrap
- /openapi.json 含所有路由
- /docs 返回 HTML
"""
from __future__ import annotations
import sqlite3
import pytest
def test_create_app_runs_lifespan(client):
"""TestClient 上下文会触发 lifespan,验证 bootstrap 完成。"""
# 客户端创建时 lifespan 已运行 → 已有 admin 用户
resp = client.get("/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "ok"
assert body["service"] == "gaokao-admin"
assert body["env"] == "dev"
def test_openapi_json_exposes_all_routes(client):
"""/openapi.json 含所有 T6.1 路由。"""
resp = client.get("/openapi.json")
assert resp.status_code == 200
schema = resp.json()
paths = set(schema["paths"].keys())
expected = {
"/health",
"/api/auth/login",
"/api/auth/me",
"/api/orders",
"/api/orders/{order_id}",
"/api/stats/dashboard",
"/api/stats/orders",
"/api/meta",
}
missing = expected - paths
assert not missing, f"OpenAPI 缺失路径: {missing}"
def test_docs_serves_swagger_ui(client):
"""/docs 返回 HTML(200 + 含 swagger-ui 字样)。"""
resp = client.get("/docs")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text.lower()
assert "swagger" in body or "openapi" in body
def test_redoc_served(client):
"""/redoc 也可用。"""
resp = client.get("/redoc")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
def test_dashboard_page_served(client):
"""/dashboard 提供运营后台页面骨架,并避免技术细节直出。"""
resp = client.get("/dashboard")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "运营总览" in body
assert "核心经营指标" in body
assert "/static/dashboard.js" in body
assert "/static/portal-ui.css" in body
assert 'id="trend-chart"' in body
assert 'id="status-chart"' in body
assert 'id="source-chart"' in body
assert 'id="service-chart"' in body
assert 'id="range-7d"' in body
assert 'id="range-30d"' in body
assert 'id="system-status"' in body
assert 'id="pending-orders"' in body
assert 'id="orders-spark"' in body
assert 'id="revenue-spark"' in body
assert 'id="status-title"' in body
assert 'id="quick-refresh-btn"' in body
assert 'id="quick-logout-btn"' in body
assert 'id="login-form"' in body
assert 'id="quick-links"' in body
assert 'class="card empty"' in body
assert 'class="chart-empty"' in body
assert 'data-card="orders"' in body
assert 'data-card="revenue"' in body
assert 'data-card="users"' in body
assert 'data-card="pending"' in body
assert "尚未连接" in body
assert "立即刷新" in body
assert "退出登录" in body
assert "等待加载状态分布" in body
assert "等待加载来源分布" in body
assert "等待加载服务版本分布" in body
assert "登录后展示订单与收入趋势" in body
assert "/static/vendor/echarts.min.js" in body
assert "jsdelivr.net" not in body
assert "最小仪表盘" not in body
assert "admin_users 总数" not in body
assert "接口: <code>/api/stats/dashboard</code>" not in body
def test_dashboard_static_js_served(client):
"""前端脚本包含趋势切换与 3 张分布图渲染逻辑。"""
resp = client.get("/static/dashboard.js")
assert resp.status_code == 200
assert "javascript" in resp.headers["content-type"]
body = resp.text
assert "renderSummary" in body
assert "/api/stats/dashboard" in body
assert "status-chart" in body
assert "source-chart" in body
assert "service-chart" in body
assert 'range: "7d"' in body
assert "trends?.[state.range]" in body
assert "sessionStorage" in body
assert "localStorage" not in body
assert "admin_users 总数" not in body
assert "setStatus" in body
assert "setChartEmpty" in body
assert "pending-orders" in body
def test_bootstrap_admin_only_once(client, settings):
"""lifespan 已 bootstrap 后,再次调用应报告已存在,不再创建。"""
from admin.db import AdminUserRepo, bootstrap_admin
# client fixture 已触发 lifespan → 已有 bootstrap 用户
created1, msg1 = bootstrap_admin(settings)
assert created1 is False
assert "已存在" in msg1
repo = AdminUserRepo(settings.db_path)
assert repo.count() == 1
def test_default_admin_login_works(client, settings):
"""默认 admin/admin123 (此处覆写为 test-pass-123) 可登录。"""
resp = client.post(
"/api/auth/login",
json={"username": "admin", "password": "test-pass-123"},
)
assert resp.status_code == 200
body = resp.json()
assert body["token_type"] == "bearer"
assert body["expires_in"] == settings.jwt_expire_minutes * 60
assert isinstance(body["access_token"], str)
assert len(body["access_token"]) > 20
def test_create_app_bootstraps_orders_schema(tmp_path, monkeypatch):
"""真实启动流程会顺手初始化 orders DBfresh app 的 dashboard 空库可用。"""
db_path = tmp_path / "admin.db"
orders_db_path = tmp_path / "orders.db"
monkeypatch.setenv("GAOKAO_ENV", "dev")
monkeypatch.setenv("GAOKAO_DB_PATH", str(db_path))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db_path))
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
from fastapi.testclient import TestClient
from admin.app import create_app
from admin.config import load_settings
app = create_app(load_settings())
with TestClient(app) as client:
login = client.post(
"/api/auth/login",
json={"username": "admin", "password": "test-pass-123"},
)
assert login.status_code == 200
token = login.json()["access_token"]
resp = client.get(
"/api/stats/dashboard",
headers={"Authorization": f"Bearer {token}"},
)
assert resp.status_code == 200
assert resp.json()["summary"] == {
"total_orders": 0,
"total_revenue_cents": 0,
"total_users": 1,
"orders_today": 0,
"orders_7d": 0,
"orders_30d": 0,
"revenue_today_cents": 0,
"revenue_7d_cents": 0,
"revenue_30d_cents": 0,
}
with sqlite3.connect(orders_db_path) as conn:
row = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='orders'"
).fetchone()
assert row is not None
def test_prod_rejects_default_admin_password(tmp_path, monkeypatch):
db_path = tmp_path / "admin.db"
orders_db_path = tmp_path / "orders.db"
monkeypatch.setenv("GAOKAO_ENV", "prod")
monkeypatch.setenv("GAOKAO_DB_PATH", str(db_path))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db_path))
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_PORTAL_TOKEN_SECRET", "Z" * 64)
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "admin123")
# 显式提供合规 webhook secret,避免被 P2-5 fail-closed 提前拦截,
# 让本测试聚焦于管理员密码策略。
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "P" + "r" * 31 + "!" * 32)
from fastapi.testclient import TestClient
from admin.app import create_app
from admin.config import load_settings
app = create_app(load_settings())
with pytest.raises(RuntimeError, match="default admin password insecure in prod"):
with TestClient(app):
pass