Skip to content

Commit b4f2d60

Browse files
Log failed referral queries and bulk deletes
1 parent 430c553 commit b4f2d60

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
@@ -102,6 +102,15 @@ class Log(_Base):
102102
_SIEM: (*_CALLER, "resource_ura"),
103103
},
104104
)
105+
REFERRAL_QUERY_FAILED = LogEvent( # NVI-REF-007
106+
"900406",
107+
logging.WARNING,
108+
(_APP, _SIEM),
109+
{
110+
_APP: (*_CALLER, "http_status", "error_reason"),
111+
_SIEM: (*_CALLER, "http_status", "error_reason"),
112+
},
113+
)
105114

106115
REFERRAL_DELETED = LogEvent( # NVI-DEL-001
107116
"900500",
@@ -132,6 +141,15 @@ class Log(_Base):
132141
_SIEM: (*_CALLER, "deleted_count"),
133142
},
134143
)
144+
REFERRALS_DELETE_FAILED = LogEvent( # NVI-DEL-005
145+
"900504",
146+
logging.WARNING,
147+
(_APP, _SIEM),
148+
{
149+
_APP: (*_CALLER, "http_status", "error_reason"),
150+
_SIEM: (*_CALLER, "http_status", "error_reason"),
151+
},
152+
)
135153

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

‎app/services/pseudonym_referral_service.py‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,27 @@ def query(
6666
limit: int | None = None,
6767
offset: int = 0,
6868
) -> tuple[int, Sequence[ReferralEntity]]:
69-
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
70-
return self._referral_service.query(
71-
ura_number, encrypted_pseudonym=encrypted, source=source, limit=limit, offset=offset
72-
)
69+
try:
70+
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
71+
return self._referral_service.query(
72+
ura_number, encrypted_pseudonym=encrypted, source=source, limit=limit, offset=offset
73+
)
74+
except Exception as exc:
75+
_log_failure(Log.REFERRAL_QUERY_FAILED, "Referral query failed", exc)
76+
raise
7377

7478
def delete_many(
7579
self,
7680
ura_number: UraNumber,
7781
pseudonym: OprfPseudonym | None = None,
7882
source: str | None = None,
7983
) -> int:
80-
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
81-
return self._referral_service.delete_many(ura_number, encrypted_pseudonym=encrypted, source=source)
84+
try:
85+
encrypted = self._resolve_encrypted(pseudonym) if pseudonym else None
86+
return self._referral_service.delete_many(ura_number, encrypted_pseudonym=encrypted, source=source)
87+
except Exception as exc:
88+
_log_failure(Log.REFERRALS_DELETE_FAILED, "Referral deletion failed", exc)
89+
raise
8290

8391
def _resolve_encrypted(self, pseudonym: OprfPseudonym) -> EncryptedPseudonym:
8492
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
@@ -35,9 +35,11 @@ def test_defines_every_required_event(self) -> None:
3535
("REFERRALS_QUERIED", "900403"),
3636
("REFERRAL_REGISTRATION_FAILED", "900404"),
3737
("REFERRAL_ACCESS_DENIED", "900405"),
38+
("REFERRAL_QUERY_FAILED", "900406"),
3839
("REFERRAL_DELETED", "900500"),
3940
("ALL_PATIENT_REFERRALS_DELETED", "900501"),
4041
("ALL_URA_REFERRALS_DELETED", "900503"),
42+
("REFERRALS_DELETE_FAILED", "900504"),
4143
("LOCALIZATION_SUCCESS", "900600"),
4244
("LOCALIZATION_FAILED", "900601"),
4345
("LOCALIZATION_ERROR", "900601"),

‎tests/routers/test_localization_list.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any
2+
from unittest.mock import Mock
23

34
import pytest
45
from fastapi.testclient import TestClient
@@ -9,6 +10,7 @@
910
from app.models.auth.data import AuthorizationScope
1011
from app.models.pseudonym import EncryptedPseudonym
1112
from app.models.ura import UraNumber
13+
from app.services.exceptions import PseudonymExchangeError
1214
from app.services.key_info import KeyInfoService
1315
from app.services.referral_service import ReferralService
1416
from tests.conftest import TEST_PSEUDONYM_TOKEN, make_list_resource
@@ -367,3 +369,29 @@ def test_delete_by_query_reports_not_found_when_nothing_matched(self, client: Te
367369

368370
assert response.status_code == 404
369371
assert response.json()["issue"][0]["code"] == "not-found"
372+
373+
374+
@pytest.fixture()
375+
def rejecting_exchange(crypto_client: CryptoServiceApiClientMock, monkeypatch: pytest.MonkeyPatch) -> None:
376+
rejected = PseudonymExchangeError("Invalid pseudonym or oprf_key")
377+
monkeypatch.setattr(crypto_client, "exchange", Mock(side_effect=rejected))
378+
379+
380+
@pytest.mark.usefixtures("rejecting_exchange")
381+
def test_logs_a_failed_search_as_a_failed_query(client: TestClient) -> None:
382+
params = {"subject:identifier": TEST_PSEUDONYM_TOKEN, "source:identifier": TEST_SOURCE_ID}
383+
with capture_records() as records:
384+
response = client.get("/fhir/List", params=params)
385+
386+
assert response.status_code == 400
387+
assert_event_emitted(records, Log.REFERRAL_QUERY_FAILED, organization_identifier=TEST_URA, http_status=400)
388+
assert records.for_event(Log.LOCALIZATION_FAILED) == []
389+
390+
391+
@pytest.mark.usefixtures("rejecting_exchange")
392+
def test_logs_a_failed_bulk_delete(client: TestClient) -> None:
393+
with capture_records() as records:
394+
response = client.delete("/fhir/List", params={"subject:identifier": TEST_PSEUDONYM_TOKEN})
395+
396+
assert response.status_code == 400
397+
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
@@ -192,3 +192,46 @@ def test_delete_many_without_pseudonym_removes_referrals_of_the_source(
192192

193193
assert pseudonym_referral_service.delete_many(ura_number, source=SOURCE) == 2
194194
assert pseudonym_referral_service.query(ura_number)[0] == 1
195+
196+
197+
def test_query_logs_a_failure_when_the_pseudonym_is_rejected(
198+
rejecting_service: PseudonymReferralService, ura_number: UraNumber, caplog: pytest.LogCaptureFixture
199+
) -> None:
200+
with caplog.at_level(logging.INFO), pytest.raises(PseudonymExchangeError):
201+
rejecting_service.query(ura_number, pseudonym=PSEUDONYM)
202+
203+
(fields,) = logged_fields(caplog, Log.REFERRAL_QUERY_FAILED)
204+
assert fields["http_status"] == 400
205+
206+
207+
def test_query_without_a_pseudonym_logs_no_failure(
208+
pseudonym_referral_service: PseudonymReferralService, ura_number: UraNumber, caplog: pytest.LogCaptureFixture
209+
) -> None:
210+
with caplog.at_level(logging.INFO):
211+
pseudonym_referral_service.query(ura_number)
212+
213+
assert logged_fields(caplog, Log.REFERRAL_QUERY_FAILED) == []
214+
215+
216+
def test_delete_many_logs_a_failure_when_it_breaks(
217+
failing_service: PseudonymReferralService, ura_number: UraNumber, caplog: pytest.LogCaptureFixture
218+
) -> None:
219+
with caplog.at_level(logging.INFO), pytest.raises(RuntimeError):
220+
failing_service.delete_many(ura_number, pseudonym=PSEUDONYM, source=SOURCE)
221+
222+
(fields,) = logged_fields(caplog, Log.REFERRALS_DELETE_FAILED)
223+
assert fields["http_status"] == 500
224+
assert fields["error_reason"] == "boom"
225+
226+
227+
def test_delete_many_logs_no_failure_when_nothing_matches(
228+
pseudonym_referral_service: PseudonymReferralService,
229+
key_info_service: KeyInfoService,
230+
ura_number: UraNumber,
231+
caplog: pytest.LogCaptureFixture,
232+
) -> None:
233+
key_info_service.add_one("nvi-label", "AES_CBC")
234+
with caplog.at_level(logging.INFO):
235+
assert pseudonym_referral_service.delete_many(ura_number, pseudonym=PSEUDONYM, source=SOURCE) == 0
236+
237+
assert logged_fields(caplog, Log.REFERRALS_DELETE_FAILED) == []

0 commit comments

Comments
 (0)