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