wrongmove/tests/integration/test_api.py

316 lines
12 KiB
Python
Raw Normal View History

"""Integration tests for API endpoints."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from httpx import AsyncClient
from api.auth import User
class TestStatusEndpoint:
"""Tests for the /api/status endpoint."""
async def test_status_endpoint_returns_ok(
self, async_client: AsyncClient
) -> None:
"""Test that status endpoint returns OK status."""
response = await async_client.get("/api/status")
assert response.status_code == 200
assert response.json() == {"status": "OK"}
class TestListingEndpoint:
"""Tests for the /api/listing endpoint."""
async def test_listing_endpoint_requires_auth(self) -> None:
"""Test that listing endpoint requires authentication."""
from api.app import app
from httpx import ASGITransport, AsyncClient
# Clear any dependency overrides to test auth requirement
app.dependency_overrides.clear()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/listing")
# Should return 401 or 403 without valid auth
assert response.status_code in (401, 403)
async def test_listing_endpoint_with_auth(
self, async_client: AsyncClient
) -> None:
"""Test that listing endpoint works with authentication."""
# Mock the repository to return empty list
with patch(
"api.app.ListingRepository.get_listings",
new_callable=AsyncMock,
return_value=[],
):
response = await async_client.get("/api/listing")
assert response.status_code == 200
data = response.json()
assert "listings" in data
class TestListingGeoJsonEndpoint:
"""Tests for the /api/listing_geojson endpoint."""
async def test_listing_geojson_requires_auth(self) -> None:
"""Test that listing_geojson endpoint requires authentication."""
from api.app import app
from httpx import ASGITransport, AsyncClient
# Clear any dependency overrides to test auth requirement
app.dependency_overrides.clear()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/listing_geojson",
params={"listing_type": "RENT"},
)
# Should return 401 or 403 without valid auth
assert response.status_code in (401, 403)
async def test_listing_geojson_with_filters(
self, async_client: AsyncClient
) -> None:
"""Test that listing_geojson accepts filter parameters."""
mock_result = MagicMock()
mock_result.data = {"type": "FeatureCollection", "features": []}
with patch(
"api.app.export_service.export_to_geojson",
new_callable=AsyncMock,
return_value=mock_result,
):
response = await async_client.get(
"/api/listing_geojson",
params={
"listing_type": "RENT",
"min_bedrooms": 2,
"max_bedrooms": 3,
"min_price": 1500,
"max_price": 3000,
},
)
assert response.status_code == 200
data = response.json()
assert data["type"] == "FeatureCollection"
class TestGetDistrictsEndpoint:
"""Tests for the /api/get_districts endpoint."""
async def test_get_districts_requires_auth(self) -> None:
"""Test that get_districts endpoint requires authentication."""
from api.app import app
from httpx import ASGITransport, AsyncClient
# Clear any dependency overrides to test auth requirement
app.dependency_overrides.clear()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/get_districts")
# Should return 401 or 403 without valid auth
assert response.status_code in (401, 403)
async def test_get_districts_returns_dict(
self, async_client: AsyncClient
) -> None:
"""Test that get_districts returns a dictionary of districts."""
response = await async_client.get("/api/get_districts")
assert response.status_code == 200
data = response.json()
assert isinstance(data, dict)
# Check some known districts exist
assert "London" in data
assert "Westminster" in data
assert "Camden" in data
async def test_get_districts_values_are_region_ids(
self, async_client: AsyncClient
) -> None:
"""Test that district values are REGION identifiers."""
response = await async_client.get("/api/get_districts")
data = response.json()
# All values should be REGION^... format
for district_name, region_id in data.items():
assert region_id.startswith("REGION^"), (
f"District {district_name} has invalid region ID: {region_id}"
)
class TestRefreshListingsEndpoint:
"""Tests for the /api/refresh_listings endpoint."""
async def test_refresh_listings_requires_auth(self) -> None:
"""Test that refresh_listings endpoint requires authentication."""
from api.app import app
from httpx import ASGITransport, AsyncClient
# Clear any dependency overrides to test auth requirement
app.dependency_overrides.clear()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/refresh_listings",
params={"listing_type": "RENT"},
)
# Should return 401 or 403 without valid auth
assert response.status_code in (401, 403)
class TestTaskStatusEndpoint:
"""Tests for the /api/task_status endpoint."""
async def test_task_status_requires_auth(self) -> None:
"""Test that task_status endpoint requires authentication."""
from api.app import app
from httpx import ASGITransport, AsyncClient
# Clear any dependency overrides to test auth requirement
app.dependency_overrides.clear()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/task_status",
params={"task_id": "test-task-id"},
)
# Should return 401 or 403 without valid auth
assert response.status_code in (401, 403)
class TestStreamListingGeoJsonEndpoint:
"""Tests for the /api/listing_geojson/stream endpoint."""
async def test_stream_returns_ndjson_with_metadata(
self, async_client: AsyncClient
) -> None:
"""Test that the stream endpoint returns valid NDJSON starting with a metadata message."""
fake_features = [
{"type": "Feature", "properties": {"id": 1}, "geometry": {"type": "Point", "coordinates": [0, 0]}},
{"type": "Feature", "properties": {"id": 2}, "geometry": {"type": "Point", "coordinates": [1, 1]}},
]
with patch("api.app.get_cached_count", return_value=2), \
patch("api.app.get_cached_features", return_value=iter([fake_features])):
response = await async_client.get(
"/api/listing_geojson/stream",
params={"listing_type": "RENT", "batch_size": 50},
)
assert response.status_code == 200
assert response.headers["content-type"] == "application/x-ndjson"
lines = [line for line in response.text.strip().split("\n") if line]
assert len(lines) >= 2 # at least metadata + complete
metadata = json.loads(lines[0])
assert metadata["type"] == "metadata"
assert "batch_size" in metadata
assert "total_expected" in metadata
complete = json.loads(lines[-1])
assert complete["type"] == "complete"
assert "total" in complete
async def test_stream_cache_hit_path(
self, async_client: AsyncClient
) -> None:
"""Test that cache-hit path returns cached: True in metadata."""
fake_features = [
{"type": "Feature", "properties": {"id": 1}, "geometry": {"type": "Point", "coordinates": [0, 0]}},
]
with patch("api.app.get_cached_count", return_value=1), \
patch("api.app.get_cached_features", return_value=iter([fake_features])):
response = await async_client.get(
"/api/listing_geojson/stream",
params={"listing_type": "RENT"},
)
assert response.status_code == 200
lines = [line for line in response.text.strip().split("\n") if line]
metadata = json.loads(lines[0])
assert metadata["cached"] is True
assert metadata["total_expected"] == 1
batch_msg = json.loads(lines[1])
assert batch_msg["type"] == "batch"
assert len(batch_msg["features"]) == 1
async def test_stream_cache_miss_path(
self, async_client: AsyncClient
) -> None:
"""Test that cache-miss path queries DB and returns cached: False."""
from datetime import datetime
fake_rows = [
{
"id": 100,
"price": 2000.0,
"number_of_bedrooms": 2,
"square_meters": 50.0,
"longitude": -0.1,
"latitude": 51.5,
"photo_thumbnail": None,
"last_seen": datetime(2024, 1, 1),
"agency": "Test Agency",
"price_history_json": "[]",
"available_from": None,
},
]
mock_repo = MagicMock()
mock_repo.count_listings.return_value = 1
mock_repo.stream_listings_optimized.return_value = iter(fake_rows)
with patch("api.app.get_cached_count", return_value=None), \
patch("api.app.ListingRepository", return_value=mock_repo), \
patch("api.app.cache_features_batch"):
response = await async_client.get(
"/api/listing_geojson/stream",
params={"listing_type": "RENT"},
)
assert response.status_code == 200
lines = [line for line in response.text.strip().split("\n") if line]
metadata = json.loads(lines[0])
assert metadata["cached"] is False
assert metadata["total_expected"] == 1
batch_msg = json.loads(lines[1])
assert batch_msg["type"] == "batch"
assert len(batch_msg["features"]) == 1
assert batch_msg["features"][0]["type"] == "Feature"
assert batch_msg["features"][0]["properties"]["total_price"] == 2000.0
complete = json.loads(lines[-1])
assert complete["type"] == "complete"
assert complete["total"] == 1
async def test_stream_with_limit(
self, async_client: AsyncClient
) -> None:
"""Test that the limit parameter caps the number of streamed features."""
fake_features = [
{"type": "Feature", "properties": {"id": i}, "geometry": {"type": "Point", "coordinates": [0, 0]}}
for i in range(5)
]
with patch("api.app.get_cached_count", return_value=5), \
patch("api.app.get_cached_features", return_value=iter([fake_features])):
response = await async_client.get(
"/api/listing_geojson/stream",
params={"listing_type": "RENT", "limit": 3},
)
assert response.status_code == 200
lines = [line for line in response.text.strip().split("\n") if line]
metadata = json.loads(lines[0])
assert metadata["total_expected"] == 3
complete = json.loads(lines[-1])
assert complete["type"] == "complete"
assert complete["total"] == 3