Files
WonderQ-Project/WonderQ-Admin/tests/test_home.py
duanshuwen 6296392e80 refactor(home-api): 统一首页API响应格式并迁移配套代码
完成首页数据模型的统一重构,具体变更如下:
1.  重构公共首页API接口,移除`playRecommendations`字段,将玩法推荐数据统一放入`experiences`字段
2.  更新前后端类型定义、序列化逻辑与前端页面组件,适配新的API响应结构
3.  为微信登录相关接口添加`trust_env=True`配置,支持企业代理环境并新增`socksio`依赖
4.  新增图片画廊上传组件,重构SingleImageUploader组件支持自定义宽高比
5.  重构Toast与Select组件的实现与样式,统一后台UI设计系统
6.  移除个人中心页面不必要的返回事件与顶部标题组件,优化详情卡片布局
7.  新增微信接口单元测试,更新官方文档与测试用例适配变更
8.  删除过期的文档图片资源
2026-08-20 21:18:27 +08:00

560 lines
19 KiB
Python

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,
HomeExperience,
HomeTeamBuilding,
HomeWanfaRecommendation,
HomeWildArchive,
WanfaCategory,
WanfaRoute,
)
from app.schemas import HomeExperienceCreate, HomeTeamBuildingCreate, HomeWanfaRecommendationCreate, HomeWildArchiveCreate
class FakeScalarResult:
def __init__(self, items):
self.items = items
def all(self):
return self.items
class FakeDb:
def __init__(self, *, items=None, items_by_model=None, scalar_values=None):
self.items = list(items or [])
self.items_by_model = items_by_model or {}
self.scalar_values = list(scalar_values or [])
self.added = []
self.deleted = []
self.committed = False
def scalars(self, statement):
entity = statement.column_descriptions[0].get("entity")
return FakeScalarResult(self.items_by_model.get(entity, self.items))
def scalar(self, _statement):
return self.scalar_values.pop(0) if self.scalar_values else None
def get(self, model, item_id):
candidates = self.items_by_model.get(model, self.items)
return next((item for item in candidates if isinstance(item, model) and item.id == item_id), None)
def add(self, item):
self.added.append(item)
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 = "home-item-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_experience(**overrides):
return HomeExperience(
id=overrides.get("id", "waterfall-descent"),
badge=overrides.get("badge", "玩过推荐"),
category=overrides.get("category", "瀑降体验"),
title=overrides.get("title", "悬崖瀑降"),
englishTitle=overrides.get("englishTitle", "WATERFALL DESCENT"),
image=overrides.get("image", "https://example.test/waterfall.jpg"),
demandKeyword=overrides.get("demandKeyword", "悬崖瀑降"),
isActive=overrides.get("isActive", True),
sortOrder=overrides.get("sortOrder", 0),
createdAt=datetime(2026, 1, 1),
updatedAt=datetime(2026, 1, 2),
)
def make_team(**overrides):
return HomeTeamBuilding(
id=overrides.get("id", "wild-challenge"),
tag=overrides.get("tag", "户外挑战"),
title=overrides.get("title", "山野挑战,共创极境"),
description=overrides.get("description", "洞穴、瀑降与协作,适合 10-30 人。"),
image=overrides.get("image", "https://example.test/team.jpg"),
demandKeyword=overrides.get("demandKeyword", "户外团建"),
detailSubtitle=overrides.get("detailSubtitle", "越过山丘,向来处去"),
detailParagraphs=overrides.get("detailParagraphs", ["真正的贵州,从未被写进攻略里的山野故事。", "我们用一次次出发,把团队重新带回自然。"]),
isActive=overrides.get("isActive", True),
sortOrder=overrides.get("sortOrder", 0),
createdAt=datetime(2026, 1, 1),
updatedAt=datetime(2026, 1, 2),
)
def make_archive(**overrides):
return HomeWildArchive(
id=overrides.get("id", "hundred-meter-descent"),
title=overrides.get("title", "百米自降"),
image=overrides.get("image", "https://example.test/archive.jpg"),
images=overrides.get("images", ["https://example.test/archive.jpg", "https://example.test/archive-2.jpg"]),
demandKeyword=overrides.get("demandKeyword", "悬崖瀑降"),
isActive=overrides.get("isActive", True),
sortOrder=overrides.get("sortOrder", 0),
createdAt=datetime(2026, 1, 1),
updatedAt=datetime(2026, 1, 2),
)
def make_wanfa_category(**overrides):
route = WanfaRoute(
id="family-water",
categoryId=overrides.get("id", "family-route"),
title="亲子玩水",
subtitle="贵州·轻松节奏与自然课堂",
image="https://example.test/family-water.jpg",
routeCount=4,
demandKeyword="亲子玩水",
sortOrder=0,
)
return WanfaCategory(
id=overrides.get("id", "family-route"),
label=overrides.get("label", "亲子路线"),
sortOrder=0,
routes=[route],
)
def make_wanfa_recommendation(category=None, **overrides):
category = category or make_wanfa_category()
return HomeWanfaRecommendation(
id=overrides.get("id", "recommendation-1"),
categoryId=category.id,
category=category,
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_home_experience_schema_normalizes_text_and_url():
item = HomeExperienceCreate(
badge=" 玩过推荐 ",
category=" 瀑降体验 ",
title=" 悬崖瀑降 ",
englishTitle=" WATERFALL DESCENT ",
image=" https://example.test/waterfall.jpg ",
demandKeyword=" 悬崖瀑降 ",
)
assert item.title == "悬崖瀑降"
assert item.image == "https://example.test/waterfall.jpg"
assert item.sortOrder is None
def test_home_experience_schema_rejects_invalid_image_url():
with pytest.raises(ValidationError):
HomeExperienceCreate(
badge="玩过推荐",
category="瀑降体验",
title="悬崖瀑降",
englishTitle="WATERFALL DESCENT",
image="/waterfall.jpg",
demandKeyword="悬崖瀑降",
)
def test_home_wild_archive_schema_normalizes_detail_image_urls():
item = HomeWildArchiveCreate(
title=" 石龙洞 ",
image=" https://example.test/cover.jpg ",
images=[" https://example.test/one.jpg ", "https://example.test/two.jpg"],
demandKeyword=" 地心探险 ",
)
assert item.title == "石龙洞"
assert item.image == "https://example.test/cover.jpg"
assert item.images == ["https://example.test/one.jpg", "https://example.test/two.jpg"]
def test_home_team_building_schema_normalizes_detail_content():
item = HomeTeamBuildingCreate(
tag=" 户外挑战 ",
title=" 山野挑战,共创极境 ",
description=" 洞穴、瀑降与协作。 ",
image=" https://example.test/team.jpg ",
demandKeyword=" 户外团建 ",
detailSubtitle=" 越过山丘,向来处去 ",
detailParagraphs=[" 第一段 ", "", " 第二段 "],
)
assert item.detailSubtitle == "越过山丘,向来处去"
assert item.detailParagraphs == ["第一段", "第二段"]
def test_admin_home_experience_list_returns_full_record():
fake_db = FakeDb(items=[make_experience()])
app = admin_app(fake_db)
try:
response = TestClient(app).get("/api/admin/home/experiences")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
item = response.json()["data"]["items"][0]
assert item["id"] == "waterfall-descent"
assert item["isActive"] is True
assert item["createdAt"] == "2026-01-01T00:00:00"
def test_admin_home_team_create_appends_to_end():
fake_db = FakeDb(scalar_values=[2])
app = admin_app(fake_db)
try:
response = TestClient(app).post(
"/api/admin/home/team-buildings",
json={
"tag": " 团队协作 ",
"title": "峡谷溯溪,默契同行",
"description": "溯溪与分组协作,兼顾参与感与安全。",
"image": "https://example.test/team.jpg",
"demandKeyword": "峡谷团建",
"detailSubtitle": "并肩探索,共创默契",
"detailParagraphs": ["第一段正文", "第二段正文"],
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 201
assert fake_db.added[0].tag == "团队协作"
assert fake_db.added[0].detailSubtitle == "并肩探索,共创默契"
assert fake_db.added[0].detailParagraphs == ["第一段正文", "第二段正文"]
assert fake_db.added[0].sortOrder == 3
assert fake_db.committed
def test_admin_home_wild_patch_updates_record():
archive = make_archive()
fake_db = FakeDb(items=[archive])
app = admin_app(fake_db)
try:
response = TestClient(app).patch(
"/api/admin/home/wild-archives/hundred-meter-descent",
json={"title": "百米自降体验", "isActive": False},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["title"] == "百米自降体验"
assert response.json()["data"]["isActive"] is False
assert fake_db.committed
def test_admin_home_team_patch_updates_detail_content():
team = make_team()
fake_db = FakeDb(items=[team])
app = admin_app(fake_db)
try:
response = TestClient(app).patch(
"/api/admin/home/team-buildings/wild-challenge",
json={
"detailSubtitle": " 并肩探索,共创默契 ",
"detailParagraphs": [" 第一段正文 ", "", " 第二段正文 "],
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert team.detailSubtitle == "并肩探索,共创默契"
assert team.detailParagraphs == ["第一段正文", "第二段正文"]
assert fake_db.committed
def test_admin_home_wild_create_persists_detail_images():
fake_db = FakeDb(scalar_values=[0])
app = admin_app(fake_db)
try:
response = TestClient(app).post(
"/api/admin/home/wild-archives",
json={
"title": "石龙洞——客片案例",
"image": "https://example.test/cover.jpg",
"images": ["https://example.test/cover.jpg", "https://example.test/inside.jpg"],
"demandKeyword": "地心探险",
},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 201
assert fake_db.added[0].images == [
"https://example.test/cover.jpg",
"https://example.test/inside.jpg",
]
def test_admin_home_delete_normalizes_remaining_order():
first = make_experience(id="experience-1", sortOrder=0)
second = make_experience(id="experience-2", sortOrder=1)
fake_db = FakeDb(items=[first, second])
app = admin_app(fake_db)
try:
response = TestClient(app).delete("/api/admin/home/experiences/experience-1")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {
"code": 200,
"msg": "success",
"data": {"id": "experience-1"},
}
assert second.sortOrder == 0
assert fake_db.committed
def test_admin_home_reorder_rejects_incomplete_ids():
fake_db = FakeDb(items=[make_experience(id="experience-1"), make_experience(id="experience-2", sortOrder=1)])
app = admin_app(fake_db)
try:
response = TestClient(app).patch(
"/api/admin/home/experiences/reorder",
json={"itemIds": ["experience-1"]},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 400
assert response.json()["errorCode"] == "HOME_REORDER_INVALID"
assert not fake_db.committed
def test_home_wanfa_recommendation_schema_requires_category_id():
item = HomeWanfaRecommendationCreate(categoryId=" family-route ")
assert item.categoryId == "family-route"
def test_admin_home_wanfa_recommendation_create_links_existing_category():
category = make_wanfa_category()
fake_db = FakeDb(items=[category], scalar_values=[0])
app = admin_app(fake_db)
try:
response = TestClient(app).post(
"/api/admin/home/play-recommendations",
json={"categoryId": " family-route "},
)
finally:
app.dependency_overrides.clear()
assert response.status_code == 201
assert response.json()["data"]["categoryId"] == "family-route"
assert response.json()["data"]["categoryLabel"] == "亲子路线"
assert fake_db.added[0].categoryId == "family-route"
def test_public_home_places_active_wanfa_category_recommendations_in_experiences():
recommendation = make_wanfa_recommendation()
app = create_app()
fake_db = FakeDb(
items_by_model={
HomeExperience: [],
HomeTeamBuilding: [],
HomeWildArchive: [],
HomeWanfaRecommendation: [recommendation],
}
)
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/home")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
payload = response.json()["data"]
assert payload["experiences"] == [
{
"id": "recommendation-1",
"categoryId": "family-route",
"label": "亲子路线",
"routes": [
{
"id": "family-water",
"title": "亲子玩水",
"subtitle": "贵州·轻松节奏与自然课堂",
"image": "https://example.test/family-water.jpg",
"routeCount": 4,
"demandKeyword": "亲子玩水",
}
],
}
]
assert "playRecommendations" not in payload
def test_public_home_returns_rendering_fields_for_all_sections():
app = create_app()
fake_db = FakeDb(
items_by_model={
HomeExperience: [make_experience()],
HomeTeamBuilding: [make_team()],
HomeWildArchive: [make_archive()],
}
)
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/home")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {
"code": 200,
"msg": "success",
"data": {
"experiences": [],
"teamBuildings": [
{
"id": "wild-challenge",
"tag": "户外挑战",
"title": "山野挑战,共创极境",
"description": "洞穴、瀑降与协作,适合 10-30 人。",
"image": "https://example.test/team.jpg",
"demandKeyword": "户外团建",
}
],
"wildArchives": [
{
"id": "hundred-meter-descent",
"title": "百米自降",
"image": "https://example.test/archive.jpg",
"demandKeyword": "悬崖瀑降",
"photoCount": 2,
}
],
},
}
def test_public_wild_archive_list_returns_photo_count():
archive = make_archive()
app = create_app()
fake_db = FakeDb(items_by_model={HomeWildArchive: [archive]})
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/home/wild-archives")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["items"][0] == {
"id": "hundred-meter-descent",
"title": "百米自降",
"image": "https://example.test/archive.jpg",
"demandKeyword": "悬崖瀑降",
"photoCount": 2,
}
def test_public_wild_archive_detail_returns_all_images():
archive = make_archive()
app = create_app()
fake_db = FakeDb(scalar_values=[archive])
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/home/wild-archives/hundred-meter-descent")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["title"] == "百米自降"
assert response.json()["data"]["images"] == [
"https://example.test/archive.jpg",
"https://example.test/archive-2.jpg",
]
def test_public_team_building_detail_returns_long_form_content():
team = make_team()
app = create_app()
fake_db = FakeDb(scalar_values=[team])
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/home/team-buildings/wild-challenge")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {
"code": 200,
"msg": "success",
"data": {
"id": "wild-challenge",
"tag": "户外挑战",
"title": "山野挑战,共创极境",
"description": "洞穴、瀑降与协作,适合 10-30 人。",
"image": "https://example.test/team.jpg",
"demandKeyword": "户外团建",
"detailSubtitle": "越过山丘,向来处去",
"detailParagraphs": [
"真正的贵州,从未被写进攻略里的山野故事。",
"我们用一次次出发,把团队重新带回自然。",
],
},
}
def test_public_team_building_detail_returns_not_found_for_missing_item():
app = create_app()
app.dependency_overrides[get_db] = lambda: FakeDb(scalar_values=[None])
try:
response = TestClient(app).get("/api/public/home/team-buildings/missing")
finally:
app.dependency_overrides.clear()
assert response.status_code == 404
assert response.json()["msg"] == "团队共创不存在"
def test_public_team_building_detail_falls_back_to_legacy_description():
team = make_team(detailSubtitle="", detailParagraphs=[])
app = create_app()
fake_db = FakeDb(scalar_values=[team])
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).get("/api/public/home/team-buildings/wild-challenge")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["data"]["detailSubtitle"] == team.description
assert response.json()["data"]["detailParagraphs"] == [team.description]