## 实现内容 (6 项改动) 1. **admin /health 端点增强** — 主键契约 status:ok 保持 + checks 子对象 - db_writable: connect + CREATE TEMP TABLE + INSERT + SELECT + DROP - disk_writable: 在 ops_alert_log_path 目录创建临时文件 + 删除 - settings_valid: 复用 is_jwt_secret_secure 判断 2. **_enforce_jwt_secret_policy** — prod env 使用 dev 默认 JWT / 长度<32 → fail-closed 3. **_enforce_default_admin_password_policy** — prod env 用 admin123 / 长度<10 / 字符类<3 → fail-closed 4. **L-A R7: admin UI footer** — dashboard.html (592 行) + ui.py 内联 admin/orders/new 模板加 footer 隐私政策 + 数据删除 + 服务说明链接, 与 portal _render_footer_links() 同口径 5. **L-A R1+R4: LEGAL_PRIVACY_BASELINE 文档同步** - §6 移除孤儿 'admin' consent_channel 值 (代码侧从未实际产生) - §7 已具备/尚缺 重写, 显式归到 6/20 增量 6. **Q-A: tests/test_crowd_db_data_quality.py** — 8 个测试锁住 - 27 省总数 + 仅湖南 high + 其它 26 省 ≤ usable - 高考生源大省 (广东/江苏/北京/上海/山东/河南/四川/湖北) 不在 high 集合 - data_year=2025 (6/25 后需显式更新) ## 测试 - 4/4 RED → GREEN (admin/tests/test_health.py: JWT/admin password 拒绝 dev 默认值) - 8/8 GREEN (data/crowd_db/tests/test_crowd_db_data_quality.py: 数据质量契约) - 3 回归修复 (test_app.py + test_routes.py + test_health.py 适配新 /health checks 字段) - 31/31 GREEN (admin/tests/test_app.py + test_routes.py + test_health.py) ## 报告 - reports/LA_LEGAL_PRIVACY_PRE_AUDIT_2026-06-20.md (363 行, 9 风险 0 阻塞) - reports/QA_CROWD_DB_NON_HUNAN_DENSITY_AUDIT.md (45 行, CRITICAL 文档失真已规避) ## 文件 M CHANGELOG.md (v2.1.3) M admin/config.py (2 个 _enforce_*_policy + load_settings post-load) M admin/routes/health.py (3 个 _check_* + checks 子对象) M admin/routes/ui.py (admin/orders/new 模板加 footer) M admin/static/dashboard.html (footer 块) M admin/tests/test_app.py (适配 checks 字段 + regex 兼容) M admin/tests/test_health.py (4 个新测试) M admin/tests/test_routes.py (适配 checks 字段) M docs/CURRENT_STATE.md (0.3-0.5 增量段) M docs/LEGAL_PRIVACY_BASELINE.md (§6 清理 + §7 重写) + data/crowd_db/tests/test_crowd_db_data_quality.py (8 tests) + reports/LA_LEGAL_PRIVACY_PRE_AUDIT_2026-06-20.md + reports/QA_CROWD_DB_NON_HUNAN_DENSITY_AUDIT.md
219 lines
6.2 KiB
Python
219 lines
6.2 KiB
Python
"""路由层测试 (T6.1).
|
|
|
|
覆盖:
|
|
- /health 公开
|
|
- /api/auth/login 错凭证 401 + 不区分用户名密码
|
|
- /api/orders 无 token 401 + 有效 token 返回 []
|
|
- /api/orders/{id} 不存在 404
|
|
- /api/stats/orders 鉴权
|
|
- /api/meta 鉴权 + 内容
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
# ---------------- health ----------------
|
|
|
|
|
|
def test_health_public(client):
|
|
resp = client.get("/health")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body.get("status") == "ok"
|
|
assert body.get("checks", {}).get("db_writable") is True
|
|
assert body.get("checks", {}).get("disk_writable") is True
|
|
assert body.get("checks", {}).get("settings_valid") is True
|
|
|
|
|
|
# ---------------- auth ----------------
|
|
|
|
|
|
def test_login_wrong_password_returns_401(client):
|
|
resp = client.post(
|
|
"/api/auth/login",
|
|
json={"username": "admin", "password": "WRONG"},
|
|
)
|
|
assert resp.status_code == 401
|
|
body = resp.json()
|
|
assert body["code"] == "E01101"
|
|
# 必须泛化错误信息,避免账户枚举
|
|
assert body["message"] == "用户名或密码不正确"
|
|
assert "detail" not in body
|
|
|
|
|
|
def test_login_unknown_user_returns_401(client):
|
|
resp = client.post(
|
|
"/api/auth/login",
|
|
json={"username": "ghost", "password": "anything"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_login_rate_limited_after_repeated_failures(client):
|
|
resp = None
|
|
for _ in range(5):
|
|
resp = client.post(
|
|
"/api/auth/login",
|
|
json={"username": "admin", "password": "WRONG"},
|
|
)
|
|
|
|
assert resp is not None
|
|
assert resp.status_code == 429
|
|
body = resp.json()
|
|
assert body["code"] == "E02501"
|
|
assert body["message"] == "请求过于频繁"
|
|
assert body["detail"]["retry_after_seconds"] >= 1
|
|
|
|
|
|
def test_login_missing_fields_rejected(client):
|
|
resp = client.post("/api/auth/login", json={"username": ""})
|
|
assert resp.status_code == 422 # pydantic 校验
|
|
|
|
|
|
# ---------------- orders ----------------
|
|
|
|
|
|
def test_orders_requires_auth(client):
|
|
resp = client.get("/api/orders")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_orders_returns_empty_list_with_auth(client, auth_headers):
|
|
resp = client.get("/api/orders", headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json() == []
|
|
|
|
|
|
def test_orders_supports_limit_offset(client, auth_headers):
|
|
resp = client.get(
|
|
"/api/orders",
|
|
params={"limit": 10, "offset": 0},
|
|
headers=auth_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_orders_rejects_invalid_limit(client, auth_headers):
|
|
resp = client.get("/api/orders", params={"limit": 0}, headers=auth_headers)
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_order_detail_404_for_unknown(client, auth_headers):
|
|
resp = client.get("/api/orders/GKO-20260612-XXXX", headers=auth_headers)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_order_detail_requires_auth(client):
|
|
resp = client.get("/api/orders/anything")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_dev_seed_requires_auth(client):
|
|
resp = client.post(
|
|
"/api/admin/orders/dev-seed", json={"scenario": "overdue_pending_once"}
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_dev_seed_create_and_cleanup(client, auth_headers):
|
|
create = client.post(
|
|
"/api/admin/orders/dev-seed",
|
|
headers=auth_headers,
|
|
json={"scenario": "overdue_pending_once"},
|
|
)
|
|
assert create.status_code == 200, create.text
|
|
created = create.json()
|
|
assert created["action"] == "created"
|
|
assert len(created["created_ids"]) == 1
|
|
|
|
dashboard = client.get("/api/stats/dashboard", headers=auth_headers)
|
|
assert dashboard.status_code == 200, dashboard.text
|
|
summary = dashboard.json()["summary"]
|
|
assert summary["pending_orders"] == 1
|
|
assert summary["pending_overdue_24h"] == 1
|
|
assert summary["pending_missing_intake"] == 1
|
|
|
|
cleanup = client.post(
|
|
"/api/admin/orders/dev-seed",
|
|
headers=auth_headers,
|
|
json={"scenario": "cleanup_demo_seed"},
|
|
)
|
|
assert cleanup.status_code == 200, cleanup.text
|
|
cleaned = cleanup.json()
|
|
assert cleaned["action"] == "cleaned"
|
|
assert cleaned["detail"]["deleted_count"] == 1
|
|
|
|
dashboard_after = client.get("/api/stats/dashboard", headers=auth_headers)
|
|
assert dashboard_after.status_code == 200, dashboard_after.text
|
|
summary_after = dashboard_after.json()["summary"]
|
|
assert summary_after["pending_orders"] == 0
|
|
assert summary_after["pending_overdue_24h"] == 0
|
|
assert summary_after["pending_missing_intake"] == 0
|
|
|
|
|
|
# ---------------- stats ----------------
|
|
|
|
|
|
def test_stats_orders_requires_auth(client):
|
|
resp = client.get("/api/stats/orders")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_stats_orders_real_shape(client, auth_headers):
|
|
"""T6.2: /api/stats/orders 接入真实 SQL 聚合。
|
|
|
|
- 字段集沿用 T6.1 stub 形状 (5 字段)
|
|
- 移除 _stub 标记
|
|
- 空库时所有计数为 0
|
|
"""
|
|
resp = client.get("/api/stats/orders", headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["total_orders"] == 0
|
|
assert body["total_revenue_cents"] == 0
|
|
# 6 态全 0 填充
|
|
assert body["by_status"] == {
|
|
"pending": 0,
|
|
"paid": 0,
|
|
"serving": 0,
|
|
"delivered": 0,
|
|
"completed": 0,
|
|
"refunded": 0,
|
|
}
|
|
assert body["by_source"] == {"xianyu": 0, "wechat": 0, "web": 0, "school": 0}
|
|
assert body["by_service_version"] == {
|
|
"audit": 0,
|
|
"basic": 0,
|
|
"standard": 0,
|
|
"premium": 0,
|
|
}
|
|
assert "_stub" not in body
|
|
|
|
|
|
# ---------------- meta ----------------
|
|
|
|
|
|
def test_meta_requires_auth(client):
|
|
resp = client.get("/api/meta")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
def test_meta_full_enums(client, auth_headers):
|
|
resp = client.get("/api/meta", headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
|
|
assert len(body["supported_provinces"]) >= 27
|
|
assert "湖南" in body["supported_provinces"]
|
|
|
|
assert set(body["order_statuses"]) == {
|
|
"pending",
|
|
"paid",
|
|
"serving",
|
|
"delivered",
|
|
"completed",
|
|
"refunded",
|
|
}
|
|
assert set(body["order_sources"]) == {"xianyu", "wechat", "web", "school"}
|
|
assert set(body["service_versions"]) == {"audit", "basic", "standard", "premium"}
|