Files
WonderQ-Project/WonderQ-Admin/tests/test_details.py
duanshuwen feec6ab80f feat(wanfa): 新增路线详情参考价格配置及展示功能
- 新增数据库迁移脚本,为详情表添加四个价格相关字段
- 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则
- 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位
- 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明
- 更新小程序端详情页面,支持展示配置的参考价格信息
- 补充相关测试用例,更新API文档与类型定义
2026-08-27 22:02:51 +08:00

365 lines
11 KiB
Python
Raw Permalink 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.

from datetime import datetime
from fastapi.testclient import TestClient
from pydantic import ValidationError
import pytest
from app.auth import require_admin
from app.database import get_db
from app.main import create_app
from app.models import AdminUser, ConciergeAdvisor, DetailRecord
from app.schemas import DetailCreate
class FakeScalarResult:
def __init__(self, items):
self.items = items
def all(self):
return self.items
class FakeDb:
def __init__(self, *, scalar_results=None, scalar_values=None, get_results=None):
self.scalar_results = list(scalar_results or [])
self.scalar_values = list(scalar_values or [])
self.get_results = get_results or {}
self.added = []
self.deleted = []
self.committed = False
def scalars(self, _statement):
return FakeScalarResult(self.scalar_results.pop(0) if self.scalar_results else [])
def scalar(self, _statement):
return self.scalar_values.pop(0) if self.scalar_values else None
def get(self, model, item_id):
return self.get_results.get((model, item_id))
def add(self, item):
self.added.append(item)
def delete(self, item):
self.deleted.append(item)
def flush(self):
for item in self.added:
if not getattr(item, "id", None):
item.id = "detail-test-id"
def commit(self):
self.committed = True
def make_admin_user():
return AdminUser(
id="admin-test",
email="admin@example.test",
name="Admin",
role="admin",
passwordHash="not-used",
isActive=True,
)
def make_detail(**overrides):
values = {
"id": "detail-family-water",
"key": "family-water",
"eyebrow": "玩法推荐",
"duration": "5天4晚",
"title": "亲子玩水",
"subtitle": "贵州·轻松节奏与自然课堂",
"intro": "沿着贵州山地的自然纹理深入探索。",
"highlights": ["核心景观串联"],
"included": ["行程内用车与接送服务"],
"excluded": ["往返大交通"],
"notes": ["请准备轻便雨具。"],
"gallery": ["https://example.test/family-water.jpg"],
"priceStartingValue": 1.68,
"priceUnit": "person",
"pricePeopleRange": "2-4人",
"priceDescription": "价格以最终确认方案为准。",
"isActive": True,
"sortOrder": 0,
"conciergeAdvisorId": None,
"createdAt": datetime(2026, 1, 1),
"updatedAt": datetime(2026, 1, 2),
}
values.update(overrides)
return DetailRecord(**values)
def make_advisor(**overrides):
values = {
"id": "advisor-amanda",
"avatar": "https://example.test/amanda.jpg",
"name": "Amanda",
"role": "SENIOR TRAVEL ADVISOR",
"details": [{"icon": "calendar", "label": "服务经验8年"}],
"qrImage": "https://example.test/amanda-qr.png",
"isActive": True,
"sortOrder": 0,
"createdAt": datetime(2026, 1, 1),
"updatedAt": datetime(2026, 1, 2),
}
values.update(overrides)
return ConciergeAdvisor(**values)
def authenticated_app(fake_db):
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
app.dependency_overrides[require_admin] = make_admin_user
return app
def test_detail_schema_trims_text_and_array_items():
detail = DetailCreate(
key=" family-water ",
eyebrow=" 玩法推荐 ",
duration=" 5天4晚 ",
title=" 亲子玩水 ",
subtitle=" 贵州·轻松节奏与自然课堂 ",
intro=" 沿着山地深入探索。 ",
highlights=[" 核心景观串联 ", "", " 小团出行 "],
included=[" 行程内用车 "],
excluded=[" 往返大交通 "],
notes=[" 请准备雨具。 "],
gallery=[" https://example.test/family-water.jpg "],
priceStartingValue=1.68,
priceUnit="person",
pricePeopleRange=" 2-4人 ",
priceDescription=" 价格以最终确认方案为准。 ",
)
assert detail.key == "family-water"
assert detail.highlights == ["核心景观串联", "小团出行"]
assert detail.gallery == ["https://example.test/family-water.jpg"]
assert detail.priceStartingValue == 1.68
assert detail.priceUnit == "person"
assert detail.pricePeopleRange == "2-4人"
assert detail.priceDescription == "价格以最终确认方案为准。"
def test_detail_schema_rejects_unknown_price_unit():
with pytest.raises(ValidationError):
DetailCreate(
key="family-water",
eyebrow="玩法推荐",
duration="5天4晚",
title="亲子玩水",
subtitle="贵州路线",
intro="介绍",
priceStartingValue=1.68,
priceUnit="month",
)
def test_detail_schema_rejects_invalid_gallery_url():
with pytest.raises(ValidationError):
DetailCreate(
key="family-water",
eyebrow="玩法推荐",
duration="5天4晚",
title="亲子玩水",
subtitle="贵州路线",
intro="介绍",
highlights=["亮点"],
included=["包含"],
excluded=["不含"],
notes=["注意"],
gallery=["not-a-url"],
)
def test_detail_schema_normalizes_empty_concierge_advisor_id_to_null():
detail = DetailCreate(
key="family-water",
eyebrow="玩法推荐",
duration="5天4晚",
title="亲子玩水",
subtitle="贵州路线",
intro="介绍",
conciergeAdvisorId=" ",
)
assert detail.conciergeAdvisorId is None
def test_public_detail_returns_frontend_contract_without_auth():
fake_db = FakeDb(scalar_values=[make_detail()])
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/details/family-water")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {
"code": 200,
"msg": "success",
"data": {
"key": "family-water",
"eyebrow": "玩法推荐",
"duration": "5天4晚",
"title": "亲子玩水",
"subtitle": "贵州·轻松节奏与自然课堂",
"intro": "沿着贵州山地的自然纹理深入探索。",
"highlights": ["核心景观串联"],
"included": ["行程内用车与接送服务"],
"excluded": ["往返大交通"],
"notes": ["请准备轻便雨具。"],
"gallery": ["https://example.test/family-water.jpg"],
"priceStartingValue": 1.68,
"priceUnit": "person",
"pricePeopleRange": "2-4人",
"priceDescription": "价格以最终确认方案为准。",
"conciergeAdvisor": None,
},
}
def test_public_detail_includes_active_concierge_advisor():
advisor = make_advisor()
fake_db = FakeDb(
scalar_values=[make_detail(conciergeAdvisorId=advisor.id)],
get_results={(ConciergeAdvisor, advisor.id): advisor},
)
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/details/family-water")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["conciergeAdvisor"] == {
"avatar": advisor.avatar,
"name": advisor.name,
"role": advisor.role,
"details": advisor.details,
"qrImage": advisor.qrImage,
}
def test_public_detail_hides_inactive_concierge_advisor():
advisor = make_advisor(isActive=False)
fake_db = FakeDb(
scalar_values=[make_detail(conciergeAdvisorId=advisor.id)],
get_results={(ConciergeAdvisor, advisor.id): advisor},
)
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/details/family-water")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["conciergeAdvisor"] is None
def test_public_detail_returns_not_found_for_missing_key():
fake_db = FakeDb(scalar_values=[None])
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/details/missing")
finally:
app.dependency_overrides.clear()
assert response.status_code == 404
def test_public_detail_returns_not_found_for_inactive_record():
# The query filters inactive rows, so an inactive match is represented as no scalar result.
fake_db = FakeDb(scalar_values=[None])
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/details/family-water")
finally:
app.dependency_overrides.clear()
assert response.status_code == 404
def test_admin_detail_list_includes_inactive_records():
fake_db = FakeDb(scalar_results=[[make_detail(isActive=False)]])
app = authenticated_app(fake_db)
try:
response = TestClient(app).get("/api/admin/details")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["details"][0]["isActive"] is False
def test_admin_detail_create_normalizes_key_and_appends():
fake_db = FakeDb(scalar_values=[None, 2])
app = authenticated_app(fake_db)
try:
response = TestClient(app).post(
"/api/admin/details",
json={
"key": " family-water ",
"eyebrow": "玩法推荐",
"duration": "5天4晚",
"title": "亲子玩水",
"subtitle": "贵州路线",
"intro": "沿着山地深入探索。",
"highlights": ["核心景观串联"],
"included": [],
"excluded": [],
"notes": [],
"gallery": ["https://example.test/family-water.jpg"],
"priceStartingValue": 1.68,
"priceUnit": "person",
"pricePeopleRange": "2-4人",
"priceDescription": "价格以最终确认方案为准。",
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 201
assert response.json()["data"]["key"] == "family-water"
assert fake_db.added[0].priceStartingValue == 1.68
assert fake_db.added[0].priceUnit == "person"
assert fake_db.added[0].sortOrder == 3
def test_admin_detail_create_rejects_unknown_concierge_advisor():
fake_db = FakeDb(scalar_values=[None, 2])
app = authenticated_app(fake_db)
try:
response = TestClient(app).post(
"/api/admin/details",
json={
"key": "family-water",
"eyebrow": "玩法推荐",
"duration": "5天4晚",
"title": "亲子玩水",
"subtitle": "贵州路线",
"intro": "沿着山地深入探索。",
"conciergeAdvisorId": "missing-advisor",
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 422
assert response.json()["errorCode"] == "DETAIL_CONCIERGE_ADVISOR_INVALID"