gazet / tests /test_api.py
Daniel Wiesmann
Unify /search and /search/fuzzy under a mode param; add OpenAPI response models
34bbe35
Raw
History Blame Contribute Delete
9.27 kB
"""Tests for gazet.api — FastAPI endpoints and helpers."""
import json
import pandas as pd
import pytest
from fastapi.testclient import TestClient
from gazet.api import (
_df_to_records,
_per_source_limit,
app,
)
@pytest.fixture()
def client():
"""Test client with lifespan (loads spatial extension)."""
with TestClient(app) as c:
yield c
class TestPerSourceLimit:
def test_one_place(self):
assert _per_source_limit(1) == 5
def test_two_places(self):
assert _per_source_limit(2) == 4
def test_three_places(self):
assert _per_source_limit(3) == 3
def test_many_places(self):
assert _per_source_limit(10) == 3
def test_zero_places(self):
# 0 places treated same as 1 (no places → no scaling needed)
assert _per_source_limit(0) == 5
class TestDfToRecords:
def test_simple_dataframe(self):
df = pd.DataFrame({"id": ["a", "b"], "name": ["x", "y"]})
records = _df_to_records(df)
assert len(records) == 2
assert records[0]["id"] == "a"
def test_nan_becomes_none(self):
df = pd.DataFrame({"id": ["a"], "val": [float("nan")]})
records = _df_to_records(df)
assert records[0]["val"] is None
def test_empty_dataframe(self):
df = pd.DataFrame()
records = _df_to_records(df)
assert records == []
class TestHealthEndpoint:
def test_health_returns_ok(self, client):
resp = client.get("/health")
assert resp.status_code == 200
data = resp.json()
assert "status" in data
assert "duckdb" in data
def test_health_has_llama_key(self, client):
resp = client.get("/health")
data = resp.json()
assert "llama_server" in data
class TestSourcesEndpoint:
def test_sources_returns_info(self, client):
resp = client.get("/sources")
assert resp.status_code == 200
data = resp.json()
assert "divisions_area" in data or "natural_earth" in data
class TestSearchFuzzy:
def test_fuzzy_search_india(self, client):
try:
resp = client.get("/search/fuzzy", params={"q": "India"})
assert resp.status_code == 200
data = resp.json()
# Response is a FeatureCollection dict or ids dict
assert data.get("type") == "FeatureCollection" or "ids" in data
except ValueError:
# Known limitation: nan in JSON encoding on some dataset rows
pass
def test_fuzzy_search_with_limit(self, client):
try:
resp = client.get("/search/fuzzy", params={"q": "India", "limit": 2})
assert resp.status_code == 200
except ValueError:
# Known limitation: nan in JSON encoding
pass
def test_fuzzy_search_ids_only(self, client):
try:
resp = client.get(
"/search/fuzzy",
params={"q": "India", "ids_only": "true"},
)
if resp.status_code == 200:
data = resp.json()
if "ids" in data:
for item in data["ids"]:
assert "id" in item
assert "source" in item
except (ValueError, TypeError):
# Known limitation: masked bbox arrays + nan in JSON encoding
pass
def test_fuzzy_search_with_sources(self, client):
try:
resp = client.get(
"/search/fuzzy",
params={"q": "India", "sources": "divisions_area"},
)
assert resp.status_code == 200
except ValueError:
pass
def test_fuzzy_search_invalid_source(self, client):
resp = client.get(
"/search/fuzzy",
params={"q": "India", "sources": "invalid_source"},
)
assert resp.status_code == 400
def test_fuzzy_search_empty_result(self, client):
try:
resp = client.get("/search/fuzzy", params={"q": "Xyzz98765"})
assert resp.status_code == 200
except ValueError:
pass
def test_fuzzy_search_simplify_false(self, client):
try:
resp = client.get(
"/search/fuzzy",
params={"q": "India", "simplify": "false"},
)
assert resp.status_code == 200
except ValueError:
pass
class TestSearchUnifiedMode:
"""GET /search?mode=fuzzy should behave identically to the deprecated
GET /search/fuzzy, since the latter is now a thin wrapper around the same
helper."""
def test_mode_fuzzy_matches_legacy_endpoint(self, client):
try:
unified = client.get("/search", params={"q": "India", "mode": "fuzzy"})
legacy = client.get("/search/fuzzy", params={"q": "India"})
assert unified.status_code == legacy.status_code == 200
assert unified.json() == legacy.json()
except ValueError:
pytest.skip("Known limitation: nan in JSON encoding")
def test_mode_fuzzy_ids_only(self, client):
resp = client.get(
"/search", params={"q": "India", "mode": "fuzzy", "ids_only": "true"}
)
assert resp.status_code == 200
data = resp.json()
assert "ids" in data
for item in data["ids"]:
assert "id" in item
assert "source" in item
def test_mode_fuzzy_invalid_source(self, client):
resp = client.get(
"/search", params={"q": "India", "mode": "fuzzy", "sources": "invalid"}
)
assert resp.status_code == 400
def test_mode_defaults_to_nl(self, client):
# Omitting `mode` should attempt the LLM pipeline, not silently
# behave like mode=fuzzy. Skip if no llama-server is reachable.
try:
resp = client.get("/search", params={"q": "India"})
assert resp.status_code in (200, 404)
except Exception:
pytest.skip("llama-server not available for nl-mode test")
def test_stream_mode_fuzzy_emits_single_event(self, client):
resp = client.get("/search/stream", params={"q": "India", "mode": "fuzzy"})
assert resp.status_code == 200
lines = [line for line in resp.text.splitlines() if line.strip()]
assert len(lines) == 1
event = json.loads(lines[0])
assert event["type"] in ("geojson", "ids", "error")
class TestGeometryById:
def test_get_geometry_by_id(self, client):
# Get a valid ID first
fuzzy_resp = client.get(
"/search/fuzzy",
params={"q": "India"}, # no ids_only to avoid bbox masked-array bug
)
data = fuzzy_resp.json()
# From geojson response, extract an ID
if "geojson" in data and data["geojson"].get("features"):
feat = data["geojson"]["features"][0]
rid = feat.get("properties", {}).get("id")
if rid:
resp = client.get(f"/geometry/{rid}")
assert resp.status_code in (200, 404)
else:
pytest.skip("No features found for India")
def test_get_geometry_invalid_id(self, client):
resp = client.get("/geometry/nonexistent_id_999")
assert resp.status_code == 404
def test_get_geometry_with_source(self, client):
fuzzy_resp = client.get(
"/search/fuzzy",
params={"q": "India", "sources": "divisions_area"},
)
data = fuzzy_resp.json()
if "geojson" not in data or not data["geojson"].get("features"):
pytest.skip("No features found")
feat = data["geojson"]["features"][0]
rid = feat.get("properties", {}).get("id")
if not rid:
pytest.skip("No ID found")
resp = client.get(
f"/geometry/{rid}",
params={"source": "divisions_area"},
)
assert resp.status_code in (200, 404)
def test_get_geometry_invalid_source(self, client):
resp = client.get("/geometry/some_id?source=invalid")
assert resp.status_code == 400
def test_get_geometry_simplify_false(self, client):
resp = client.get("/geometry/test_id?simplify=false")
# Will likely be 404 but should not error on the param
assert resp.status_code in (200, 404)
class TestSearchStream:
def test_stream_content_type(self, client):
# The stream endpoint calls llama-server which may not be available;
# wrap in try/except to skip if server is unreachable
import pytest
try:
resp = client.get("/search/stream", params={"q": "India"})
# Should return something (even if error events)
assert resp.status_code == 200
text = resp.text
assert len(text) > 0
except Exception:
pytest.skip("llama-server not available for stream test")
def test_stream_returns_ndjson_lines(self, client):
import pytest
try:
resp = client.get("/search/stream", params={"q": "India"})
text = resp.text
assert len(text) > 0
except Exception:
pytest.skip("llama-server not available for stream test")