配置层: - Settings 新增 llm_provider/api_key/base_url/model/timeout/max_tokens - 生产 fail-closed: GAOKAO_LLM_PROVIDER=none 禁止, provider!=none 且 API key 为空禁止 - .env.docker.example / .env.payment.example 补 LLM 变量 - payment_readiness_doctor 将 LLM 配置纳入 readiness 检查 基础设施: - 新增 data/llm/client.py: OpenAI-compatible LLMClient - 新增 data/llm/prompts.py: audit/cwb/full_plan prompt 模板 - 新增 data/llm/tests/test_llm.py: 12 个单元测试 主链接入: - ReviewResultContract 新增 llm_generated / llm_summary / llm_cwb_suggestions - _start_review_result 优先尝试 LLM 生成审核结果, 失败时回退到原规则默认逻辑 - cwb 页面优先展示 LLM 生成的三档建议 测试适配: - conftest / health / app / p2_4 tests 注入默认 LLM 测试配置,避免被新 fail-closed 提前拦截 验证: - data/llm/tests 12 passed - 核心 prod settings tests 62 passed
79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
from __future__ import annotations
|
||
|
||
|
||
def test_health_endpoint_returns_minimal_readiness_only(client):
|
||
"""公开健康检查:保持 `status: ok` 主键 + 详细 checks 字段。"""
|
||
resp = client.get("/health")
|
||
assert resp.status_code == 200
|
||
body = resp.json()
|
||
# 主键契约:status 必须存在且为 ok(PRODUCTION_DEPLOYMENT_CHECKLIST §4)
|
||
assert body.get("status") == "ok"
|
||
# 6/20 加固:增 checks 子对象,断言关键 readiness 字段
|
||
assert "checks" in body
|
||
checks = body["checks"]
|
||
assert checks.get("db_writable") is True
|
||
assert checks.get("disk_writable") is True
|
||
# settings_ok=True 意味着 prod fail-closed 通过(默认 dev 环境)
|
||
|
||
|
||
def test_load_settings_prod_rejects_dev_jwt_secret(monkeypatch, tmp_path):
|
||
"""GAOKAO_ENV=prod 时使用 dev 默认 JWT secret 必须 fail-closed。"""
|
||
import pytest
|
||
from admin.config import load_settings
|
||
|
||
monkeypatch.setenv("GAOKAO_ENV", "prod")
|
||
monkeypatch.setenv(
|
||
"GAOKAO_JWT_SECRET", "dev-only-do-not-use-in-prod-please-override-via-env"
|
||
)
|
||
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "x" * 32)
|
||
monkeypatch.setenv("GAOKAO_PORTAL_TOKEN_SECRET", "y" * 32)
|
||
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "StrongPass1!")
|
||
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
|
||
monkeypatch.setenv("GAOKAO_LLM_PROVIDER", "dashscope")
|
||
monkeypatch.setenv("GAOKAO_LLM_API_KEY", "sk-test")
|
||
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(tmp_path / "orders.db"))
|
||
monkeypatch.setenv("GAOKAO_DB_PATH", str(tmp_path / "admin.db"))
|
||
|
||
with pytest.raises(RuntimeError, match="JWT.*dev|JWT.*prod|密钥"):
|
||
load_settings()
|
||
|
||
|
||
def test_load_settings_prod_rejects_short_jwt_secret(monkeypatch, tmp_path):
|
||
"""GAOKAO_ENV=prod 时 JWT secret 长度 < 32 必须 fail-closed。"""
|
||
import pytest
|
||
from admin.config import load_settings
|
||
|
||
monkeypatch.setenv("GAOKAO_ENV", "prod")
|
||
monkeypatch.setenv("GAOKAO_JWT_SECRET", "short_secret_under_32")
|
||
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "x" * 32)
|
||
monkeypatch.setenv("GAOKAO_PORTAL_TOKEN_SECRET", "y" * 32)
|
||
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "StrongPass1!")
|
||
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
|
||
monkeypatch.setenv("GAOKAO_LLM_PROVIDER", "dashscope")
|
||
monkeypatch.setenv("GAOKAO_LLM_API_KEY", "sk-test")
|
||
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(tmp_path / "orders.db"))
|
||
monkeypatch.setenv("GAOKAO_DB_PATH", str(tmp_path / "admin.db"))
|
||
|
||
with pytest.raises(RuntimeError, match="JWT.*长度|32"):
|
||
load_settings()
|
||
|
||
|
||
def test_load_settings_prod_rejects_default_admin_password(monkeypatch, tmp_path):
|
||
"""GAOKAO_ENV=prod 时 GAOKAO_ADMIN_PASS=admin123 必须 fail-closed。"""
|
||
import pytest
|
||
from admin.config import load_settings
|
||
|
||
monkeypatch.setenv("GAOKAO_ENV", "prod")
|
||
monkeypatch.setenv("GAOKAO_JWT_SECRET", "z" * 32)
|
||
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "x" * 32)
|
||
monkeypatch.setenv("GAOKAO_PORTAL_TOKEN_SECRET", "y" * 32)
|
||
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "admin123")
|
||
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
|
||
monkeypatch.setenv("GAOKAO_LLM_PROVIDER", "dashscope")
|
||
monkeypatch.setenv("GAOKAO_LLM_API_KEY", "sk-test")
|
||
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(tmp_path / "orders.db"))
|
||
monkeypatch.setenv("GAOKAO_DB_PATH", str(tmp_path / "admin.db"))
|
||
|
||
with pytest.raises(RuntimeError, match="admin|默认管理员密码"):
|
||
load_settings()
|