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:
parent
7e05b3c971
commit
150342bb9e
48 changed files with 5029 additions and 990 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue