Files
gaokao-volunteer-system/admin/tests/test_web_public_portal_info.py
Hermes Agent 0a6d9a7e39 test(web_public): 新增 3 个 TestClient 真实页面回归测试
- test_web_public_content_pages.py: 公开页 (/, /pricing, /privacy, /service-terms) 真实回归
- test_web_public_portal_info.py: portal info 表单 5 步向导完整测试
- test_web_public_review_flow.py: review 表单 + 303 redirect 真实测试

主链路不再依赖 RouteClient 自证, 改用真实 TestClient + 真实协议 + 真实重定向
2026-06-25 09:26:54 +08:00

574 lines
20 KiB
Python

"""用户端 Portal 资料页测试。"""
from __future__ import annotations
from data.orders.dao import OrdersDAO
def test_info_page_wizard_actions_outside_form_for_sticky_bottom(client, settings):
"""资料页移动端关键操作按钮必须放在 form 之外。"""
from data.orders.dao import OrdersDAO
from data.orders.public_flow import PublicOrderCreate, create_public_order
from data.customer_portal.token import issue_portal_token
with OrdersDAO.connect(settings.orders_db_path) as dao:
order = create_public_order(
dao,
PublicOrderCreate(
service_version="standard",
amount_cents=9900,
customer_name="Sticky 用户",
customer_phone="13800138000",
candidate_name="Sticky-User",
candidate_province="湖南",
),
)
token = issue_portal_token(order.id, settings.portal_token_secret)
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
body = resp.text
form_end = body.find("</form>")
wizard_start = body.find('<div class="wizard-actions">')
assert form_end > 0
assert wizard_start > 0
assert wizard_start > form_end
for label in ["保存草稿", "下一步", "上一步", "确认并提交资料"]:
assert label in body
assert "safe-area-inset-bottom" in body
def test_confirm_summary_does_not_use_innerhtml_for_user_fields(client, settings):
from data.orders.dao import OrdersDAO
from data.orders.public_flow import PublicOrderCreate, create_public_order
from data.customer_portal.token import issue_portal_token
with OrdersDAO.connect(settings.orders_db_path) as dao:
order = create_public_order(
dao,
PublicOrderCreate(
service_version="standard",
amount_cents=9900,
customer_name="Summary 用户",
customer_phone="13800138000",
candidate_name="Summary-User",
candidate_province="湖南",
),
)
token = issue_portal_token(order.id, settings.portal_token_secret)
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
body = resp.text
assert "confirm-summary').innerHTML" not in body
assert "replaceChildren" in body
assert "createElement('div')" in body
def test_portal_info_page_renders_candidate_province_field(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
token = create_resp.json()["portal_info_url"].split("/portal/")[1].split("/info")[0]
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
assert 'name="candidate_province"' in resp.text
assert "candidate_province: form.get('candidate_province')" in resp.text
def test_portal_info_page_step1_no_longer_requires_preference_target(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
token = create_resp.json()["portal_info_url"].split("/portal/")[1].split("/info")[0]
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
assert "至少一个偏好目标" not in resp.text
def test_submit_order_info_minimal_step1_marks_profile_complete_and_persists_province(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
from data.orders.intake_store import IntakeStore
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_info_url"].split("/portal/")[1].split("/info")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
result = submit_order_info(
token,
IntakePayload(
mode="submit",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
candidate_rank=12345,
consent_version="portal-v1",
consent_scope="step1",
privacy_accepted=True,
service_terms_accepted=True,
guardian_confirmed=True,
),
settings,
)
assert result.intake_status == "submitted"
assert result.profile_minimum_complete is True
with OrdersDAO.connect(settings.orders_db_path) as dao:
order = dao.get(body["order_id"])
assert order.candidate_province == "湖南"
assert order.candidate_score == 578
assert order.candidate_rank == 12345
assert order.candidate_subjects == ["物理", "化学", "生物"]
intake_store = IntakeStore.for_db(settings.orders_db_path)
try:
intake = intake_store.get(body["order_id"])
finally:
intake_store.close()
assert intake is not None
assert intake.payload["candidate_province"] == "湖南"
def test_submit_order_info_returns_profile_missing_fields_when_step1_incomplete(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_info_url"].split("/portal/")[1].split("/info")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
result = submit_order_info(
token,
IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
),
settings,
)
assert result.profile_minimum_complete is False
assert result.profile_missing_fields == ["candidate_rank"]
def test_portal_info_page_renders_step2_to_step4_fields(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
token = create_resp.json()["portal_info_url"].split("/portal/")[1].split("/info")[0]
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
body = resp.text
assert 'name="school_region_preferences"' in body
assert 'name="school_preference_types"' in body
assert 'name="target_schools"' in body
assert 'name="disliked_majors"' in body
assert 'name="priority_strategy"' in body
assert 'name="graduation_plan"' in body
assert 'name="tuition_preference"' in body
assert 'name="employment_region_preferences"' in body
def test_portal_info_page_renders_target_cities_field(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
token = create_resp.json()["portal_info_url"].split("/portal/")[1].split("/info")[0]
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
assert 'name="target_cities"' in resp.text
def test_submit_order_info_persists_step2_to_step4_preferences(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
from data.orders.intake_store import IntakeStore
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_info_url"].split("/portal/")[1].split("/info")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
result = submit_order_info(
token,
IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
candidate_rank=12345,
school_region_preferences=["长三角", "珠三角"],
school_preference_types=["985", "双一流"],
target_schools=["浙江大学", "中山大学"],
disliked_majors=["土木工程"],
priority_strategy="major_first",
graduation_plan="就业优先",
tuition_preference="10000-20000",
employment_region_preferences=["上海", "深圳"],
),
settings,
)
assert result.intake_status == "draft"
intake_store = IntakeStore.for_db(settings.orders_db_path)
try:
intake = intake_store.get(body["order_id"])
finally:
intake_store.close()
assert intake is not None
assert intake.payload["school_region_preferences"] == ["长三角", "珠三角"]
assert intake.payload["school_preference_types"] == ["985", "双一流"]
assert intake.payload["target_schools"] == ["浙江大学", "中山大学"]
assert intake.payload["disliked_majors"] == ["土木工程"]
assert intake.payload["priority_strategy"] == "major_first"
assert intake.payload["graduation_plan"] == "就业优先"
assert intake.payload["tuition_preference"] == "10000-20000"
assert intake.payload["employment_region_preferences"] == ["上海", "深圳"]
def test_portal_info_page_renders_p2_deep_preference_fields(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
token = create_resp.json()["portal_info_url"].split("/portal/")[1].split("/info")[0]
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
body = resp.text
assert 'name="family_background"' in body
assert 'name="industry_resources"' in body
assert 'name="extra_notes"' in body
def test_submit_order_info_persists_p2_deep_preference_fields(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
from data.orders.intake_store import IntakeStore
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_info_url"].split("/portal/")[1].split("/info")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
result = submit_order_info(
token,
IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
candidate_rank=12345,
family_background="父母支持省内发展",
industry_resources="有本地制造业实习资源",
extra_notes="希望后续优先考虑长沙与深圳双城路径",
),
settings,
)
assert result.intake_status == "draft"
intake_store = IntakeStore.for_db(settings.orders_db_path)
try:
intake = intake_store.get(body["order_id"])
finally:
intake_store.close()
assert intake is not None
assert intake.payload["family_background"] == "父母支持省内发展"
assert intake.payload["industry_resources"] == "有本地制造业实习资源"
assert intake.payload["extra_notes"] == "希望后续优先考虑长沙与深圳双城路径"
def test_submit_order_info_creates_profile_version_history_without_duplicate_snapshots(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
from data.orders.intake_store import IntakeStore
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_info_url"].split("/portal/")[1].split("/info")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
initial_payload = IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
candidate_rank=12345,
target_cities=["长沙", "深圳"],
)
submit_order_info(token, initial_payload, settings)
submit_order_info(token, initial_payload, settings)
submit_order_info(
token,
IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=579,
candidate_rank=12001,
target_cities=["长沙", "深圳"],
),
settings,
)
submit_order_info(
token,
IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=579,
candidate_rank=12001,
target_cities=["长沙", "深圳", "上海"],
family_background="家长更希望省内优先",
),
settings,
)
intake_store = IntakeStore.for_db(settings.orders_db_path)
try:
intake = intake_store.get(body["order_id"])
finally:
intake_store.close()
assert intake is not None
versions = intake.payload["profile_versions"]
assert intake.payload["latest_profile_version_id"] == versions[-1]["profile_version_id"]
assert len(versions) == 3
assert versions[0]["stage_label"] == "初始档案方案"
assert versions[1]["stage_label"] == "查分后校准方案"
assert versions[2]["stage_label"] == "正式填报前调整方案"
def test_portal_info_page_renders_interest_assessment_fields(client):
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"candidate_name": "张三",
"candidate_province": "湖南",
},
)
assert create_resp.status_code == 201, create_resp.text
token = create_resp.json()["portal_info_url"].split("/portal/")[1].split("/info")[0]
resp = client.get(f"/portal/{token}/info")
assert resp.status_code == 200, resp.text
body = resp.text
assert 'name="interest_assessment_type"' in body
assert 'name="interest_assessment_result"' in body
assert 'name="interest_assessment_notes"' in body
def test_submit_order_info_persists_interest_assessment_fields(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
from data.orders.intake_store import IntakeStore
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_info_url"].split("/portal/")[1].split("/info")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
result = submit_order_info(
token,
IntakePayload(
mode="draft",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
candidate_rank=12345,
interest_assessment_type="holland",
interest_assessment_result="R型+I型",
interest_assessment_notes="适合作为专业推荐补充因子,不作为唯一判断",
),
settings,
)
assert result.intake_status == "draft"
intake_store = IntakeStore.for_db(settings.orders_db_path)
try:
intake = intake_store.get(body["order_id"])
finally:
intake_store.close()
assert intake is not None
assert intake.payload["interest_assessment_type"] == "holland"
assert intake.payload["interest_assessment_result"] == "R型+I型"
assert intake.payload["interest_assessment_notes"] == "适合作为专业推荐补充因子,不作为唯一判断"
def test_order_status_page_shows_profile_minimum_complete_badge(client, settings):
from admin.routes.web_public import submit_order_info
from data.orders.intake_schema import IntakePayload
create_resp = client.post(
"/api/public/orders",
json={
"service_version": "standard",
"amount_cents": 9900,
"customer_phone": "13800138000",
"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]
token = body["portal_status_url"].split("/portal/")[1].split("/status")[0]
paid = client.post(f"/pay/mock/{payment_id}/complete", follow_redirects=False)
assert paid.status_code == 303, paid.text
submit_order_info(
token,
IntakePayload(
mode="submit",
candidate_province="湖南",
candidate_subjects=["物理", "化学", "生物"],
candidate_score=578,
candidate_rank=12345,
consent_version="portal-v1",
consent_scope="step1",
privacy_accepted=True,
service_terms_accepted=True,
guardian_confirmed=True,
),
settings,
)
resp = client.get(f"/portal/{token}/status")
assert resp.status_code == 200, resp.text
assert "Step 1 最小建档:已完成" in resp.text