Files
gaokao-volunteer-system/admin/tests/test_web_public.py
Hermes Agent a86bd08281
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): polish landing, pricing, and checkout conversion pages
2026-06-16 17:14:51 +08:00

363 lines
13 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.
"""用户端 Web 自助入口页面测试T12.2/T12.3)。"""
from __future__ import annotations
from fastapi.testclient import TestClient
from data.orders.dao import OrdersDAO
def test_public_landing_page_served(client):
resp = client.get("/")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "高考志愿填报智能规划服务" in body
assert "新高考志愿填报 · 志愿决策支持" in body
assert "湖南新高考志愿填报" not in body
assert "为什么选择我们" in body
assert 'href="/pricing"' in body
assert "服务流程" in body
assert "志愿方案审计更聚焦" in body
assert "先判断现有方案值不值得继续" in body
assert "把风险解释清楚" in body
assert "先审计后规划" in body
assert "风险重点可解释" in body
assert "进度站内可查" in body
assert "隐私与删除入口可见" in body
assert "01" in body
assert "04" in body
assert "家长决策支持" not in body
assert "家长联系方式" not in body
assert "把风险解释给家长听懂" not in body
assert '/static/portal-ui.css' in body
def test_public_pricing_page_served(client):
resp = client.get("/pricing")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "服务套餐" in body
assert "49元 AI方案审核" in body
assert "99元 完整志愿方案" in body
assert "199元 深度辅导版" in body
assert 'data-package="audit"' in body
assert 'data-package="standard"' in body
assert 'data-package="premium"' in body
assert "推荐方案" in body
assert "支付接入建设中" not in body
assert "先做快速审核" in body
assert "立即开始完整规划" in body
assert "了解深度辅导" in body
assert "先审计再决定" in body
assert '/static/portal-ui.css' in body
def test_public_checkout_page_served(client):
resp = client.get("/checkout/standard")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
body = resp.text
assert "完整志愿方案" in body
assert "订单摘要" in body
assert "服务保障" in body
assert "立即支付" in body
assert "状态站内可追踪" in body
assert "当前建议" in body
assert "交付方式" in body
assert "最小下单" not in body
assert '/static/portal-ui.css' in body
def test_public_create_order_endpoint(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"customer_email": "parent@example.com",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 201, resp.text
body = resp.json()
assert body["order_id"].startswith("GKO-")
assert body["source"] == "web"
assert body["status"] == "pending"
assert body["service_version"] == "standard"
assert body["amount_cents"] == 9900
assert body["next_step"] == "payment"
assert body["checkout_url"].startswith("/pay/mock/")
assert "/portal/" in body["portal_status_url"]
with OrdersDAO.connect(client.app.state.settings.orders_db_path) as dao:
created = dao.get(body["order_id"])
assert created.customer_email == "parent@example.com"
assert created.candidate_name == "张三"
def test_public_create_order_returns_503_with_friendly_message_when_encryption_key_missing(
tmp_path, monkeypatch
):
admin_db = tmp_path / "admin.db"
orders_db = tmp_path / "orders.db"
share_db = tmp_path / "share.db"
share_reports = tmp_path / "share_reports"
share_reports.mkdir()
monkeypatch.setenv("GAOKAO_ENV", "dev")
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
monkeypatch.delenv("GAOKAO_ORDERS_FERNET_KEY", raising=False)
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
from admin.app import create_app
from admin.config import load_settings
from data.orders import crypto
crypto.get_fernet.cache_clear()
settings = load_settings()
app = create_app(settings)
with TestClient(app) as client:
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 503, resp.text
body = resp.json()
assert "当前暂时无法创建订单" in body["detail"]["reason"]
def test_public_create_order_rejects_missing_minimal_fields(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "audit",
"amount_cents": 4900,
"customer_name": "李家长",
"candidate_province": "广东",
},
)
assert resp.status_code == 422
body = resp.json()
assert body["message"] == "请求数据未通过校验"
assert any(
field["field"] in {"body.customer_phone", "body.candidate_name"}
for field in body["detail"]["fields"]
)
def test_public_create_order_rejects_price_tampering(client):
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 1,
"customer_name": "张家长",
"customer_phone": "13800138000",
"customer_email": "parent@example.com",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 422
body = resp.json()
assert body["message"] == "请求数据未通过校验"
assert (
body["detail"]["fields"][0]["reason"]
== "Value error, amount_cents 与套餐价格不一致"
)
def test_mock_payment_complete_rejects_wrong_portal_token(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "张家长",
"customer_phone": "13800138000",
"customer_email": "parent@example.com",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
body = create_resp.json()
payment_id = body["checkout_url"].split("/pay/mock/")[1].split("?")[0]
resp = client.post(f"/pay/mock/{payment_id}/complete?token=wrong-token")
assert resp.status_code == 401 or resp.status_code == 403
def test_payment_return_redirects_to_portal_status(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "张家长",
"customer_phone": "13800138000",
"customer_email": "parent@example.com",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
body = create_resp.json()
token = body["portal_status_url"].split("/portal/")[1].split("/status")[0]
resp = client.get(f"/portal/payment-return?token={token}", follow_redirects=False)
assert resp.status_code == 303, resp.text
assert resp.headers["location"] == f"/portal/{token}/status"
def test_public_pages_include_privacy_and_deletion_links(client):
landing = client.get("/")
assert landing.status_code == 200, landing.text
assert 'href="/privacy"' in landing.text
assert 'href="/service-terms"' in landing.text
assert 'href="/deletion-policy"' in landing.text
pricing = client.get("/pricing")
assert pricing.status_code == 200, pricing.text
assert 'href="/privacy"' in pricing.text
assert 'href="/service-terms"' in pricing.text
assert 'href="/deletion-policy"' in pricing.text
def test_privacy_and_deletion_pages_are_served(client):
privacy = client.get("/privacy")
assert privacy.status_code == 200, privacy.text
assert "隐私政策" in privacy.text
terms = client.get("/service-terms")
assert terms.status_code == 200, terms.text
assert "服务说明与免责声明" in terms.text
deletion = client.get("/deletion-policy")
assert deletion.status_code == 200, deletion.text
assert "删除申请" in deletion.text
def test_public_create_order_returns_503_without_creating_orphan_order_when_provider_unavailable(
tmp_path, monkeypatch
):
admin_db = tmp_path / "admin.db"
orders_db = tmp_path / "orders.db"
share_db = tmp_path / "share.db"
share_reports = tmp_path / "share_reports"
share_reports.mkdir()
monkeypatch.setenv("GAOKAO_ENV", "dev")
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
monkeypatch.setenv("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-web-self-service")
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
monkeypatch.setenv("GAOKAO_PAYMENT_BASE_URL", "http://testserver")
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "missing-real-provider-env")
from admin.app import create_app
from admin.config import load_settings
settings = load_settings()
app = create_app(settings)
with TestClient(app) as client:
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "张家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert resp.status_code == 503, resp.text
assert "payment provider unavailable" in resp.text
with OrdersDAO.connect(settings.orders_db_path) as dao:
assert dao.count() == 0
def test_public_create_order_returns_503_without_creating_orphan_order_when_checkout_fails(
tmp_path, monkeypatch
):
admin_db = tmp_path / "admin.db"
orders_db = tmp_path / "orders.db"
share_db = tmp_path / "share.db"
share_reports = tmp_path / "share_reports"
share_reports.mkdir()
monkeypatch.setenv("GAOKAO_ENV", "dev")
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
monkeypatch.setenv("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-web-self-service")
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "mock")
monkeypatch.setenv("GAOKAO_PAYMENT_BASE_URL", "http://testserver")
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "test-payment-secret")
from admin.app import create_app
from admin.config import load_settings
from data.payments.service import PaymentError, PaymentService
original = PaymentService.create_checkout
def _boom(self, order_id: str, *, portal_token: str):
raise PaymentError("checkout transport failed")
monkeypatch.setattr(PaymentService, "create_checkout", _boom)
settings = load_settings()
app = create_app(settings)
with TestClient(app) as client:
resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_name": "张家长",
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
monkeypatch.setattr(PaymentService, "create_checkout", original)
assert resp.status_code == 503, resp.text
assert "payment checkout unavailable" in resp.text
with OrdersDAO.connect(settings.orders_db_path) as dao:
assert dao.count() == 0