Files
next-tjwater-server/tests/api/test_place_search_endpoints.py
T
jiang be6317eb48 feat(server): integrate MCP queries and investigation messaging
Add project-bound OAuth MCP queries and controlled analysis with scoped evidence,
limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs.
Default nearby searches to 200 meters and five results.

Integrate investigation messaging/presentation, health/readiness and typed history;
remove duplicate web search and fix online simulation and compressed baseline writes.
Include project migrations 023-025; keep local credentials and demo seeding excluded.

Validation: 319 focused pytest checks; real PostGIS temporary-table regression;
read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
2026-10-19 18:46:58 +08:00

66 lines
3.4 KiB
Python

from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.v1.endpoints import place_search
from app.api.v1.rest_router import api_router
from app.auth.project_dependencies import ProjectContext, get_project_context
from app.domain.schemas.place_search import PlaceSearchItem, PlaceSearchResponse
from app.services.place_search import PlaceSearchError
@pytest.fixture
def app():
application = FastAPI()
application.include_router(api_router, prefix="/api/v1")
application.dependency_overrides[get_project_context] = lambda: ProjectContext(uuid4(), "demo", uuid4(), "viewer")
return application
def test_viewer_can_search_with_existing_project_read_permission(app, monkeypatch):
service = AsyncMock(return_value=PlaceSearchResponse(items=[PlaceSearchItem(
id="place-1", name="南桥路", address="上海市奉贤区南桥路", longitude=121.44, latitude=31.22,
)], total=1))
monkeypatch.setattr(place_search, "search_tianditu_places", service)
response = TestClient(app).post("/api/v1/place-search-requests", json={"keyword": "南桥路"})
assert response.status_code == 200
assert response.json()["items"][0]["name"] == "南桥路"
assert response.json()["region"] == "奉贤区"
assert set(response.json()) == {"items", "total", "truncated", "region"}
assert service.await_args.args[0].keyword == "南桥路"
@pytest.mark.parametrize("status", [502, 503, 504])
def test_endpoint_preserves_provider_failure_status(app, monkeypatch, status):
monkeypatch.setattr(place_search, "search_tianditu_places", AsyncMock(side_effect=PlaceSearchError(status, "查询暂不可用")))
response = TestClient(app).post("/api/v1/place-search-requests", json={"keyword": "南桥路"})
assert response.status_code == status and response.json()["detail"] == "查询暂不可用"
@pytest.mark.parametrize("extra", [{"region": "北京市"}, {"specify": "156310104"}, {"offset": 0}, {"limit": 10}])
def test_scope_and_result_limit_cannot_be_overridden(app, monkeypatch, extra):
service = AsyncMock()
monkeypatch.setattr(place_search, "search_tianditu_places", service)
response = TestClient(app).post("/api/v1/place-search-requests", json={"keyword": "南桥路", **extra})
assert response.status_code == 422 and not service.called
def test_search_denies_users_without_project_read_permission(app, monkeypatch):
app.dependency_overrides[get_project_context] = lambda: ProjectContext(uuid4(), "demo", uuid4(), "none")
service = AsyncMock()
monkeypatch.setattr(place_search, "search_tianditu_places", service)
response = TestClient(app).post("/api/v1/place-search-requests", json={"keyword": "南桥路"})
assert response.status_code == 403 and not service.called
def test_search_contract_has_typed_results_and_required_project_header(app):
contract = app.openapi()
operation = contract["paths"]["/api/v1/place-search-requests"]["post"]
assert any(param["name"] == "X-Project-Id" and param["required"] for param in operation["parameters"])
assert operation["responses"]["200"]["content"]["application/json"]["schema"]["$ref"].endswith("PlaceSearchResponse")
assert set(contract["components"]["schemas"]["PlaceSearchRequest"]["properties"]) == {"keyword"}
assert contract["components"]["schemas"]["PlaceSearchResponse"]["properties"]["items"]["maxItems"] == 10