From 0514a3a54e20deeaea8a0a71c3a2dd4d45443910 Mon Sep 17 00:00:00 2001 From: GeoWill Date: Wed, 1 Oct 2025 16:42:35 +0100 Subject: [PATCH 1/4] Bump dc-logging-utils --- common/pyproject.toml | 2 +- uv.lock | 17 ++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/common/pyproject.toml b/common/pyproject.toml index 278526c3..617eb31e 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ # note: PEP508 syntax here instead of tool.uv.sources # so we can pip install the package - "dc-logging-utils @ git+https://github.com/DemocracyClub/dc_logging.git@1.0.2" + "dc-logging-utils @ git+https://github.com/DemocracyClub/dc_logging.git@3.0.0" ] [tool.uv] diff --git a/uv.lock b/uv.lock index 0230c087..da71a4a6 100644 --- a/uv.lock +++ b/uv.lock @@ -146,15 +146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/47/e35f788047c91110f48703a6254e5c84e33111b3291f7b57a653ca00accf/botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be", size = 12468049, upload-time = "2024-08-15T19:25:18.301Z" }, ] -[[package]] -name = "cachetools" -version = "5.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, -] - [[package]] name = "certifi" version = "2025.6.15" @@ -307,7 +298,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "dc-logging-utils", git = "https://github.com/DemocracyClub/dc_logging.git?rev=1.0.2" }, + { name = "dc-logging-utils", git = "https://github.com/DemocracyClub/dc_logging.git?rev=3.0.0" }, { name = "httpx", extras = ["http2"], specifier = "==0.26.0" }, { name = "mangum", specifier = "==0.17.0" }, { name = "sentry-sdk", extras = ["starlette"], specifier = "==2.32.0" }, @@ -337,10 +328,10 @@ dependencies = [ [[package]] name = "dc-logging-utils" -version = "1.0.2" -source = { git = "https://github.com/DemocracyClub/dc_logging.git?rev=1.0.2#804b5fd766208b47b21af05a25a8acefc602cb4d" } +version = "3.0.0" +source = { git = "https://github.com/DemocracyClub/dc_logging.git?rev=3.0.0#523a6be28394a2ab8b2323cc14636c3b38bd2cda" } dependencies = [ - { name = "cachetools" }, + { name = "boto3" }, ] [[package]] From 6ee2a8e9acd47639ea88a05f6b1fafdceefb2a34 Mon Sep 17 00:00:00 2001 From: GeoWill Date: Thu, 2 Oct 2025 16:37:48 +0100 Subject: [PATCH 2/4] Only log postcode if postcode isn't split If WDIV returns a list of addresses don't log. We will log the result returned if there is a follow up request on the address endpoint. --- .../v1/voting_information/postcode.py | 23 +++++++---- tests/v1/voting_information/test_postcode.py | 40 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/api/endpoints/v1/voting_information/postcode.py b/api/endpoints/v1/voting_information/postcode.py index 49ba0982..0a7c10cc 100644 --- a/api/endpoints/v1/voting_information/postcode.py +++ b/api/endpoints/v1/voting_information/postcode.py @@ -10,14 +10,6 @@ async def get_postcode(request: Request): postcode = request.path_params["postcode"].upper() logger: DCWidePostcodeLoggingClient = request.app.state.POSTCODE_LOGGER - entry = logger.entry_class( - postcode=str(postcode), - dc_product=logger.dc_product.aggregator_api, - api_key=request.scope["api_user"].api_key, - calls_devs_dc_api=False, - **request.scope["utm_dict"], - ) - logger.log(entry) client = WdivWcivfApiClient(query_params=request.query_params) try: @@ -31,4 +23,19 @@ async def get_postcode(request: Request): else: result = stitcher.make_result_known_response() + has_ballot = any( + date.get("ballots") for date in result.get("dates", []) + ) + + logger.log( + logger.entry_class( + postcode=str(postcode), + dc_product=logger.dc_product.aggregator_api, + api_key=request.scope["api_user"].api_key, + calls_devs_dc_api=False, + had_election=has_ballot, + **request.scope["utm_dict"], + ) + ) + return JSONResponse(result) diff --git a/tests/v1/voting_information/test_postcode.py b/tests/v1/voting_information/test_postcode.py index 22616723..412f04b2 100644 --- a/tests/v1/voting_information/test_postcode.py +++ b/tests/v1/voting_information/test_postcode.py @@ -1,4 +1,5 @@ import logging +from unittest.mock import patch import httpx import pytest @@ -42,7 +43,14 @@ def test_valid( expected = load_sandbox_output( postcode, base_url="http://testserver/api/v1/" ) - response = vi_app_client.get(f"/api/v1/postcode/{postcode}/") + with patch( + "api.endpoints.v1.voting_information.address.DCWidePostcodeLoggingClient.log" + ) as mock_log: + response = vi_app_client.get(f"/api/v1/postcode/{postcode}/") + if postcode == "AA13AA": + mock_log.assert_not_called() # address picker + else: + mock_log.assert_called_once() assert response.status_code == 200 assert response.json() == expected @@ -79,13 +87,26 @@ def test_wcivf_missing_ballot(respx_mock, vi_app_client, api_settings): assert "mayor.lewisham.2018-05-03" in resp.text -def test_logging_working(respx_mock, vi_app_client, caplog, api_settings): +@pytest.mark.parametrize( + "postcode,fixture_name,expected_had_election", + [ + ("SW1A1AA", "addresspc_endpoints/test_multiple_elections", "true"), + ("AA11AA", "addresspc_endpoints/test_no_elections", "false"), + ], +) +def test_logging_working( + respx_mock, + vi_app_client, + caplog, + api_settings, + postcode, + fixture_name, + expected_had_election, +): caplog.set_level(logging.DEBUG) - fixture = load_fixture( - "addresspc_endpoints/test_multiple_elections", "wdiv" - ) + fixture = load_fixture(fixture_name, "wdiv") respx_mock.get( - "https://wheredoivote.co.uk/api/beta/postcode/SW1A1AA/" + f"https://wheredoivote.co.uk/api/beta/postcode/{postcode}/" ).mock( return_value=httpx.Response( 200, @@ -100,14 +121,14 @@ def test_logging_working(respx_mock, vi_app_client, caplog, api_settings): return_value=httpx.Response( 200, json=load_fixture( - "addresspc_endpoints/test_multiple_elections", + fixture_name, ballot["ballot_paper_id"], ), ) ) vi_app_client.get( - "/api/v1/postcode/SW1A1AA/", + f"/api/v1/postcode/{postcode}/", params={ "foo": "bar", "utm_source": "test", @@ -121,9 +142,10 @@ def test_logging_working(respx_mock, vi_app_client, caplog, api_settings): if record.message.startswith("dc-postcode-searches"): logging_message = record assert logging_message - assert '"postcode": "SW1A1AA"' in logging_message.message + assert f'"postcode": "{postcode}"' in logging_message.message assert '"dc_product": "AGGREGATOR_API"' in logging_message.message assert '"api_key": "local-dev"' in logging_message.message assert '"utm_source": "test"' in logging_message.message assert '"utm_campaign": "better_tracking"' in logging_message.message assert '"utm_medium": "pytest"' in logging_message.message + assert f'"had_election": {expected_had_election}' in logging_message.message From 32308951a6942d74cb8f98d19317e0412a8aea94 Mon Sep 17 00:00:00 2001 From: GeoWill Date: Tue, 7 Oct 2025 15:38:08 +0100 Subject: [PATCH 3/4] Log address searches --- .../v1/voting_information/address.py | 34 ++++- .../test_multiple_elections/wdiv_address.json | 94 ++++++++++++ .../test_no_elections/wdiv_address.json | 45 ++++++ .../wdiv_address.json | 101 +++++++++++++ .../wdiv_address.json | 58 +++++++ tests/v1/voting_information/test_address.py | 143 +++++++++++++++++- 6 files changed, 468 insertions(+), 7 deletions(-) create mode 100644 tests/v1/test_data/addresspc_endpoints/test_multiple_elections/wdiv_address.json create mode 100644 tests/v1/test_data/addresspc_endpoints/test_no_elections/wdiv_address.json create mode 100644 tests/v1/test_data/addresspc_endpoints/test_one_election_station_known_with_candidates/wdiv_address.json create mode 100644 tests/v1/test_data/addresspc_endpoints/test_one_election_station_not_known_with_candidates/wdiv_address.json diff --git a/api/endpoints/v1/voting_information/address.py b/api/endpoints/v1/voting_information/address.py index 9df74cd4..cc402810 100644 --- a/api/endpoints/v1/voting_information/address.py +++ b/api/endpoints/v1/voting_information/address.py @@ -1,4 +1,6 @@ +from dc_logging_client import DCWidePostcodeLoggingClient from elections_api_client import WdivWcivfApiClient +from sentry_sdk import logger as sentry_logger from starlette.requests import Request from starlette.responses import JSONResponse from stitcher import Stitcher @@ -8,10 +10,40 @@ def get_address(request: Request): uprn: str = request.path_params["uprn"] + logger: DCWidePostcodeLoggingClient = request.app.state.POSTCODE_LOGGER client = WdivWcivfApiClient(query_params=request.query_params) try: wdiv, wcivf = client.get_data_for_address(uprn) except UpstreamApiError as error: return JSONResponse(error.message, status_code=error.status) stitcher = Stitcher(wdiv, wcivf, request) - return JSONResponse(stitcher.make_result_known_response()) + result = stitcher.make_result_known_response() + + has_ballot = any(date.get("ballots") for date in result.get("dates", [])) + + # Try and get the postcode from the postcode location + postcode_location = result.get("postcode_location", {}) + properties = postcode_location.get("properties", {}) + postcode = properties.get("postcode", None) + + if postcode: + logger.log( + logger.entry_class( + postcode=str(postcode), + dc_product=logger.dc_product.aggregator_api, + api_key=request.scope["api_user"].api_key, + calls_devs_dc_api=False, + had_election=has_ballot, + **request.scope["utm_dict"], + ) + ) + if not postcode: + sentry_logger.error( + f"WDIV didn't return a postcode for uprn:{uprn}", + attributes={ + "uprn": uprn, + "api_key": request.scope["api_user"].api_key, + }, + ) + + return JSONResponse(result) diff --git a/tests/v1/test_data/addresspc_endpoints/test_multiple_elections/wdiv_address.json b/tests/v1/test_data/addresspc_endpoints/test_multiple_elections/wdiv_address.json new file mode 100644 index 00000000..55a303c1 --- /dev/null +++ b/tests/v1/test_data/addresspc_endpoints/test_multiple_elections/wdiv_address.json @@ -0,0 +1,94 @@ +{ + "polling_station_known": false, + "postcode_location": { + "type": "Feature", + "properties": { + "postcode": "AA1 4AA" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 0.008211636363636364, + 51.46623861363635 + ] + } + }, + "council": { + "url": "http://127.0.0.1:8001/api/beta/councils/LEW/", + "council_id": "LEW", + "name": "London Borough of Lewisham", + "nation": "England", + "email": "electoral.services@lewisham.gov.uk", + "phone": "020 8314 6086", + "website": "http://www.lewisham.gov.uk/", + "postcode": "SE6 4RU", + "address": "Electoral Registration Officer\nLewisham Town Hall\nCatford Road\nCatford", + "identifiers": [ + "E09000023" + ] + }, + "polling_station": null, + "advance_voting_station": null, + "addresses": [ + { + "url": "http://127.0.0.1:8000/api/beta/address/1-foo-street-bar-town/", + "address": "1 Foo St., Bar Town", + "postcode": "AA1 4AA", + "council": "London Borough of Lewisham", + "polling_station_id": "", + "uprn": "1-foo-street-bar-town" + } + ], + "report_problem_url": null, + "metadata": null, + "ballots": [ + { + "ballot_paper_id": "mayor.lewisham.2018-05-03", + "ballot_title": "Mayor of Lewisham election", + "poll_open_date": "2018-05-03", + "elected_role": "Mayor of Lewisham", + "metadata": null, + "cancelled": false, + "cancellation_reason": null, + "replaced_by": null, + "replaces": null, + "requires_voter_id": "EA-2022" + }, + { + "ballot_paper_id": "local.lewisham.blackheath.2018-05-03", + "ballot_title": "Lewisham local election Blackheath", + "poll_open_date": "2018-05-03", + "elected_role": "Local Councillor", + "metadata": null, + "cancelled": true, + "cancellation_reason": "UNDER_CONTESTED", + "replaced_by": "local.lewisham.blackheath.2018-05-10", + "replaces": null, + "requires_voter_id": "EA-2022" + }, + { + "ballot_paper_id": "local.lewisham.blackheath.2018-05-10", + "ballot_title": "Lewisham local election Blackheath", + "poll_open_date": "2018-05-10", + "elected_role": "Local Councillor", + "metadata": null, + "cancelled": false, + "cancellation_reason": null, + "replaced_by": null, + "replaces": "local.lewisham.blackheath.2018-05-03", + "requires_voter_id": "EA-2022" + }, + { + "ballot_paper_id": "parl.lewisham-east.by.2018-06-14", + "ballot_title": "Lewisham East by-election", + "poll_open_date": "2018-06-14", + "elected_role": "Member of Parliament", + "metadata": null, + "cancelled": false, + "cancellation_reason": null, + "replaced_by": null, + "replaces": null, + "requires_voter_id": "EA-2022" + } + ] +} diff --git a/tests/v1/test_data/addresspc_endpoints/test_no_elections/wdiv_address.json b/tests/v1/test_data/addresspc_endpoints/test_no_elections/wdiv_address.json new file mode 100644 index 00000000..07edbb63 --- /dev/null +++ b/tests/v1/test_data/addresspc_endpoints/test_no_elections/wdiv_address.json @@ -0,0 +1,45 @@ +{ + "polling_station_known": false, + "postcode_location": { + "type": "Feature", + "properties": { + "postcode": "AA1 1AA" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -0.13447605, + 51.489488200000004 + ] + } + }, + "council": { + "url": "https://wheredoivote.co.uk/api/beta/councils/WSM/", + "council_id": "WSM", + "name": "City of Westminster", + "nation": "England", + "email": "electoralservices@westminster.gov.uk", + "phone": "020 7641 2730", + "website": "http://www.westminster.gov.uk/", + "postcode": "WC2N 5HR", + "address": "Electoral Registration Officer\nWestminster City Council\n2nd Floor, City Hall\n5 Strand", + "identifiers": [ + "E09000033" + ] + }, + "polling_station": null, + "advance_voting_station": null, + "addresses": [ + { + "url": "http://127.0.0.1:8000/api/beta/address/1-foo-street-bar-town/", + "address": "1 Foo St., Bar Town", + "postcode": "AA1 1AA", + "council": "City of Westminster", + "polling_station_id": "", + "uprn": "1-foo-street-bar-town" + } + ], + "report_problem_url": null, + "metadata": null, + "ballots": [] +} \ No newline at end of file diff --git a/tests/v1/test_data/addresspc_endpoints/test_one_election_station_known_with_candidates/wdiv_address.json b/tests/v1/test_data/addresspc_endpoints/test_one_election_station_known_with_candidates/wdiv_address.json new file mode 100644 index 00000000..c74c29bf --- /dev/null +++ b/tests/v1/test_data/addresspc_endpoints/test_one_election_station_known_with_candidates/wdiv_address.json @@ -0,0 +1,101 @@ +{ + "polling_station_known": true, + "postcode_location": { + "type": "Feature", + "properties": { + "postcode": "AA1 2AA" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -0.1804843, + 51.5113775 + ] + } + }, + "council": { + "url": "https://wheredoivote.co.uk/api/beta/councils/WSM/", + "council_id": "WSM", + "name": "City of Westminster", + "nation": "England", + "email": "electoralservices@westminster.gov.uk", + "phone": "020 7641 2730", + "website": "http://www.westminster.gov.uk/", + "postcode": "WC2N 5HR", + "address": "Electoral Registration Officer\nWestminster City Council\n2nd Floor, City Hall\n5 Strand", + "identifiers": [ + "E09000033" + ] + }, + "polling_station": { + "id": "E09000033.39-york-room-lancaster-hall-hotel", + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.1785134375, + 51.512779025 + ] + }, + "properties": { + "urls": { + "detail": "http://127.0.0.1:8001/api/beta/pollingstations/?council_id=E09000033&station_id=39-york-room-lancaster-hall-hotel", + "geo": "http://127.0.0.1:8001/api/beta/pollingstations/geo/?council_id=E09000033&station_id=39-york-room-lancaster-hall-hotel" + }, + "council": "http://127.0.0.1:8001/api/beta/councils/E09000033/", + "station_id": "39-york-room-lancaster-hall-hotel", + "postcode": "W2 3EL", + "address": "York Room, Lancaster Hall Hotel\n35 Craven Terrace\nLondon" + } + }, + "advance_voting_station": { + "name": "Exeter Guildhall", + "address": "Exeter City Council\nCivic Centre\nParis Street\nExeter\nDevon", + "postcode": "EX1 1JN", + "location": { + "type": "Point", + "coordinates": [ + -3.524551005678706, + 50.72486002944331 + ] + }, + "opening_times": [ + [ + "2018-11-20", + "10:00:00", + "16:00:00" + ], + [ + "2018-11-21", + "10:00:00", + "16:00:00" + ] + ] + }, + "addresses": [ + { + "url": "http://127.0.0.1:8000/api/beta/address/1-foo-street-bar-town/", + "address": "1 Foo St., Bar Town", + "postcode": "AA1 2AA", + "council": "City of Westminster", + "polling_station_id": "", + "uprn": "1-foo-street-bar-town" + } + ], + "report_problem_url": "https://wheredoivote.co.uk/report_problem/?source=testing&source_url=testing", + "metadata": null, + "ballots": [ + { + "ballot_paper_id": "local.westminster.lancaster-gate.by.2018-11-22", + "ballot_title": "Westminster local election Lancaster Gate by-election", + "poll_open_date": "2018-11-22", + "elected_role": "Local Councillor", + "metadata": null, + "cancelled": false, + "cancellation_reason": null, + "replaced_by": null, + "replaces": null, + "requires_voter_id": "EA-2022" + } + ] +} diff --git a/tests/v1/test_data/addresspc_endpoints/test_one_election_station_not_known_with_candidates/wdiv_address.json b/tests/v1/test_data/addresspc_endpoints/test_one_election_station_not_known_with_candidates/wdiv_address.json new file mode 100644 index 00000000..bc8898b5 --- /dev/null +++ b/tests/v1/test_data/addresspc_endpoints/test_one_election_station_not_known_with_candidates/wdiv_address.json @@ -0,0 +1,58 @@ +{ + "polling_station_known": false, + "postcode_location": { + "type": "Feature", + "properties": { + "postcode": "AA1 2AB" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -0.1804843, + 51.5113775 + ] + } + }, + "council": { + "url": "https://wheredoivote.co.uk/api/beta/councils/WSM/", + "council_id": "WSM", + "name": "City of Westminster", + "nation": "England", + "email": "electoralservices@westminster.gov.uk", + "phone": "020 7641 2730", + "website": "http://www.westminster.gov.uk/", + "postcode": "WC2N 5HR", + "address": "Electoral Registration Officer\nWestminster City Council\n2nd Floor, City Hall\n5 Strand", + "identifiers": [ + "E09000033" + ] + }, + "polling_station": null, + "advance_voting_station": null, + "addresses": [ + { + "url": "http://127.0.0.1:8000/api/beta/address/1-foo-street-bar-town/", + "address": "1 Foo St., Bar Town", + "postcode": "AA1 2AB", + "council": "City of Westminster", + "polling_station_id": "", + "uprn": "1-foo-street-bar-town" + } + ], + "report_problem_url": null, + "metadata": null, + "ballots": [ + { + "ballot_paper_id": "local.westminster.lancaster-gate.by.2018-11-22", + "ballot_title": "Westminster local election Lancaster Gate by-election", + "poll_open_date": "2018-11-22", + "elected_role": "Local Councillor", + "metadata": null, + "cancelled": false, + "cancellation_reason": null, + "replaced_by": null, + "replaces": null, + "requires_voter_id": "EA-2022" + } + ] +} diff --git a/tests/v1/voting_information/test_address.py b/tests/v1/voting_information/test_address.py index 53bd406f..f1c71031 100644 --- a/tests/v1/voting_information/test_address.py +++ b/tests/v1/voting_information/test_address.py @@ -1,3 +1,6 @@ +import logging +from unittest.mock import patch + import httpx import pytest from voting_information.elections_api_client import ( @@ -18,7 +21,7 @@ def test_no_stitcher_error_with_mismatched_ballots(respx_mock, vi_app_client): """ fixture = load_fixture( - "addresspc_endpoints/test_multiple_elections", "wdiv" + "addresspc_endpoints/test_multiple_elections", "wdiv_address" ) respx_mock.get( "https://wheredoivote.co.uk/api/beta/address/1-foo-street-bar-town/?all_future_ballots=1&utm_medium=devs.DC+API" @@ -41,8 +44,11 @@ def test_no_stitcher_error_with_mismatched_ballots(respx_mock, vi_app_client): ), ) ) - - response = vi_app_client.get("/api/v1/address/1-foo-street-bar-town/") + with patch( + "api.endpoints.v1.voting_information.address.DCWidePostcodeLoggingClient.log" + ) as mock_log: + response = vi_app_client.get("/api/v1/address/1-foo-street-bar-town/") + mock_log.assert_called_once() assert response.status_code == 200 @@ -50,7 +56,7 @@ def test_no_stitcher_error_with_mismatched_ballots(respx_mock, vi_app_client): def test_valid(postcode, vi_app_client, respx_mock, api_settings): # iterate through the subset of applicable expected inputs/outputs # we test against in test_stitcher.py - fixture = load_fixture(fixture_map[postcode], "wdiv") + fixture = load_fixture(fixture_map[postcode], "wdiv_address") respx_mock.get( "https://wheredoivote.co.uk/api/beta/address/1-foo-street-bar-town/?all_future_ballots=1&utm_medium=devs.DC+API" ).mock(return_value=httpx.Response(200, json=fixture)) @@ -71,8 +77,133 @@ def test_valid(postcode, vi_app_client, respx_mock, api_settings): expected = load_sandbox_output( postcode, base_url="http://testserver/api/v1/" ) - response = vi_app_client.get( + + with patch( + "api.endpoints.v1.voting_information.address.DCWidePostcodeLoggingClient.log" + ) as mock_log: + response = vi_app_client.get( + "/api/v1/address/1-foo-street-bar-town/", + ) + mock_log.assert_called_once() + + assert expected == response.json() + assert response.status_code == 200 + + +@pytest.mark.parametrize( + "postcode,fixture_name,expected_had_election", + [ + ("AA1 4AA", "addresspc_endpoints/test_multiple_elections", "true"), + ("AA1 1AA", "addresspc_endpoints/test_no_elections", "false"), + ], +) +def test_logging_working( + respx_mock, + vi_app_client, + caplog, + api_settings, + postcode, + fixture_name, + expected_had_election, +): + caplog.set_level(logging.DEBUG) + fixture = load_fixture(fixture_name, "wdiv_address") + respx_mock.get( + "https://wheredoivote.co.uk/api/beta/address/1-foo-street-bar-town/?all_future_ballots=1&utm_medium=devs.DC+API" + ).mock( + return_value=httpx.Response( + 200, + json=fixture, + ) + ) + + for ballot in fixture["ballots"]: + respx_mock.get( + wcivf_ballot_cache_url_from_ballot(ballot["ballot_paper_id"]) + ).mock( + return_value=httpx.Response( + 200, + json=load_fixture( + fixture_name, + ballot["ballot_paper_id"], + ), + ) + ) + + vi_app_client.get( "/api/v1/address/1-foo-street-bar-town/", + params={ + "foo": "bar", + "utm_source": "test", + "utm_campaign": "better_tracking", + "utm_medium": "pytest", + }, ) - assert expected == response.json() + + logging_message = None + for record in caplog.records: + if record.message.startswith("dc-postcode-searches"): + logging_message = record + assert logging_message + assert f'"postcode": "{postcode}"' in logging_message.message + assert '"dc_product": "AGGREGATOR_API"' in logging_message.message + assert '"api_key": "local-dev"' in logging_message.message + assert '"utm_source": "test"' in logging_message.message + assert '"utm_campaign": "better_tracking"' in logging_message.message + assert '"utm_medium": "pytest"' in logging_message.message + assert f'"had_election": {expected_had_election}' in logging_message.message + + +def test_no_postcode_logs_error(respx_mock, vi_app_client, api_settings): + """ + When no postcode is found in the response, log error to Sentry but still return the response + """ + fixture = load_fixture( + "addresspc_endpoints/test_no_elections", "wdiv_address" + ) + respx_mock.get( + "https://wheredoivote.co.uk/api/beta/address/1-foo-street-bar-town/?all_future_ballots=1&utm_medium=devs.DC+API" + ).mock( + return_value=httpx.Response( + 200, + json=fixture, + ) + ) + + for ballot in fixture["ballots"]: + respx_mock.get( + wcivf_ballot_cache_url_from_ballot(ballot["ballot_paper_id"]) + ).mock( + return_value=httpx.Response( + 200, + json=load_fixture( + "addresspc_endpoints/test_no_elections", + ballot["ballot_paper_id"], + ), + ) + ) + + with patch( + "api.endpoints.v1.voting_information.address.sentry_logger.error" + ) as mock_sentry_error, patch( + "api.endpoints.v1.voting_information.address.Stitcher.make_result_known_response" + ) as mock_result: + # Mock the result to have no postcode + mock_result.return_value = { + "dates": [], + "postcode_location": {"properties": {}}, # No postcode + } + response = vi_app_client.get("/api/v1/address/1-foo-street-bar-town/") + + # Verify Sentry error was logged with correct message and attributes + mock_sentry_error.assert_called_once() + call_args = mock_sentry_error.call_args + assert ( + call_args[0][0] + == "WDIV didn't return a postcode for uprn:1-foo-street-bar-town" + ) + assert "uprn" in call_args[1]["attributes"] + assert "api_key" in call_args[1]["attributes"] + + # Verify response is still returned successfully assert response.status_code == 200 From 2e4ffc405e1fe13e55638532128b084753b1c8dd Mon Sep 17 00:00:00 2001 From: GeoWill Date: Tue, 21 Oct 2025 11:37:18 +0100 Subject: [PATCH 4/4] add setup remote docker step to build job --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 694ab5ab..2ae50c3c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,6 +114,7 @@ jobs: - restore_cache: keys: - v9-dependencies-{{ checksum "uv.lock" }} + - setup_remote_docker - run: printenv DJANGO_SETTINGS_MODULE SAM_CONFIG_FILE SAM_LAMBDA_CONFIG_ENV SAM_PUBLIC_CONFIG_ENV - run: printenv SECRET_KEY | md5sum - run: printenv AWS_ACCESS_KEY_ID | md5sum