import os import pytest from fastapi.testclient import TestClient os.environ["LLM_MODE"] = "mock" from app.main import app # noqa: E402 client = TestClient(app) def preferences(): return { "destination": "深圳市光明区", "themes": ["亲子"], "duration": "half_day", "pace": "moderate", "interests": ["自然风光"], "adults": 2, "children": 1, "childAges": [6], "transport": "public_transport", "budgetLevel": "standard", "extraRequirements": "", } def candidates(count: int = 9): return [ { "id": f"poi_candidate_{index}", "name": f"候选点位 {index}", "categoryCode": "urban_park" if index % 2 else "cultural_venue", "tagCodes": ["family_friendly", "nature_view"], "summary": "仅用于接口契约测试。", "recommendationIndex": 1 + index % 5, } for index in range(1, count + 1) ] def request_payload(**overrides): payload = { "mode": "quick", "preferences": preferences(), "durationMinutes": 240, "selectedPoiIds": [], "candidates": candidates(), } payload.update(overrides) return payload def test_health_distinguishes_mock_and_configuration(): response = client.get("/api/v1/health") assert response.status_code == 200 data = response.json() assert data["status"] == "ok" assert data["mode"] == "mock" assert data["ready"] is True assert isinstance(data["configured"], bool) def test_quick_recommendations_only_use_candidate_whitelist(): payload = request_payload(durationMinutes=480) response = client.post("/api/v1/plans", json=payload) assert response.status_code == 200 data = response.json() allowed_ids = {candidate["id"] for candidate in payload["candidates"]} ids = [item["poiId"] for item in data["recommendations"]] assert 3 <= len(ids) <= 8 assert len(ids) == len(set(ids)) assert set(ids) <= allowed_ids assert all("longitude" not in item for item in data["recommendations"]) def test_custom_keeps_every_selected_poi_even_when_duration_is_short(): selected = [f"poi_candidate_{index}" for index in range(1, 7)] response = client.post( "/api/v1/plans", json=request_payload( mode="custom", durationMinutes=120, selectedPoiIds=selected, ), ) assert response.status_code == 200 data = response.json() ids = [item["poiId"] for item in data["recommendations"]] assert ids == selected assert data["mode"] == "custom" @pytest.mark.parametrize( "selected", [ ["poi_candidate_1"], [f"poi_candidate_{index}" for index in range(1, 10)], ], ) def test_custom_requires_two_to_eight_pois(selected): response = client.post( "/api/v1/plans", json=request_payload(mode="custom", selectedPoiIds=selected), ) assert response.status_code == 422 def test_unknown_selected_id_is_rejected(): response = client.post( "/api/v1/plans", json=request_payload( mode="custom", selectedPoiIds=["poi_candidate_1", "poi_not_in_whitelist"], ), ) assert response.status_code == 422 @pytest.mark.parametrize("duration", [90, 125, 630]) def test_duration_must_use_supported_range_and_step(duration): response = client.post( "/api/v1/plans", json=request_payload(durationMinutes=duration), ) assert response.status_code == 422 def test_quick_rejects_preselected_pois(): response = client.post( "/api/v1/plans", json=request_payload(selectedPoiIds=["poi_candidate_1"]), ) assert response.status_code == 422 def test_unknown_fields_are_rejected_in_preferences_and_candidates(): payload = request_payload() payload["preferences"]["unexpected"] = "not allowed" response = client.post("/api/v1/plans", json=payload) assert response.status_code == 422 payload = request_payload() payload["candidates"][0]["address"] = "not sent to model" response = client.post("/api/v1/plans", json=payload) assert response.status_code == 422 def test_origin_and_coordinates_are_rejected_at_the_contract_boundary(): payload = request_payload(origin={"longitude": 113.9, "latitude": 22.7}) response = client.post("/api/v1/plans", json=payload) assert response.status_code == 422 payload = request_payload() payload["candidates"][0]["longitude"] = 113.9 response = client.post("/api/v1/plans", json=payload) assert response.status_code == 422