Backend (103 tests): - Unit tests for listing_service, export_service, district_service - Regression tests for API response contracts and query parameter validation - Integration tests for API workflows, Redis listing cache, listing processor pipeline, and repository advanced queries - E2E tests for streaming with filters, batching, caching, and task management Frontend (116 tests): - Service tests for apiClient, streamingService, taskService, listingService, healthService - Hook tests for useTaskProgress (WebSocket + polling) - Component tests for PropertyCard, FilterPanel, Header, ListView, TaskProgressDrawer, TaskIndicator, StreamingProgressBar, HealthIndicator - E2E tests for filter-stream-display flow Infrastructure: - Add pytest-xdist and test markers (regression, integration, e2e) - Add conftest fixtures: fake_redis, rent_listing_factory, seeded_repository - Add vitest + testing-library + MSW for frontend testing
129 lines
6 KiB
Python
129 lines
6 KiB
Python
"""Unit tests for services/listing_service.py."""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from models.listing import QueryParameters, ListingType
|
|
from services import listing_service
|
|
|
|
|
|
class TestGetListings:
|
|
async def test_empty_db_returns_zero(self, listing_repository):
|
|
result = await listing_service.get_listings(listing_repository)
|
|
assert result.total_count == 0
|
|
assert result.listings == []
|
|
|
|
async def test_with_results_returns_correct_count(self, listing_repository, sample_rent_listings):
|
|
await listing_repository.upsert_listings(sample_rent_listings)
|
|
result = await listing_service.get_listings(listing_repository)
|
|
assert result.total_count == len(sample_rent_listings)
|
|
assert len(result.listings) == len(sample_rent_listings)
|
|
|
|
async def test_with_query_parameters_filters(self, listing_repository, sample_rent_listings):
|
|
await listing_repository.upsert_listings(sample_rent_listings)
|
|
params = QueryParameters(listing_type=ListingType.RENT, min_bedrooms=3)
|
|
result = await listing_service.get_listings(listing_repository, query_parameters=params)
|
|
for listing in result.listings:
|
|
assert listing.number_of_bedrooms >= 3
|
|
|
|
async def test_limit_works(self, listing_repository, sample_rent_listings):
|
|
await listing_repository.upsert_listings(sample_rent_listings)
|
|
result = await listing_service.get_listings(listing_repository, limit=1)
|
|
assert len(result.listings) <= 1
|
|
|
|
async def test_only_ids_works(self, listing_repository, sample_rent_listings):
|
|
await listing_repository.upsert_listings(sample_rent_listings)
|
|
target_ids = [sample_rent_listings[0].id]
|
|
result = await listing_service.get_listings(listing_repository, only_ids=target_ids)
|
|
assert all(l.id in target_ids for l in result.listings)
|
|
|
|
|
|
class TestRefreshListings:
|
|
async def test_async_mode_dispatches_celery_task(self, listing_repository):
|
|
params = QueryParameters(listing_type=ListingType.RENT)
|
|
with patch("tasks.listing_tasks.dump_listings_task") as mock_task:
|
|
mock_task.apply_async.return_value = MagicMock(id="fake-task-id")
|
|
result = await listing_service.refresh_listings(
|
|
listing_repository, params, async_mode=True, user_email="test@example.com"
|
|
)
|
|
mock_task.apply_async.assert_called_once()
|
|
assert result.task_id == "fake-task-id"
|
|
|
|
async def test_sync_mode_calls_fetcher(self, listing_repository):
|
|
params = QueryParameters(listing_type=ListingType.RENT)
|
|
with patch("services.listing_fetcher.dump_listings", new_callable=AsyncMock) as mock_dump:
|
|
mock_dump.return_value = []
|
|
result = await listing_service.refresh_listings(
|
|
listing_repository, params, async_mode=False
|
|
)
|
|
mock_dump.assert_called_once()
|
|
assert result.task_id is None
|
|
|
|
async def test_full_mode_calls_dump_listings_full(self, listing_repository):
|
|
params = QueryParameters(listing_type=ListingType.RENT)
|
|
with patch("services.listing_fetcher.dump_listings_full", new_callable=AsyncMock) as mock_full:
|
|
mock_full.return_value = []
|
|
result = await listing_service.refresh_listings(
|
|
listing_repository, params, full=True, async_mode=False
|
|
)
|
|
mock_full.assert_called_once()
|
|
|
|
async def test_sync_returns_new_listings_count(self, listing_repository):
|
|
params = QueryParameters(listing_type=ListingType.RENT)
|
|
fake_listings = [MagicMock(), MagicMock(), MagicMock()]
|
|
with patch("services.listing_fetcher.dump_listings", new_callable=AsyncMock) as mock_dump:
|
|
mock_dump.return_value = fake_listings
|
|
result = await listing_service.refresh_listings(
|
|
listing_repository, params, async_mode=False
|
|
)
|
|
assert result.new_listings_count == 3
|
|
|
|
async def test_async_result_has_message(self, listing_repository):
|
|
params = QueryParameters(listing_type=ListingType.RENT)
|
|
with patch("tasks.listing_tasks.dump_listings_task") as mock_task:
|
|
mock_task.apply_async.return_value = MagicMock(id="fake-task-id")
|
|
result = await listing_service.refresh_listings(
|
|
listing_repository, params, async_mode=True
|
|
)
|
|
assert result.message is not None
|
|
assert len(result.message) > 0
|
|
|
|
|
|
class TestDownloadImages:
|
|
async def test_calls_image_fetcher(self, listing_repository):
|
|
with patch("services.image_fetcher.dump_images", new_callable=AsyncMock) as mock_dump:
|
|
mock_dump.return_value = None
|
|
result = await listing_service.download_images(listing_repository)
|
|
mock_dump.assert_called_once()
|
|
|
|
|
|
class TestDetectFloorplans:
|
|
async def test_calls_floorplan_detector(self, listing_repository):
|
|
with patch("services.floorplan_detector.detect_floorplan", new_callable=AsyncMock) as mock_detect:
|
|
mock_detect.return_value = None
|
|
result = await listing_service.detect_floorplans(listing_repository)
|
|
mock_detect.assert_called_once()
|
|
|
|
|
|
class TestCalculateRoutes:
|
|
async def test_passes_correct_travel_mode(self, listing_repository):
|
|
with patch("services.route_calculator.calculate_route", new_callable=AsyncMock) as mock_calc:
|
|
mock_calc.return_value = None
|
|
result = await listing_service.calculate_routes(
|
|
listing_repository,
|
|
destination_address="London Bridge",
|
|
travel_mode="TRANSIT",
|
|
limit=10,
|
|
)
|
|
mock_calc.assert_called_once()
|
|
|
|
async def test_passes_limit(self, listing_repository):
|
|
with patch("services.route_calculator.calculate_route", new_callable=AsyncMock) as mock_calc:
|
|
mock_calc.return_value = None
|
|
result = await listing_service.calculate_routes(
|
|
listing_repository,
|
|
destination_address="Kings Cross",
|
|
travel_mode="TRANSIT",
|
|
limit=5,
|
|
)
|
|
assert result == 5
|