Skip to content

Commit c74857e

Browse files
committed
Log failed referral queries and bulk deletes
1 parent b7b872c commit c74857e

5 files changed

Lines changed: 105 additions & 6 deletions

File tree

‎app/logging/events.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ class Log(_Base):
103103
_SIEM: (*_CALLER, "resource_ura"),
104104
},
105105
)
106+
REFERRAL_QUERY_FAILED = LogEvent( # NVI-REF-007
107+
"900406",
108+
logging.WARNING,
109+
(_APP, _SIEM),
110+
{
111+
_APP: (*_CALLER, "http_status", "error_reason"),
112+
_SIEM: (*_CALLER, "http_status", "error_reason"),
113+
},
114+
)
106115

107116
REFERRAL_DELETED = LogEvent( # NVI-DEL-001
108117
"900500",
@@ -133,6 +142,15 @@ class Log(_Base):
133142
_SIEM: (*_CALLER, "deleted_count"),
134143
},
135144
)
145+
REFERRALS_DELETE_FAILED = LogEvent( # NVI-DEL-005
146+
"900504",
147+
logging.WARNING,
148+
(_APP, _SIEM),
149+
{
150+
_APP: (*_CALLER, "http_status", "error_reason"),
151+
_SIEM: (*_CALLER, "http_status", "error_reason"),
152+
},
153+
)
136154

137155
LOCALIZATION_SUCCESS = LogEvent( # NVI-LOC-001
138156
"900600",

‎app/services/pseudonym_referral_service.py‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,27 @@ def query(
7272
limit: int | None = None,
7373
offset: int = 0,
7474
) -> tuple[int, Sequence[ReferralEntity]]:
75-
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
76-
return self._referral_service.query(
77-
ura_number, encrypted_pseudonym=encrypted, source=source, limit=limit, offset=offset
78-
)
75+
try:
76+
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
77+
return self._referral_service.query(
78+
ura_number, encrypted_pseudonym=encrypted, source=source, limit=limit, offset=offset
79+
)
80+
except Exception as exc:
81+
_log_failure(Log.REFERRAL_QUERY_FAILED, "Referral query failed", exc)
82+
raise
7983

8084
def delete_many(
8185
self,
8286
ura_number: UraNumber,
8387
pseudonym: OprfPseudonym | None = None,
8488
source: str | None = None,
8589
) -> int:
86-
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
87-
return self._referral_service.delete_many(ura_number, encrypted_pseudonym=encrypted, source=source)
90+
try:
91+
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
92+
return self._referral_service.delete_many(ura_number, encrypted_pseudonym=encrypted, source=source)
93+
except Exception as exc:
94+
_log_failure(Log.REFERRALS_DELETE_FAILED, "Referral deletion failed", exc)
95+
raise
8896

8997
def _resolve_encrypted(self, pseudonym: OprfPseudonym) -> EncryptedPseudonym:
9098
return self._pseudonym_resolver.resolve(pseudonym).encrypted

‎tests/logging/test_events.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ def test_defines_every_required_event(self) -> None:
3636
("REFERRAL_REGISTRATION_FAILED", "900404"),
3737
("REFERRAL_REGISTRATION_ERROR", "900404"),
3838
("REFERRAL_ACCESS_DENIED", "900405"),
39+
("REFERRAL_QUERY_FAILED", "900406"),
3940
("REFERRAL_DELETED", "900500"),
4041
("ALL_PATIENT_REFERRALS_DELETED", "900501"),
4142
("ALL_URA_REFERRALS_DELETED", "900503"),
43+
("REFERRALS_DELETE_FAILED", "900504"),
4244
("LOCALIZATION_SUCCESS", "900600"),
4345
("LOCALIZATION_FAILED", "900601"),
4446
("LOCALIZATION_ERROR", "900601"),

‎tests/routers/test_localization_list.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from typing import Any
3+
from unittest.mock import Mock
34

45
import pytest
56
from fastapi.testclient import TestClient
@@ -11,6 +12,7 @@
1112
from app.models.fhir.resources.data import PSEUDONYM_SYSTEM
1213
from app.models.pseudonym import EncryptedPseudonym
1314
from app.models.ura import UraNumber
15+
from app.services.exceptions import PseudonymExchangeError
1416
from app.services.key_info import KeyInfoService
1517
from app.services.referral_service import ReferralService
1618
from tests.conftest import TEST_PSEUDONYM_TOKEN, make_list_resource
@@ -435,3 +437,29 @@ def test_delete_by_query_reports_not_found_when_nothing_matched(self, client: Te
435437

436438
assert response.status_code == 404
437439
assert response.json()["issue"][0]["code"] == "not-found"
440+
441+
442+
@pytest.fixture()
443+
def rejecting_exchange(crypto_client: CryptoServiceApiClientMock, monkeypatch: pytest.MonkeyPatch) -> None:
444+
rejected = PseudonymExchangeError("Invalid pseudonym or oprf_key")
445+
monkeypatch.setattr(crypto_client, "exchange", Mock(side_effect=rejected))
446+
447+
448+
@pytest.mark.usefixtures("rejecting_exchange")
449+
def test_logs_a_failed_search_as_a_failed_query(client: TestClient) -> None:
450+
params = {"subject:identifier": TEST_PSEUDONYM_TOKEN, "source:identifier": TEST_SOURCE_ID}
451+
with capture_records() as records:
452+
response = client.get("/fhir/List", params=params)
453+
454+
assert response.status_code == 400
455+
assert_event_emitted(records, Log.REFERRAL_QUERY_FAILED, organization_identifier=TEST_URA, http_status=400)
456+
assert records.for_event(Log.LOCALIZATION_FAILED) == []
457+
458+
459+
@pytest.mark.usefixtures("rejecting_exchange")
460+
def test_logs_a_failed_bulk_delete(client: TestClient) -> None:
461+
with capture_records() as records:
462+
response = client.delete("/fhir/List", params={"subject:identifier": TEST_PSEUDONYM_TOKEN})
463+
464+
assert response.status_code == 400
465+
assert_event_emitted(records, Log.REFERRALS_DELETE_FAILED, organization_identifier=TEST_URA, http_status=400)

‎tests/services/test_pseudonym_referral_service.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,46 @@ def test_delete_many_without_pseudonym_removes_referrals_of_the_source(
245245

246246
assert pseudonym_referral_service.delete_many(ura_number, source=SOURCE) == 2
247247
assert pseudonym_referral_service.query(ura_number)[0] == 1
248+
249+
250+
def test_query_logs_a_failure_when_the_pseudonym_is_rejected(
251+
rejecting_service: PseudonymReferralService, ura_number: UraNumber, caplog: pytest.LogCaptureFixture
252+
) -> None:
253+
with caplog.at_level(logging.INFO), pytest.raises(PseudonymExchangeError):
254+
rejecting_service.query(ura_number, pseudonym=PSEUDONYM)
255+
256+
(fields,) = logged_fields(caplog, Log.REFERRAL_QUERY_FAILED)
257+
assert fields["http_status"] == 400
258+
259+
260+
def test_query_without_a_pseudonym_logs_no_failure(
261+
pseudonym_referral_service: PseudonymReferralService, ura_number: UraNumber, caplog: pytest.LogCaptureFixture
262+
) -> None:
263+
with caplog.at_level(logging.INFO):
264+
pseudonym_referral_service.query(ura_number)
265+
266+
assert logged_fields(caplog, Log.REFERRAL_QUERY_FAILED) == []
267+
268+
269+
def test_delete_many_logs_a_failure_when_it_breaks(
270+
failing_service: PseudonymReferralService, ura_number: UraNumber, caplog: pytest.LogCaptureFixture
271+
) -> None:
272+
with caplog.at_level(logging.INFO), pytest.raises(RuntimeError):
273+
failing_service.delete_many(ura_number, pseudonym=PSEUDONYM, source=SOURCE)
274+
275+
(fields,) = logged_fields(caplog, Log.REFERRALS_DELETE_FAILED)
276+
assert fields["http_status"] == 500
277+
assert fields["error_reason"] == "boom"
278+
279+
280+
def test_delete_many_logs_no_failure_when_nothing_matches(
281+
pseudonym_referral_service: PseudonymReferralService,
282+
key_info_service: KeyInfoService,
283+
ura_number: UraNumber,
284+
caplog: pytest.LogCaptureFixture,
285+
) -> None:
286+
key_info_service.add_one("nvi-label", "AES_CBC")
287+
with caplog.at_level(logging.INFO):
288+
assert pseudonym_referral_service.delete_many(ura_number, pseudonym=PSEUDONYM, source=SOURCE) == 0
289+
290+
assert logged_fields(caplog, Log.REFERRALS_DELETE_FAILED) == []

0 commit comments

Comments
 (0)