from unittest.mock import MagicMock, patch, call import pytest from backend.epc_client.client import EpcClientService from backend.utils.epc_address_match import find_best_epc_match from datatypes.epc.search import EpcSearchResult from backend.epc_client.exceptions import EpcNotFoundError, EpcRateLimitError from datatypes.epc.domain.epc_property_data import EpcPropertyData from backend.epc_client.tests.conftest import make_search_row def _mock_response(status_code=200, json_data=None): resp = MagicMock() resp.status_code = status_code resp.is_success = 200 <= status_code < 300 resp.json.return_value = json_data or {} resp.text = str(json_data) return resp # --------------------------------------------------------------------------- # Test 1: get_by_certificate_number happy path # --------------------------------------------------------------------------- def test_get_by_certificate_number_returns_epc_property_data(epc_service, rdsap_21_0_1_cert): cert_response = {"data": rdsap_21_0_1_cert} with patch("httpx.get", return_value=_mock_response(200, cert_response)): result = epc_service.get_by_certificate_number("CERT-001") assert isinstance(result, EpcPropertyData) # --------------------------------------------------------------------------- # Test 2: get_by_certificate_number 404 → EpcNotFoundError # --------------------------------------------------------------------------- def test_get_by_certificate_number_404_raises_not_found(epc_service): with patch("httpx.get", return_value=_mock_response(404)): with pytest.raises(EpcNotFoundError): epc_service.get_by_certificate_number("BAD-CERT") # --------------------------------------------------------------------------- # Test 3: 429 retried, succeeds on 3rd attempt # --------------------------------------------------------------------------- def test_get_by_certificate_number_retries_on_429_and_succeeds(epc_service, rdsap_21_0_1_cert): cert_response = {"data": rdsap_21_0_1_cert} responses = [ _mock_response(429), _mock_response(429), _mock_response(200, cert_response), ] with patch("httpx.get", side_effect=responses), patch("time.sleep"): result = epc_service.get_by_certificate_number("CERT-001") assert isinstance(result, EpcPropertyData) # --------------------------------------------------------------------------- # Test 4: get_by_uprn empty search → None # --------------------------------------------------------------------------- def test_get_by_uprn_returns_none_when_no_results(epc_service): with patch("httpx.get", return_value=_mock_response(200, {"data": []})): result = epc_service.get_by_uprn(100023336956) assert result is None # --------------------------------------------------------------------------- # Test 5: get_by_uprn multiple results → fetches latest by registration_date # --------------------------------------------------------------------------- def test_get_by_uprn_picks_most_recent_certificate(epc_service, rdsap_21_0_1_cert): search_rows = [ make_search_row(cert_num="CERT-OLD", registration_date="2022-01-01"), make_search_row(cert_num="CERT-NEW", registration_date="2024-06-01"), make_search_row(cert_num="CERT-MID", registration_date="2023-03-15"), ] cert_response = {"data": rdsap_21_0_1_cert} def fake_get(url, params=None, **kwargs): if "search" in url: return _mock_response(200, {"data": search_rows}) return _mock_response(200, cert_response) with patch("httpx.get", side_effect=fake_get) as mock_get: result = epc_service.get_by_uprn(100023336956) assert isinstance(result, EpcPropertyData) # Second call must be for the most recent cert cert_call = mock_get.call_args_list[1] assert cert_call.kwargs["params"]["certificate_number"] == "CERT-NEW" # --------------------------------------------------------------------------- # Test 6: search_by_postcode returns list[EpcSearchResult] # --------------------------------------------------------------------------- def test_search_by_postcode_returns_results(epc_service): rows = [ make_search_row(cert_num="CERT-A", address_line_1="1 High Street"), make_search_row(cert_num="CERT-B", address_line_1="2 High Street"), ] with patch("httpx.get", return_value=_mock_response(200, {"data": rows})): results = epc_service.search_by_postcode("SW1A 1AA") assert len(results) == 2 assert all(isinstance(r, EpcSearchResult) for r in results) assert results[0].certificate_number == "CERT-A" assert results[1].address_line_1 == "2 High Street" # --------------------------------------------------------------------------- # Test 7: search_by_postcode 404 → empty list # --------------------------------------------------------------------------- def test_search_by_postcode_404_returns_empty_list(epc_service): with patch("httpx.get", return_value=_mock_response(404)): results = epc_service.search_by_postcode("ZZ9 9ZZ") assert results == [] # --------------------------------------------------------------------------- # Tests 8-10: find_best_epc_match — real scoring, only HTTP mocked # --------------------------------------------------------------------------- def test_find_best_match_clear_winner_on_first_pass(epc_service, rdsap_21_0_1_cert): search_rows = [ make_search_row(cert_num="CERT-WIN", address_line_1="1 High Street"), make_search_row(cert_num="CERT-LOSE", address_line_1="99 Nowhere Lane"), ] cert_response = {"data": rdsap_21_0_1_cert} def fake_get(url, params=None, **kwargs): if "search" in url: return _mock_response(200, {"data": search_rows}) return _mock_response(200, cert_response) with patch("httpx.get", side_effect=fake_get): result = find_best_epc_match(epc_service, "SW1A 1AA", "1 High Street") assert isinstance(result, EpcPropertyData) def test_find_best_match_resolves_on_second_pass_using_full_address(epc_service, rdsap_21_0_1_cert): # Both candidates share address_line_1 — round 1 is ambiguous. # Round 2 scores against full_address and picks the correct floor. search_rows = [ make_search_row( cert_num="CERT-A", address_line_1="1 High Street", address_line_2="Ground Floor", ), make_search_row( cert_num="CERT-B", address_line_1="1 High Street", address_line_2="First Floor", ), ] cert_response = {"data": rdsap_21_0_1_cert} def fake_get(url, params=None, **kwargs): if "search" in url: return _mock_response(200, {"data": search_rows}) return _mock_response(200, cert_response) with patch("httpx.get", side_effect=fake_get): result = find_best_epc_match(epc_service, "SW1A 1AA", "1 High Street Ground Floor") assert isinstance(result, EpcPropertyData) def test_find_best_match_returns_none_when_no_good_match(epc_service): search_rows = [make_search_row(cert_num="CERT-X", address_line_1="99 Nowhere Lane")] with patch("httpx.get", return_value=_mock_response(200, {"data": search_rows})): result = find_best_epc_match(epc_service, "SW1A 1AA", "1 Completely Different Road") assert result is None