Refactor codebase following Clean Code principles and add 229 tests

- Extract helpers to reduce function sizes (listing_tasks, app.py, query.py, listing_fetcher)
  - Replace nonlocal mutations with _PipelineState dataclass in listing_tasks
  - Fix bugs: isinstance→equality check in repository, verify_exp for OIDC tokens
  - Consolidate duplicate filter methods in listing_repository
  - Move hardcoded config to env vars with backward-compatible defaults
  - Simplify CLI decorator to auto-build QueryParameters
  - Add deprecation docstring to data_access.py
  - Test count: 158 → 387 (all passing)
This commit is contained in:
Viktor Barzin 2026-02-07 20:19:57 +00:00
parent 7e05b3c971
commit 150342bb9e
No known key found for this signature in database
GPG key ID: 0EB088298288D958
48 changed files with 5029 additions and 990 deletions

View file

@ -1,16 +1,24 @@
"""Unit tests for Listing models."""
import dataclasses
from datetime import datetime
import json
import pytest
from pydantic import ValidationError
from models.listing import (
BuyListing,
DestinationMode,
FurnishType,
ListingSite,
ListingType,
PriceHistoryItem,
QueryParameters,
RentListing,
Listing,
Route,
RouteLegStep,
)
from rec.routing import TravelMode
class TestListing:
@ -341,3 +349,190 @@ class TestBuyListing:
lease_left=120,
)
assert listing.lease_left == 120
def _make_listing_with_routing(routing_info_json: str | None) -> RentListing:
"""Helper to create a RentListing with given routing_info_json."""
return RentListing(
id=1,
price=2000.0,
number_of_bedrooms=2,
square_meters=50.0,
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=routing_info_json,
furnish_type=FurnishType.FURNISHED,
available_from=None,
)
def _make_sample_routing_info() -> dict[DestinationMode, list[Route]]:
"""Helper to create sample routing info for tests."""
destination_mode = DestinationMode(
destination_address="London Bridge",
travel_mode=TravelMode.TRANSIT,
)
routes = [
Route(
legs=[
RouteLegStep(
distance_meters=500,
duration_s=120,
travel_mode=TravelMode.WALK,
),
RouteLegStep(
distance_meters=4000,
duration_s=480,
travel_mode=TravelMode.TRANSIT,
),
],
distance_meters=4500,
duration_s=600,
)
]
return {destination_mode: routes}
class TestQueryParametersValidation:
"""Tests for QueryParameters validation."""
def test_valid_parameters(self) -> None:
"""Basic valid QueryParameters creation."""
params = QueryParameters(
listing_type=ListingType.RENT,
min_price=1000,
max_price=3000,
min_bedrooms=1,
max_bedrooms=3,
)
assert params.min_price == 1000
assert params.max_price == 3000
assert params.min_bedrooms == 1
assert params.max_bedrooms == 3
def test_invalid_price_range_raises(self) -> None:
"""min_price > max_price should raise ValidationError."""
with pytest.raises(ValidationError, match="min_price.*must be <= max_price"):
QueryParameters(
listing_type=ListingType.RENT,
min_price=5000,
max_price=1000,
)
def test_invalid_bedroom_range_raises(self) -> None:
"""min_bedrooms > max_bedrooms should raise ValidationError."""
with pytest.raises(ValidationError, match="min_bedrooms.*must be <= max_bedrooms"):
QueryParameters(
listing_type=ListingType.RENT,
min_bedrooms=5,
max_bedrooms=2,
)
def test_negative_bedrooms_raises(self) -> None:
"""Negative bedroom counts should raise ValidationError."""
with pytest.raises(ValidationError, match="min_bedrooms.*must be non-negative"):
QueryParameters(
listing_type=ListingType.RENT,
min_bedrooms=-1,
max_bedrooms=3,
)
class TestDestinationMode:
"""Tests for DestinationMode."""
def test_to_dict(self) -> None:
"""Test to_dict returns correct dict."""
dm = DestinationMode(
destination_address="London Bridge",
travel_mode=TravelMode.TRANSIT,
)
result = dm.to_dict()
assert result == {
"destination_address": "London Bridge",
"travel_mode": TravelMode.TRANSIT,
}
def test_hash(self) -> None:
"""Test hashing works correctly."""
dm1 = DestinationMode(
destination_address="London Bridge",
travel_mode=TravelMode.TRANSIT,
)
dm2 = DestinationMode(
destination_address="London Bridge",
travel_mode=TravelMode.TRANSIT,
)
dm3 = DestinationMode(
destination_address="King's Cross",
travel_mode=TravelMode.TRANSIT,
)
assert hash(dm1) == hash(dm2)
assert dm1 == dm2
assert hash(dm1) != hash(dm3)
# Can be used as dict key
d = {dm1: "route1"}
assert d[dm2] == "route1"
class TestRoutingInfoSerialization:
"""Tests for routing info via RouteSerializer."""
def test_routing_info_property_returns_parsed_routes(self) -> None:
"""Test routing_info property deserializes correctly."""
routing_info = _make_sample_routing_info()
listing = _make_listing_with_routing(None)
serialized = listing.serialize_routing_info(routing_info)
listing.routing_info_json = serialized
result = listing.routing_info
assert len(result) == 1
dest_mode = list(result.keys())[0]
assert dest_mode.destination_address == "London Bridge"
assert dest_mode.travel_mode == TravelMode.TRANSIT
routes = result[dest_mode]
assert len(routes) == 1
assert routes[0].distance_meters == 4500
assert routes[0].duration_s == 600
assert len(routes[0].legs) == 2
assert routes[0].legs[0].distance_meters == 500
assert routes[0].legs[0].travel_mode == TravelMode.WALK
def test_routing_info_empty_json(self) -> None:
"""Test routing_info with no routing data."""
listing = _make_listing_with_routing(None)
assert listing.routing_info == {}
def test_serialize_routing_info_roundtrip(self) -> None:
"""Test serialize then deserialize via routing_info property."""
routing_info = _make_sample_routing_info()
listing = _make_listing_with_routing(None)
# Serialize
serialized = listing.serialize_routing_info(routing_info)
assert isinstance(serialized, str)
# Assign and deserialize via property
listing.routing_info_json = serialized
deserialized = listing.routing_info
# Compare
orig_dm = list(routing_info.keys())[0]
result_dm = list(deserialized.keys())[0]
assert orig_dm.destination_address == result_dm.destination_address
assert orig_dm.travel_mode == result_dm.travel_mode
orig_route = routing_info[orig_dm][0]
result_route = deserialized[result_dm][0]
assert orig_route.distance_meters == result_route.distance_meters
assert orig_route.duration_s == result_route.duration_s
assert len(orig_route.legs) == len(result_route.legs)