wrongmove/crawler/tests/unit/test_repository.py

382 lines
14 KiB
Python
Raw Normal View History

"""Unit tests for ListingRepository."""
from datetime import datetime, timedelta
from unittest.mock import AsyncMock, MagicMock
import pytest
from sqlalchemy import Engine
from models.listing import (
FurnishType,
ListingType,
QueryParameters,
RentListing,
)
from repositories.listing_repository import ListingRepository
class TestListingRepository:
"""Tests for ListingRepository methods."""
async def test_get_listings_empty_db(
self, listing_repository: ListingRepository
) -> None:
"""Test that get_listings returns empty list for empty database."""
listings = await listing_repository.get_listings()
assert listings == []
async def test_get_listings_returns_inserted_listings(
self,
listing_repository: ListingRepository,
sample_rent_listing: RentListing,
) -> None:
"""Test that get_listings returns listings that were inserted."""
await listing_repository.upsert_listings([sample_rent_listing])
listings = await listing_repository.get_listings()
assert len(listings) == 1
assert listings[0].id == sample_rent_listing.id
async def test_upsert_listings_creates_new(
self,
listing_repository: ListingRepository,
sample_rent_listing: RentListing,
) -> None:
"""Test that upsert_listings creates new listings."""
result = await listing_repository.upsert_listings([sample_rent_listing])
assert len(result) == 1
assert result[0].id == sample_rent_listing.id
# Verify it's in the database
listings = await listing_repository.get_listings()
assert len(listings) == 1
async def test_upsert_listings_updates_existing(
self,
listing_repository: ListingRepository,
sample_rent_listing: RentListing,
) -> None:
"""Test that upsert_listings updates existing listings."""
# Insert initial listing
await listing_repository.upsert_listings([sample_rent_listing])
# Update the listing
sample_rent_listing.price = 3000.0
await listing_repository.upsert_listings([sample_rent_listing])
# Verify update
listings = await listing_repository.get_listings()
assert len(listings) == 1
assert listings[0].price == 3000.0
async def test_mark_seen_updates_timestamp(
self,
listing_repository: ListingRepository,
sample_rent_listing: RentListing,
) -> None:
"""Test that mark_seen updates the last_seen timestamp."""
# Set an old timestamp
old_time = datetime.now() - timedelta(days=7)
sample_rent_listing.last_seen = old_time
await listing_repository.upsert_listings([sample_rent_listing])
# Mark as seen
await listing_repository.mark_seen(sample_rent_listing.id)
# Verify timestamp was updated
listings = await listing_repository.get_listings()
assert len(listings) == 1
assert listings[0].last_seen > old_time
async def test_mark_seen_nonexistent_listing(
self, listing_repository: ListingRepository
) -> None:
"""Test that mark_seen handles nonexistent listings gracefully."""
# Should not raise an exception
await listing_repository.mark_seen(999999)
async def test_get_listings_with_only_ids(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test that get_listings filters by only_ids."""
await listing_repository.upsert_listings(sample_rent_listings)
# Request only specific IDs
listings = await listing_repository.get_listings(only_ids=[1, 3])
assert len(listings) == 2
listing_ids = [l.id for l in listings]
assert 1 in listing_ids
assert 3 in listing_ids
assert 2 not in listing_ids
async def test_get_listings_with_limit(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test that get_listings respects limit parameter."""
await listing_repository.upsert_listings(sample_rent_listings)
listings = await listing_repository.get_listings(limit=2)
assert len(listings) == 2
class TestListingRepositoryFilters:
"""Tests for ListingRepository query parameter filtering."""
async def test_filter_by_bedrooms(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test filtering by bedroom count."""
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
min_bedrooms=2,
max_bedrooms=2,
)
listings = await listing_repository.get_listings(query_parameters=query_params)
assert len(listings) == 1
assert listings[0].number_of_bedrooms == 2
async def test_filter_by_price_range(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test filtering by price range."""
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
min_price=1800,
max_price=2500,
)
listings = await listing_repository.get_listings(query_parameters=query_params)
assert len(listings) == 1
assert listings[0].price == 2000.0
async def test_filter_by_min_sqm(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test filtering by minimum square meters."""
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
min_sqm=60,
)
listings = await listing_repository.get_listings(query_parameters=query_params)
assert len(listings) == 1
assert listings[0].square_meters == 80.0
async def test_filter_by_furnish_type(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test filtering by furnish type."""
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
furnish_types=[FurnishType.UNFURNISHED],
)
listings = await listing_repository.get_listings(query_parameters=query_params)
assert len(listings) == 1
assert listings[0].furnish_type == FurnishType.UNFURNISHED
async def test_filter_by_last_seen_days(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test filtering by last_seen_days."""
# Make one listing old
sample_rent_listings[0].last_seen = datetime.now() - timedelta(days=30)
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
last_seen_days=7,
)
listings = await listing_repository.get_listings(query_parameters=query_params)
# Only 2 should be recent enough
assert len(listings) == 2
async def test_combined_filters(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test combining multiple filters."""
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
min_bedrooms=1,
max_bedrooms=2,
min_price=1000,
max_price=2500,
furnish_types=[FurnishType.FURNISHED, FurnishType.UNFURNISHED],
)
listings = await listing_repository.get_listings(query_parameters=query_params)
# Should match listings with 1-2 bedrooms in price range
assert len(listings) == 2
class TestListingRepositoryStreaming:
"""Tests for streaming and optimized query methods."""
async def test_count_listings_empty_db(
self, listing_repository: ListingRepository
) -> None:
"""Test count returns 0 for empty database."""
count = listing_repository.count_listings()
assert count == 0
async def test_count_listings_with_data(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test count returns correct number."""
await listing_repository.upsert_listings(sample_rent_listings)
count = listing_repository.count_listings()
assert count == 3
async def test_count_listings_with_filters(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test count respects query parameters."""
await listing_repository.upsert_listings(sample_rent_listings)
query_params = QueryParameters(
listing_type=ListingType.RENT,
min_bedrooms=2,
max_bedrooms=3,
)
count = listing_repository.count_listings(query_parameters=query_params)
assert count == 2
async def test_stream_listings_optimized_returns_dicts(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test optimized streaming returns dict rows."""
await listing_repository.upsert_listings(sample_rent_listings)
results = list(listing_repository.stream_listings_optimized())
assert len(results) == 3
# Each result should be a dict
for row in results:
assert isinstance(row, dict)
assert "id" in row
assert "price" in row
assert "number_of_bedrooms" in row
async def test_stream_listings_optimized_respects_limit(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test streaming limit parameter."""
await listing_repository.upsert_listings(sample_rent_listings)
results = list(listing_repository.stream_listings_optimized(limit=2))
assert len(results) == 2
async def test_get_listing_ids(
self,
listing_repository: ListingRepository,
sample_rent_listings: list[RentListing],
) -> None:
"""Test get_listing_ids returns set of IDs."""
await listing_repository.upsert_listings(sample_rent_listings)
ids = listing_repository.get_listing_ids()
assert isinstance(ids, set)
assert ids == {1, 2, 3}
async def test_get_listing_ids_empty_db(
self,
listing_repository: ListingRepository,
) -> None:
"""Test get_listing_ids returns empty set for empty database."""
ids = listing_repository.get_listing_ids()
assert isinstance(ids, set)
assert len(ids) == 0
class TestFurnishTypeParsing:
"""Tests for _parse_furnish_type helper."""
def test_parse_furnish_type_none_detailobject(self) -> None:
"""Test that None detailobject returns UNKNOWN."""
listing = MagicMock()
listing.detailobject = None
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.UNKNOWN
def test_parse_furnish_type_missing_property_key(self) -> None:
"""Test that missing 'property' key returns UNKNOWN."""
listing = MagicMock()
listing.detailobject = {}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.UNKNOWN
def test_parse_furnish_type_missing_let_furnish_type(self) -> None:
"""Test that missing 'letFurnishType' key returns UNKNOWN."""
listing = MagicMock()
listing.detailobject = {"property": {}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.UNKNOWN
def test_parse_furnish_type_null_value(self) -> None:
"""Test that null letFurnishType value returns UNKNOWN."""
listing = MagicMock()
listing.detailobject = {"property": {"letFurnishType": None}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.UNKNOWN
def test_parse_furnish_type_furnished(self) -> None:
"""Test that 'Furnished' is parsed correctly."""
listing = MagicMock()
listing.detailobject = {"property": {"letFurnishType": "Furnished"}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.FURNISHED
def test_parse_furnish_type_unfurnished(self) -> None:
"""Test that 'Unfurnished' is parsed correctly."""
listing = MagicMock()
listing.detailobject = {"property": {"letFurnishType": "Unfurnished"}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.UNFURNISHED
def test_parse_furnish_type_part_furnished(self) -> None:
"""Test that 'Part Furnished' is parsed correctly."""
listing = MagicMock()
listing.detailobject = {"property": {"letFurnishType": "Part Furnished"}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.PART_FURNISHED
def test_parse_furnish_type_landlord_variant(self) -> None:
"""Test that landlord variants map to ASK_LANDLORD."""
listing = MagicMock()
listing.detailobject = {"property": {"letFurnishType": "Ask Landlord Please"}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.ASK_LANDLORD
def test_parse_furnish_type_landlord_case_insensitive(self) -> None:
"""Test that landlord check is case-insensitive."""
listing = MagicMock()
listing.detailobject = {"property": {"letFurnishType": "LANDLORD decides"}}
result = ListingRepository._parse_furnish_type(listing)
assert result == FurnishType.ASK_LANDLORD