155 lines
4.4 KiB
Python
155 lines
4.4 KiB
Python
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": ["亲子"],
|
||
|
|
"pace": "moderate",
|
||
|
|
"interests": ["自然风光"],
|
||
|
|
"transport": "public_transport",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
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
|