perfect-postcode/finder/test_rightmove_concurrency.py
2026-07-03 18:28:56 +01:00

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()