The listing processor was hardcoded to create RentListing objects and query only the rentlisting table. Buy listings fetched from Rightmove were stored in the wrong table with missing fields. This threads ListingType through ListingProcessor and all Step subclasses so the correct model (RentListing/BuyListing) is created, the correct table is queried, and buy-specific fields (service_charge, lease_left) are parsed from the API response and included in GeoJSON streaming output.
87 lines
3 KiB
Python
87 lines
3 KiB
Python
"""Unit tests for the listing processor."""
|
|
from datetime import datetime
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import pytest
|
|
from models.listing import FurnishType, ListingType
|
|
from listing_processor import (
|
|
_parse_furnish_type,
|
|
_parse_available_from,
|
|
ListingProcessor,
|
|
FetchListingDetailsStep,
|
|
MAX_OCR_WORKERS,
|
|
)
|
|
|
|
|
|
class TestParseFurnishType:
|
|
"""Tests for _parse_furnish_type helper."""
|
|
|
|
def test_none_returns_unknown(self):
|
|
assert _parse_furnish_type(None) == FurnishType.UNKNOWN
|
|
|
|
def test_ask_landlord_variant(self):
|
|
assert _parse_furnish_type("Ask landlord") == FurnishType.ASK_LANDLORD
|
|
|
|
def test_furnished_lowercased(self):
|
|
assert _parse_furnish_type("Furnished") == FurnishType.FURNISHED
|
|
|
|
def test_unfurnished(self):
|
|
assert _parse_furnish_type("Unfurnished") == FurnishType.UNFURNISHED
|
|
|
|
def test_part_furnished(self):
|
|
assert _parse_furnish_type("Part Furnished") == FurnishType.PART_FURNISHED
|
|
|
|
def test_unknown_string_returns_unknown(self):
|
|
assert _parse_furnish_type("unknown") == FurnishType.UNKNOWN
|
|
|
|
def test_garbage_string_returns_unknown(self):
|
|
assert _parse_furnish_type("xyzzy") == FurnishType.UNKNOWN
|
|
|
|
|
|
class TestParseAvailableFrom:
|
|
"""Tests for _parse_available_from helper."""
|
|
|
|
def test_none_returns_none(self):
|
|
assert _parse_available_from(None) is None
|
|
|
|
def test_now_returns_datetime(self):
|
|
result = _parse_available_from("Now")
|
|
assert isinstance(result, datetime)
|
|
|
|
def test_valid_date_string(self):
|
|
result = _parse_available_from("15/03/2024")
|
|
assert result is not None
|
|
assert result.day == 15
|
|
assert result.month == 3
|
|
|
|
def test_invalid_date_returns_none(self):
|
|
assert _parse_available_from("invalid") is None
|
|
|
|
|
|
class TestListingProcessor:
|
|
"""Tests for ListingProcessor."""
|
|
|
|
async def test_process_listing_marks_seen(self):
|
|
"""Test that process_listing calls mark_seen."""
|
|
mock_repo = AsyncMock()
|
|
mock_repo.get_listings = AsyncMock(return_value=[MagicMock()])
|
|
processor = ListingProcessor(mock_repo)
|
|
# Mock all steps to not need processing
|
|
for step in processor.process_steps:
|
|
step.needs_processing = AsyncMock(return_value=False)
|
|
await processor.process_listing(123)
|
|
mock_repo.mark_seen.assert_awaited_once_with(123, ListingType.RENT)
|
|
|
|
async def test_process_listing_returns_none_on_step_failure(self):
|
|
"""Test that a step failure returns None."""
|
|
mock_repo = AsyncMock()
|
|
processor = ListingProcessor(mock_repo)
|
|
for step in processor.process_steps:
|
|
step.needs_processing = AsyncMock(return_value=True)
|
|
step.process = AsyncMock(side_effect=Exception("fail"))
|
|
result = await processor.process_listing(123)
|
|
assert result is None
|
|
|
|
|
|
class TestOcrWorkersConfig:
|
|
def test_max_ocr_workers_positive(self):
|
|
assert MAX_OCR_WORKERS >= 1
|