-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtest_client.py
More file actions
429 lines (350 loc) · 13.6 KB
/
test_client.py
File metadata and controls
429 lines (350 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
"""
Unit tests for :mod:`gateway_api.sds_search`.
"""
from unittest.mock import Mock, patch
import pytest
from fhir.constants import FHIRSystem
from fhir.r4.resources.bundle import Bundle
from pytest_mock import MockerFixture
from stubs.sds.stub import SdsFhirApiStub
from gateway_api.common.error import SdsRequestFailedError
from gateway_api.conftest import FakeResponse, ScopedEnvVars
from gateway_api.get_structured_record import (
ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
SDS_SANDBOX_INTERACTION_ID,
)
from gateway_api.sds import SdsClient, SdsSearchResults, get
@pytest.fixture
def stub(monkeypatch: pytest.MonkeyPatch) -> SdsFhirApiStub:
stub = SdsFhirApiStub()
monkeypatch.setattr(
"gateway_api.sds.client.get",
lambda *args, **kwargs: stub.get(*args, **kwargs), # NOQA ARG005 (maintain signature)
)
monkeypatch.setattr("requests.get", stub.get)
return stub
def test_sds_client_get_org_details_success(stub: SdsFhirApiStub) -> None:
"""
Test SdsClient can successfully look up organization details.
:param stub: SDS stub fixture.
"""
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
result = client.get_org_details(ods_code="PROVIDER")
assert result is not None
assert isinstance(result, SdsSearchResults)
assert result.asid == "asid_PROV"
assert result.endpoint == "https://provider.example.com/fhir"
params = stub.get_params
assert any(
ACCESS_RECORD_STRUCTURED_INTERACTION_ID in str(ident)
for ident in params.get("identifier", [])
)
def test_sds_client_get_org_details_with_endpoint(stub: SdsFhirApiStub) -> None:
"""
Test SdsClient retrieves endpoint when available.
:param stub: SDS stub fixture.
:param mock_requests_get: Capture fixture for request details.
"""
# Add a device so we can get an endpoint
stub.upsert_device(
organization_ods="TESTORG",
service_interaction_id=ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
device={
"resourceType": "Device",
"id": "test-device-id",
"identifier": [
{
"system": FHIRSystem.NHS_SPINE_ASID,
"value": "999999999999",
},
],
"owner": {
"identifier": {
"system": FHIRSystem.ODS_CODE,
"value": "TESTORG",
}
},
},
)
stub.upsert_endpoint(
organization_ods="TESTORG",
service_interaction_id=ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
endpoint={
"resourceType": "Endpoint",
"id": "test-endpoint-id",
"status": "active",
"address": "https://testorg.example.com/fhir",
"managingOrganization": {
"identifier": {
"system": FHIRSystem.ODS_CODE,
"value": "TESTORG",
}
},
"identifier": [
{
"system": FHIRSystem.NHS_SPINE_ASID,
"value": "999999999999",
},
],
},
)
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
result = client.get_org_details(ods_code="TESTORG")
assert result is not None
assert result.asid == "999999999999"
assert result.endpoint == "https://testorg.example.com/fhir"
def test_sds_client_sends_correct_headers(stub: SdsFhirApiStub) -> None:
"""
Test that SdsClient sends X-Correlation-Id and apikey headers when provided.
:param stub: SDS stub fixture.
:param mock_requests_get: Capture fixture for request details.
"""
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
correlation_id = "test-correlation-123"
client.get_org_details(ods_code="PROVIDER", correlation_id=correlation_id)
# Check that the headers were
assert stub.get_headers["X-Correlation-Id"] == correlation_id
assert stub.get_headers["apikey"] == "example_api_key"
def test_sds_client_timeout_parameter(stub: SdsFhirApiStub) -> None:
"""
Test that SdsClient passes timeout parameter to requests.
:param stub: SDS stub fixture.
:param mock_requests_get: Capture fixture for request details.
"""
client = SdsClient(
base_url="https://test.com", api_key="example_api_key", timeout=30
)
client.get_org_details(ods_code="PROVIDER", timeout=60)
# Check that the custom timeout was passed
assert stub.get_timeout == 60
def test_sds_client_custom_service_interaction_id(stub: SdsFhirApiStub) -> None:
"""
Test that SdsClient uses custom interaction ID when provided.
:param stub: SDS stub fixture.
:param mock_requests_get: Capture fixture for request details.
"""
custom_interaction = "urn:nhs:names:services:custom:CUSTOM123"
# Add device with custom interaction ID
stub.upsert_device(
organization_ods="CUSTOMINT",
service_interaction_id=custom_interaction,
device={
"resourceType": "Device",
"id": "custom-device",
"identifier": [
{
"system": FHIRSystem.NHS_SPINE_ASID,
"value": "777777777777",
}
],
"owner": {
"identifier": {
"system": FHIRSystem.ODS_CODE,
"value": "CUSTOMINT",
}
},
},
)
client = SdsClient(
base_url="https://test.com",
service_interaction_id=custom_interaction,
api_key="example_api_key",
)
result = client.get_org_details(ods_code="CUSTOMINT", get_endpoint=False)
# Verify the custom interaction was used
params = stub.get_params
assert any(
custom_interaction in str(ident) for ident in params.get("identifier", [])
)
# Verify we got the result
assert result is not None
assert result.asid == "777777777777"
def test_sds_client_builds_correct_device_query_params(stub: SdsFhirApiStub) -> None:
"""
Test that SdsClient builds Device query parameters correctly.
:param stub: SDS stub fixture.
:param mock_requests_get: Capture fixture for request details.
"""
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
client.get_org_details(ods_code="PROVIDER")
params = stub.get_params
# Check organization parameter
assert params["organization"] == f"{FHIRSystem.ODS_CODE}|PROVIDER"
# Check identifier list contains interaction ID
identifiers = params["identifier"]
assert isinstance(identifiers, list)
assert any(
f"{FHIRSystem.NHS_SERVICE_INTERACTION_ID}|" in str(ident)
for ident in identifiers
)
def test_sds_client_uses_sandbox_interaction_id_for_sandbox_url(
stub: SdsFhirApiStub,
) -> None:
"""
Test that SdsClient uses SANDBOX_INTERACTION_ID when connecting to the
sandbox environment, not the default ACCESS_RECORD_STRUCTURED_INTERACTION_ID.
:param stub: SDS stub fixture.
"""
# Seed the stub with data keyed by the sandbox interaction ID
stub.upsert_device(
organization_ods="SANDBOX_ORG",
service_interaction_id=SDS_SANDBOX_INTERACTION_ID,
device={
"resourceType": "Device",
"id": "sandbox-device-id",
"identifier": [
{
"system": FHIRSystem.NHS_SPINE_ASID,
"value": "555555555555",
},
],
"owner": {
"identifier": {
"system": FHIRSystem.ODS_CODE,
"value": "SANDBOX_ORG",
}
},
},
)
client = SdsClient(
base_url="https://sandbox.api.service.nhs.uk/spine-directory/FHIR/R4",
api_key="example_api_key",
)
result = client.get_org_details(ods_code="SANDBOX_ORG", get_endpoint=False)
# Verify the sandbox interaction ID was sent
params = stub.get_params
assert any(
SDS_SANDBOX_INTERACTION_ID in str(ident)
for ident in params.get("identifier", [])
)
# Verify the default interaction ID was NOT used
assert not any(
ACCESS_RECORD_STRUCTURED_INTERACTION_ID in str(ident)
for ident in params.get("identifier", [])
)
assert result is not None
assert result.asid == "555555555555"
def test_sds_client_raises_sds_request_failed_error_on_http_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Test that SdsClient raises SdsRequestFailedError when SDS returns
a non-2xx response.
:param monkeypatch: Pytest monkeypatch fixture.
"""
stub = SdsFhirApiStub()
def get_without_apikey(
url: str,
headers: dict[str, str],
params: dict[str, str],
timeout: int = 10,
) -> object:
# Strip the apikey header so the stub returns a 400
headers_without_key = {k: v for k, v in headers.items() if k != "apikey"}
return stub.get(
url=url, headers=headers_without_key, params=params, timeout=timeout
)
monkeypatch.setattr("gateway_api.sds.client.get", get_without_apikey)
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
with pytest.raises(SdsRequestFailedError, match="SDS FHIR API request failed"):
client.get_org_details(ods_code="PROVIDER")
def test_sds_client_endpoint_entry_without_address_returns_none(
stub: SdsFhirApiStub,
) -> None:
"""
Test that get_org_details returns endpoint=None when the Endpoint resource
has no address field.
:param stub: SDS stub fixture.
"""
stub.upsert_device(
organization_ods="NOADDR",
service_interaction_id=ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
device={
"resourceType": "Device",
"id": "noaddr-device",
"identifier": [
{"system": FHIRSystem.NHS_SPINE_ASID, "value": "111111111111"},
],
},
)
stub.upsert_endpoint(
organization_ods="NOADDR",
service_interaction_id=ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
endpoint={
"resourceType": "Endpoint",
"id": "noaddr-endpoint",
"status": "active",
# no "address" field
"identifier": [
{"system": FHIRSystem.NHS_SPINE_ASID, "value": "111111111111"}
],
},
)
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
result = client.get_org_details(ods_code="NOADDR")
assert result.asid == "111111111111"
assert result.endpoint is None
@pytest.mark.usefixtures("stub")
def test_sds_client_empty_device_bundle_returns_none_asid() -> None:
"""
Test that get_org_details returns asid=None when the Device bundle has no
entries for the given ODS code, exercising the empty-entries branch in
_extract_first_entry.
:param stub: SDS stub fixture.
"""
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
# "UNKNOWN_ORG" has no seeded devices, so the bundle entry list will be empty
result = client.get_org_details(ods_code="UNKNOWN_ORG", get_endpoint=False)
assert result.asid is None
def test_sds_client_no_endpoint_bundle_entries_returns_none_endpoint(
stub: SdsFhirApiStub,
) -> None:
"""
Test that get_org_details returns endpoint=None when the Endpoint bundle has
no entries, exercising the else branch after _extract_first_entry returns {}.
:param stub: SDS stub fixture.
"""
stub.upsert_device(
organization_ods="NOENDPOINT",
service_interaction_id=ACCESS_RECORD_STRUCTURED_INTERACTION_ID,
device={
"resourceType": "Device",
"id": "noendpoint-device",
"identifier": [
{"system": FHIRSystem.NHS_SPINE_ASID, "value": "222222222222"},
],
},
)
# Deliberately do not seed any endpoint for NOENDPOINT
client = SdsClient(base_url="https://test.com", api_key="example_api_key")
result = client.get_org_details(ods_code="NOENDPOINT")
assert result.asid == "222222222222"
assert result.endpoint is None
def test_sds_client_respects_url(mocker: MockerFixture) -> None:
empty_bundle = Bundle.empty("searchset").model_dump()
mocked_get = mocker.patch(
"gateway_api.sds.client.get",
return_value=FakeResponse(status_code=200, headers={}, _json=empty_bundle),
)
client = SdsClient(
base_url="https://a.different.url/base", api_key="example_api_key"
)
_ = client.get_org_details(ods_code="A12345", get_endpoint=False)
actual_url = mocked_get.call_args.args[0]
actual_headers = mocked_get.call_args.kwargs["headers"]
assert actual_url == "https://a.different.url/base/Device"
assert actual_headers["apikey"] == "example_api_key"
@patch("gateway_api.sds.client.SdsFhirApiStub")
@patch("gateway_api.sds.client.external_sds_get")
def test_get_with_stub(mock_external_get: Mock, mock_stub: Mock) -> None:
with ScopedEnvVars({"SDS_URL": "stub"}):
get("https://example.com/", headers={}, params={}, timeout=10)
assert mock_stub.return_value.get.called
assert not mock_external_get.called
@patch("gateway_api.sds.client.SdsFhirApiStub")
@patch("gateway_api.sds.client.external_sds_get")
def test_get_without_stub(mock_external_get: Mock, mock_stub: Mock) -> None:
with ScopedEnvVars({"SDS_URL": "https://www.example.com/"}):
get("https://example.com/", headers={}, params={}, timeout=10)
assert mock_external_get.called
assert not mock_stub.return_value.get.called