Files
WonderQ-Project/WonderQ-Admin/tests/test_concierge.py
duanshuwen e082bd2d98 feat(api): 实现三端统一的JSON API响应契约
- 新增`api_response.py`统一响应封装工具类,提供标准成功/错误响应构造方法
- 重构WonderQ-Admin全局异常处理器,将所有异常转换为标准响应格式
- 修改所有公共和管理端接口的返回逻辑,统一使用`code`(与HTTP状态码一致)、`msg`和`data`的三层结构
- 新增`api-response-contract.md`文档,定义完整的三端统一JSON响应规范
- 更新所有领域API文档,明确业务数据需位于`data`字段内,补充响应格式说明
- 为WonderQ-MiniAPP和WonderQ-Admin-UI新增响应解析逻辑和类型定义,自动完成协议校验和错误处理
- 更新所有测试用例,适配新的响应结构确保接口符合契约要求
- 新增`module-config-api.md`模块配置API文档,补充站点模块配置的接口约定
- 更新项目README文档,调整文档分类顺序将响应契约置于首位
2026-08-19 22:02:20 +08:00

267 lines
8.2 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.

from datetime import datetime
import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from app.auth import require_admin
from app.database import get_db
from app.main import create_app
from app.models import AdminUser, ConciergeAdvisor
from app.schemas import ConciergeAdvisorCreate
class FakeScalarResult:
def __init__(self, items):
self.items = items
def all(self):
return self.items
class FakeDb:
def __init__(self, *, items=None, scalar_values=None):
self.items = list(items or [])
self.scalar_values = list(scalar_values or [])
self.scalar_statements = []
self.added = []
self.deleted = []
self.committed = False
def scalars(self, statement):
self.scalar_statements.append(statement)
return FakeScalarResult(self.items)
def scalar(self, _statement):
return self.scalar_values.pop(0) if self.scalar_values else None
def get(self, model, item_id):
return next((item for item in self.items if isinstance(item, model) and item.id == item_id), None)
def add(self, item):
self.added.append(item)
if isinstance(item, ConciergeAdvisor):
self.items.append(item)
def delete(self, item):
self.deleted.append(item)
if item in self.items:
self.items.remove(item)
def flush(self):
for item in self.added:
if not getattr(item, "id", None):
item.id = "concierge-advisor-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_advisor(**overrides):
return ConciergeAdvisor(
id=overrides.get("id", "advisor-amanda"),
avatar=overrides.get("avatar", "https://example.test/amanda.jpg"),
name=overrides.get("name", "Amanda"),
role=overrides.get("role", "SENIOR TRAVEL ADVISOR"),
details=overrides.get(
"details",
[
{"icon": "calendar", "label": "服务经验8年"},
{"icon": "navigate", "label": "擅长领域:瀑降 / 探洞"},
],
),
qrImage=overrides.get("qrImage", "https://example.test/amanda-qr.png"),
isActive=overrides.get("isActive", True),
sortOrder=overrides.get("sortOrder", 0),
createdAt=datetime(2026, 1, 1),
updatedAt=datetime(2026, 1, 2),
)
def admin_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_concierge_schema_normalizes_nested_details():
advisor = ConciergeAdvisorCreate(
avatar=" https://example.test/avatar.jpg ",
name=" Amanda ",
role=" SENIOR TRAVEL ADVISOR ",
details=[{"icon": " calendar ", "label": " 服务经验8年 "}],
qrImage=" https://example.test/qr.png ",
)
assert advisor.avatar == "https://example.test/avatar.jpg"
assert advisor.name == "Amanda"
assert advisor.details == [{"icon": "calendar", "label": "服务经验8年"}]
assert advisor.sortOrder is None
def test_concierge_schema_rejects_empty_detail_label():
with pytest.raises(ValidationError):
ConciergeAdvisorCreate(
avatar="https://example.test/avatar.jpg",
name="Amanda",
role="SENIOR TRAVEL ADVISOR",
details=[{"icon": "calendar", "label": " "}],
qrImage="https://example.test/qr.png",
)
def test_concierge_schema_rejects_invalid_image_url():
with pytest.raises(ValidationError):
ConciergeAdvisorCreate(
avatar="/avatar.jpg",
name="Amanda",
role="SENIOR TRAVEL ADVISOR",
details=[],
qrImage="https://example.test/qr.png",
)
def test_admin_concierge_list_returns_full_advisor_record():
fake_db = FakeDb(items=[make_advisor()])
app = admin_app(fake_db)
try:
response = TestClient(app).get("/api/admin/concierge/advisors")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
advisor = response.json()["data"]["advisors"][0]
assert advisor["id"] == "advisor-amanda"
assert advisor["isActive"] is True
assert advisor["sortOrder"] == 0
assert advisor["details"][0] == {"icon": "calendar", "label": "服务经验8年"}
assert advisor["createdAt"] == "2026-01-01T00:00:00"
def test_admin_concierge_create_appends_to_the_end():
fake_db = FakeDb(scalar_values=[2])
app = admin_app(fake_db)
try:
response = TestClient(app).post(
"/api/admin/concierge/advisors",
json={
"avatar": " https://example.test/mia.jpg ",
"name": " Mia ",
"role": "LIFESTYLE TRAVEL ADVISOR",
"details": [],
"qrImage": "https://example.test/mia-qr.png",
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 201
assert response.json()["data"]["id"] == "concierge-advisor-test-id"
assert fake_db.added[0].name == "Mia"
assert fake_db.added[0].sortOrder == 3
assert fake_db.committed
def test_admin_concierge_patch_updates_details_and_active_state():
advisor = make_advisor()
fake_db = FakeDb(items=[advisor])
app = admin_app(fake_db)
try:
response = TestClient(app).patch(
"/api/admin/concierge/advisors/advisor-amanda",
json={"details": [{"icon": "navigate", "label": "擅长领域:亲子与自然探索"}], "isActive": False},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["details"] == [{"icon": "navigate", "label": "擅长领域:亲子与自然探索"}]
assert response.json()["data"]["isActive"] is False
assert advisor.isActive is False
assert fake_db.committed
def test_admin_concierge_delete_normalizes_remaining_order():
first = make_advisor(id="advisor-1", sortOrder=0)
second = make_advisor(id="advisor-2", name="Mia", sortOrder=1)
fake_db = FakeDb(items=[first, second])
app = admin_app(fake_db)
try:
response = TestClient(app).delete("/api/admin/concierge/advisors/advisor-1")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {
"code": 200,
"msg": "success",
"data": {"id": "advisor-1"},
}
assert second.sortOrder == 0
assert fake_db.deleted == [first]
assert fake_db.committed
def test_admin_concierge_reorder_rejects_incomplete_ids():
fake_db = FakeDb(items=[make_advisor(id="advisor-1"), make_advisor(id="advisor-2", name="Mia", sortOrder=1)])
app = admin_app(fake_db)
try:
response = TestClient(app).patch(
"/api/admin/concierge/advisors/reorder",
json={"itemIds": ["advisor-1"]},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 400
assert response.json()["errorCode"] == "CONCIERGE_REORDER_INVALID"
assert not fake_db.committed
def test_public_concierge_returns_only_frontend_fields_for_active_advisors():
fake_db = FakeDb(items=[make_advisor()])
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/concierge/advisors")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {
"code": 200,
"msg": "success",
"data": {
"advisors": [
{
"avatar": "https://example.test/amanda.jpg",
"name": "Amanda",
"role": "SENIOR TRAVEL ADVISOR",
"details": [
{"icon": "calendar", "label": "服务经验8年"},
{"icon": "navigate", "label": "擅长领域:瀑降 / 探洞"},
],
"qrImage": "https://example.test/amanda-qr.png",
}
]
},
}