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
243 lines
7.2 KiB
Python
243 lines
7.2 KiB
Python
"""Shared pytest fixtures for the test suite."""
|
|
from datetime import datetime
|
|
from typing import Any, AsyncGenerator, Callable, Generator
|
|
import pytest
|
|
import fakeredis
|
|
from sqlalchemy import Engine
|
|
from sqlmodel import SQLModel, Session, create_engine
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from models.listing import (
|
|
BuyListing,
|
|
FurnishType,
|
|
ListingSite,
|
|
RentListing,
|
|
Listing,
|
|
)
|
|
from repositories.listing_repository import ListingRepository
|
|
from api.auth import User
|
|
|
|
|
|
@pytest.fixture
|
|
def in_memory_engine() -> Generator[Engine, None, None]:
|
|
"""Create an in-memory SQLite engine for testing."""
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
echo=False,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
SQLModel.metadata.create_all(engine)
|
|
yield engine
|
|
SQLModel.metadata.drop_all(engine)
|
|
|
|
|
|
@pytest.fixture
|
|
def listing_repository(in_memory_engine: Engine) -> ListingRepository:
|
|
"""Create a ListingRepository with the in-memory engine."""
|
|
return ListingRepository(engine=in_memory_engine)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_rent_listing() -> RentListing:
|
|
"""Create a sample RentListing for testing."""
|
|
return RentListing(
|
|
id=12345678,
|
|
price=2500.0,
|
|
number_of_bedrooms=2,
|
|
square_meters=65.0,
|
|
agency="Test Agency",
|
|
council_tax_band="C",
|
|
longitude=-0.1276,
|
|
latitude=51.5074,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=datetime.now(),
|
|
photo_thumbnail="https://example.com/photo.jpg",
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
furnish_type=FurnishType.FURNISHED,
|
|
available_from=datetime.now(),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_buy_listing() -> BuyListing:
|
|
"""Create a sample BuyListing for testing."""
|
|
return BuyListing(
|
|
id=87654321,
|
|
price=450000.0,
|
|
number_of_bedrooms=3,
|
|
square_meters=95.0,
|
|
agency="Test Estate Agents",
|
|
council_tax_band="D",
|
|
longitude=-0.1180,
|
|
latitude=51.5100,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=datetime.now(),
|
|
photo_thumbnail="https://example.com/buy_photo.jpg",
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
service_charge=1500.0,
|
|
lease_left=90,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_rent_listings() -> list[RentListing]:
|
|
"""Create multiple sample RentListings for testing filters."""
|
|
now = datetime.now()
|
|
return [
|
|
RentListing(
|
|
id=1,
|
|
price=1500.0,
|
|
number_of_bedrooms=1,
|
|
square_meters=40.0,
|
|
agency="Agency A",
|
|
council_tax_band="B",
|
|
longitude=-0.1,
|
|
latitude=51.5,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=now,
|
|
photo_thumbnail=None,
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
furnish_type=FurnishType.FURNISHED,
|
|
available_from=now,
|
|
),
|
|
RentListing(
|
|
id=2,
|
|
price=2000.0,
|
|
number_of_bedrooms=2,
|
|
square_meters=55.0,
|
|
agency="Agency B",
|
|
council_tax_band="C",
|
|
longitude=-0.12,
|
|
latitude=51.51,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=now,
|
|
photo_thumbnail=None,
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
furnish_type=FurnishType.UNFURNISHED,
|
|
available_from=now,
|
|
),
|
|
RentListing(
|
|
id=3,
|
|
price=3000.0,
|
|
number_of_bedrooms=3,
|
|
square_meters=80.0,
|
|
agency="Agency C",
|
|
council_tax_band="D",
|
|
longitude=-0.14,
|
|
latitude=51.52,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=now,
|
|
photo_thumbnail=None,
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
furnish_type=FurnishType.FURNISHED,
|
|
available_from=now,
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_user() -> User:
|
|
"""Create a mock user for API tests."""
|
|
return User(
|
|
sub="test-user-id",
|
|
email="test@example.com",
|
|
name="Test User",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def async_client(
|
|
in_memory_engine: Engine, mock_user: User
|
|
) -> AsyncGenerator[AsyncClient, None]:
|
|
"""Create an AsyncClient for API testing with mock auth."""
|
|
from api.app import app
|
|
from api.auth import get_current_user
|
|
|
|
# Override dependencies
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
|
|
# Patch the engine used by the repository
|
|
original_engine = None
|
|
try:
|
|
from database import engine as db_engine
|
|
original_engine = db_engine
|
|
except Exception:
|
|
pass
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
yield client
|
|
|
|
# Clean up dependency overrides
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_redis() -> Generator[fakeredis.FakeRedis, None, None]:
|
|
"""Create a fakeredis client, flushed after each test."""
|
|
client = fakeredis.FakeRedis(decode_responses=True)
|
|
yield client
|
|
client.flushall()
|
|
|
|
|
|
@pytest.fixture
|
|
def rent_listing_factory() -> Callable[..., RentListing]:
|
|
"""Factory function that creates RentListing with overridable defaults."""
|
|
_counter = 0
|
|
|
|
def _create(**overrides: Any) -> RentListing:
|
|
nonlocal _counter
|
|
_counter += 1
|
|
defaults: dict[str, Any] = dict(
|
|
id=_counter,
|
|
price=2000.0,
|
|
number_of_bedrooms=2,
|
|
square_meters=55.0,
|
|
agency="Test Agency",
|
|
council_tax_band="C",
|
|
longitude=-0.1276,
|
|
latitude=51.5074,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=datetime.now(),
|
|
photo_thumbnail="https://example.com/photo.jpg",
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
furnish_type=FurnishType.FURNISHED,
|
|
available_from=datetime.now(),
|
|
)
|
|
defaults.update(overrides)
|
|
return RentListing(**defaults)
|
|
|
|
return _create
|
|
|
|
|
|
@pytest.fixture
|
|
async def seeded_repository(
|
|
in_memory_engine: Engine,
|
|
rent_listing_factory: Callable[..., RentListing],
|
|
) -> ListingRepository:
|
|
"""Repository with 10 pre-seeded listings (varied price/bedrooms/sqm)."""
|
|
repo = ListingRepository(engine=in_memory_engine)
|
|
listings = [
|
|
rent_listing_factory(id=100 + i, price=1000 + i * 300, number_of_bedrooms=(i % 4) + 1, square_meters=30 + i * 10)
|
|
for i in range(10)
|
|
]
|
|
await repo.upsert_listings(listings)
|
|
return repo
|