"""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