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