165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
"""Tests for Rightmove's accurate-pin skip and concurrent detail fetching."""
|
|
|
|
import threading
|
|
|
|
import rightmove
|
|
from rightmove import _needs_detail_fetch, _paginate, _prime_detail_postcodes
|
|
|
|
|
|
def _prop(pid, pin=None):
|
|
loc = {"latitude": 51.5, "longitude": -0.1}
|
|
if pin is not None:
|
|
loc["pinType"] = pin
|
|
return {"id": pid, "location": loc}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _needs_detail_fetch — accurate-pin skip
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_needs_detail_fetch_skips_only_accurate_pins(monkeypatch):
|
|
monkeypatch.setattr(rightmove, "RIGHTMOVE_SKIP_DETAILS_FOR_ACCURATE_PINS", True)
|
|
assert _needs_detail_fetch(_prop("1", "ACCURATE_POINT")) is False
|
|
assert _needs_detail_fetch(_prop("2", "APPROXIMATE_POINT")) is True
|
|
assert _needs_detail_fetch(_prop("3")) is True # no pinType -> fetch
|
|
assert _needs_detail_fetch({"id": "4"}) is True # no location -> fetch
|
|
|
|
|
|
def test_needs_detail_fetch_disabled_always_fetches(monkeypatch):
|
|
monkeypatch.setattr(rightmove, "RIGHTMOVE_SKIP_DETAILS_FOR_ACCURATE_PINS", False)
|
|
assert _needs_detail_fetch(_prop("1", "ACCURATE_POINT")) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _prime_detail_postcodes — worklist selection + concurrent fetch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _record_fetcher(monkeypatch):
|
|
fetched = []
|
|
lock = threading.Lock()
|
|
|
|
def fake_fetch(_client, pid):
|
|
with lock:
|
|
fetched.append(pid)
|
|
rightmove._detail_postcode_cache[pid] = f"PC-{pid}"
|
|
return f"PC-{pid}"
|
|
|
|
monkeypatch.setattr(rightmove, "_fetch_detail_postcode", fake_fetch)
|
|
return fetched
|
|
|
|
|
|
def test_prime_skips_cached_accurate_and_dedups(monkeypatch):
|
|
rightmove._detail_postcode_cache.clear()
|
|
rightmove._detail_postcode_cache["cached"] = "SW9 9ZZ"
|
|
monkeypatch.setattr(rightmove, "RIGHTMOVE_SKIP_DETAILS_FOR_ACCURATE_PINS", True)
|
|
fetched = _record_fetcher(monkeypatch)
|
|
|
|
props = [
|
|
_prop("cached", "APPROXIMATE_POINT"), # already cached -> skip
|
|
_prop("acc", "ACCURATE_POINT"), # accurate pin -> skip
|
|
_prop("a", "APPROXIMATE_POINT"), # fetch
|
|
_prop("a", "APPROXIMATE_POINT"), # duplicate id -> skip
|
|
_prop("b"), # no pinType -> fetch
|
|
{"id": ""}, # no id -> skip
|
|
]
|
|
_prime_detail_postcodes(object(), props, True, 10)
|
|
|
|
assert sorted(fetched) == ["a", "b"]
|
|
assert rightmove._detail_postcode_cache["a"] == "PC-a"
|
|
assert rightmove._detail_postcode_cache["b"] == "PC-b"
|
|
rightmove._detail_postcode_cache.clear()
|
|
|
|
|
|
def test_prime_respects_detail_cap(monkeypatch):
|
|
rightmove._detail_postcode_cache.clear()
|
|
monkeypatch.setattr(rightmove, "RIGHTMOVE_SKIP_DETAILS_FOR_ACCURATE_PINS", True)
|
|
fetched = _record_fetcher(monkeypatch)
|
|
|
|
props = [_prop(str(i), "APPROXIMATE_POINT") for i in range(10)]
|
|
_prime_detail_postcodes(object(), props, True, 3)
|
|
|
|
assert sorted(fetched) == ["0", "1", "2"]
|
|
rightmove._detail_postcode_cache.clear()
|
|
|
|
|
|
def test_prime_is_a_noop_when_disabled_or_cap_zero(monkeypatch):
|
|
rightmove._detail_postcode_cache.clear()
|
|
|
|
def _boom(*_a): # pragma: no cover - must never be called
|
|
raise AssertionError("must not fetch when disabled")
|
|
|
|
monkeypatch.setattr(rightmove, "_fetch_detail_postcode", _boom)
|
|
_prime_detail_postcodes(object(), [_prop("a")], False, 10) # fetch_details off
|
|
_prime_detail_postcodes(object(), [_prop("a")], True, 0) # cap 0
|
|
assert rightmove._detail_postcode_cache == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _paginate — end-to-end (network stubbed): accurate pins fall back to
|
|
# coordinates, approximate pins use the detail postcode.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _StubIndex:
|
|
def __init__(self, postcode):
|
|
self._postcode = postcode
|
|
|
|
def nearest(self, _lat, _lng):
|
|
return self._postcode
|
|
|
|
|
|
def _full_prop(pid, pin):
|
|
return {
|
|
"id": pid,
|
|
"location": {"latitude": 51.477, "longitude": -0.116, "pinType": pin},
|
|
"price": {"amount": 500000, "displayPrices": []},
|
|
"displayAddress": "Caldwell Street, Stockwell, London, SW9",
|
|
"propertySubType": "Flat",
|
|
"bedrooms": 2,
|
|
"bathrooms": 1,
|
|
"propertyUrl": f"/properties/{pid}",
|
|
}
|
|
|
|
|
|
def test_paginate_uses_detail_for_approximate_and_coords_for_accurate(monkeypatch):
|
|
rightmove._detail_postcode_cache.clear()
|
|
monkeypatch.setattr(rightmove, "RIGHTMOVE_SKIP_DETAILS_FOR_ACCURATE_PINS", True)
|
|
|
|
raw = [
|
|
_full_prop("approx", "APPROXIMATE_POINT"),
|
|
_full_prop("acc", "ACCURATE_POINT"),
|
|
]
|
|
monkeypatch.setattr(
|
|
rightmove, "_collect_search_props", lambda *a, **k: (raw, len(raw))
|
|
)
|
|
|
|
fetched = []
|
|
|
|
def fake_fetch(_client, pid):
|
|
fetched.append(pid)
|
|
rightmove._detail_postcode_cache[pid] = "SW9 0HD"
|
|
return "SW9 0HD"
|
|
|
|
monkeypatch.setattr(rightmove, "_fetch_detail_postcode", fake_fetch)
|
|
|
|
props, count = _paginate(
|
|
object(),
|
|
"1",
|
|
"SW9",
|
|
{"channel": "BUY", "transactionType": "BUY", "sortType": "2"},
|
|
_StubIndex("SW9 1AA"),
|
|
fetch_details=True,
|
|
detail_cap=10,
|
|
)
|
|
|
|
assert count == 2
|
|
assert fetched == ["approx"] # accurate pin was never fetched
|
|
by_id = {p["id"]: p for p in props}
|
|
assert by_id["approx"]["Postcode"] == "SW9 0HD"
|
|
assert by_id["approx"]["Postcode source"] == "detail_address"
|
|
# Accurate pin keeps the coordinate-nearest postcode.
|
|
assert by_id["acc"]["Postcode"] == "SW9 1AA"
|
|
assert by_id["acc"]["Postcode source"] != "detail_address"
|
|
rightmove._detail_postcode_cache.clear()
|