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, HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, WanfaCategory, WanfaRoute, ) from app.schemas import 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_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_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_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_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={ 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={ 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]