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", } ] }, }