The crawler subdirectory was the only active project. Moving it to the repo root simplifies paths and removes the unnecessary nesting. The vqa/ and immoweb/ directories were legacy/unused and have been removed. Updated .drone.yml, .gitignore, .claude/ docs, and skills to reflect the new flat structure.
110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
"""Unit tests for services/floorplan_detector.py."""
|
|
import asyncio
|
|
from datetime import datetime
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
|
|
from models.listing import RentListing, ListingSite, FurnishType
|
|
from services.floorplan_detector import _calculate_sqm_ocr, detect_floorplan
|
|
|
|
|
|
def _make_listing(**kwargs) -> RentListing: # type: ignore[no-untyped-def]
|
|
defaults = dict(
|
|
id=1,
|
|
price=2000.0,
|
|
number_of_bedrooms=2,
|
|
square_meters=None,
|
|
agency="Test",
|
|
council_tax_band="C",
|
|
longitude=0.0,
|
|
latitude=0.0,
|
|
price_history_json="[]",
|
|
listing_site=ListingSite.RIGHTMOVE,
|
|
last_seen=datetime.now(),
|
|
photo_thumbnail=None,
|
|
floorplan_image_paths=[],
|
|
additional_info={"property": {"visible": True}},
|
|
routing_info_json=None,
|
|
furnish_type=FurnishType.FURNISHED,
|
|
available_from=None,
|
|
)
|
|
defaults.update(kwargs)
|
|
return RentListing(**defaults)
|
|
|
|
|
|
class TestCalculateSqmOcr:
|
|
|
|
async def test_skips_listing_with_existing_square_meters(self) -> None:
|
|
listing = _make_listing(square_meters=50.0)
|
|
semaphore = asyncio.Semaphore(1)
|
|
result = await _calculate_sqm_ocr(listing, semaphore)
|
|
assert result is None
|
|
|
|
async def test_empty_floorplan_paths_returns_listing_with_zero(self) -> None:
|
|
listing = _make_listing(floorplan_image_paths=[])
|
|
semaphore = asyncio.Semaphore(1)
|
|
result = await _calculate_sqm_ocr(listing, semaphore)
|
|
assert result is not None
|
|
assert result.square_meters == 0
|
|
|
|
@patch("services.floorplan_detector.floorplan")
|
|
async def test_with_mocked_ocr_returning_value(self, mock_floorplan: MagicMock) -> None:
|
|
mock_floorplan.calculate_ocr.return_value = (85.0, "Total: 85 sq m")
|
|
listing = _make_listing(floorplan_image_paths=["/fake/path.png"])
|
|
semaphore = asyncio.Semaphore(1)
|
|
result = await _calculate_sqm_ocr(listing, semaphore)
|
|
assert result is not None
|
|
assert result.square_meters == 85.0
|
|
|
|
@patch("services.floorplan_detector.floorplan")
|
|
async def test_with_mocked_ocr_returning_none(self, mock_floorplan: MagicMock) -> None:
|
|
mock_floorplan.calculate_ocr.return_value = (None, "no data")
|
|
listing = _make_listing(floorplan_image_paths=["/fake/path.png"])
|
|
semaphore = asyncio.Semaphore(1)
|
|
result = await _calculate_sqm_ocr(listing, semaphore)
|
|
assert result is not None
|
|
assert result.square_meters == 0
|
|
|
|
@patch("services.floorplan_detector.floorplan")
|
|
async def test_picks_max_from_multiple_floorplans(self, mock_floorplan: MagicMock) -> None:
|
|
mock_floorplan.calculate_ocr.side_effect = [
|
|
(50.0, "50 sq m"),
|
|
(90.0, "90 sq m"),
|
|
]
|
|
listing = _make_listing(floorplan_image_paths=["/fake/a.png", "/fake/b.png"])
|
|
semaphore = asyncio.Semaphore(2)
|
|
result = await _calculate_sqm_ocr(listing, semaphore)
|
|
assert result is not None
|
|
assert result.square_meters == 90.0
|
|
|
|
|
|
class TestDetectFloorplan:
|
|
|
|
@patch("services.floorplan_detector.floorplan")
|
|
async def test_detect_floorplan_with_mocked_repository(self, mock_floorplan: MagicMock) -> None:
|
|
mock_floorplan.calculate_ocr.return_value = (75.0, "75 sq m")
|
|
|
|
listing = _make_listing(
|
|
floorplan_image_paths=["/fake/path.png"],
|
|
)
|
|
repository = MagicMock()
|
|
repository.get_listings = AsyncMock(return_value=[listing])
|
|
repository.upsert_listings = AsyncMock(return_value=[])
|
|
|
|
await detect_floorplan(repository)
|
|
|
|
repository.upsert_listings.assert_called_once()
|
|
upserted = repository.upsert_listings.call_args[0][0]
|
|
assert len(upserted) == 1
|
|
assert upserted[0].square_meters == 75.0
|
|
|
|
async def test_detect_floorplan_skips_already_processed(self) -> None:
|
|
listing = _make_listing(square_meters=50.0)
|
|
repository = MagicMock()
|
|
repository.get_listings = AsyncMock(return_value=[listing])
|
|
repository.upsert_listings = AsyncMock(return_value=[])
|
|
|
|
await detect_floorplan(repository)
|
|
|
|
repository.upsert_listings.assert_called_once()
|
|
upserted = repository.upsert_listings.call_args[0][0]
|
|
assert len(upserted) == 0
|