The fixture accepted in_memory_engine but never actually patched database.engine or api.app.engine, causing tests to hit the real SQLite path which fails in CI where data/ doesn't exist.
246 lines
7.3 KiB
Python
246 lines
7.3 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."""
|
|
import database
|
|
import api.app as api_app
|
|
from api.auth import get_current_user
|
|
|
|
app = api_app.app
|
|
|
|
# Override dependencies
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
|
|
# Patch the engine so the API uses the in-memory database
|
|
original_db_engine = database.engine
|
|
original_app_engine = api_app.engine
|
|
database.engine = in_memory_engine
|
|
api_app.engine = in_memory_engine
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
yield client
|
|
|
|
# Restore original engines and clean up
|
|
database.engine = original_db_engine
|
|
api_app.engine = original_app_engine
|
|
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
|