Skip to content

Commit 5065d73

Browse files
committed
Add tests
1 parent 43b20d2 commit 5065d73

5 files changed

Lines changed: 346 additions & 80 deletions

File tree

gateway-api/src/gateway_api/test_controller.py

Lines changed: 175 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Unit tests for :mod:`gateway_api.controller`."""
22

3+
import json
4+
from copy import deepcopy
35
from typing import Any
46

57
import pytest
@@ -287,67 +289,207 @@ def mock_happy_path_get_structured_record_request(
287289
return happy_path_request
288290

289291

290-
def test_controller_creates_jwt_token_with_correct_claims(
292+
# TODO: Look at this more carefully. Does it do things done differently above?
293+
def _setup_pds_sds_mocks(
291294
mocker: MockerFixture,
292-
valid_simple_request_payload: dict[str, Any],
293-
valid_simple_response_payload: dict[str, Any],
295+
nhs_number: str,
296+
provider_ods: str,
297+
provider_endpoint: str,
294298
) -> None:
295-
"""
296-
Test that the controller creates a JWT token with the correct claims.
297-
"""
298-
nhs_number = "9000000009"
299-
provider_ods = "PROVIDER"
300-
consumer_ods = "CONSUMER"
301-
provider_endpoint = "https://provider.example/ep"
302-
303-
# Mock PDS to return provider ODS code
304299
mocker.patch(
305300
"gateway_api.pds.PdsClient.search_patient_by_nhs_number",
306301
return_value=_create_patient(nhs_number, provider_ods),
307302
)
308-
309-
# Mock SDS to return provider and consumer details
310-
provider_sds_results = SdsSearchResults(
311-
asid="asid_PROV", endpoint=provider_endpoint
312-
)
313-
consumer_sds_results = SdsSearchResults(asid="asid_CONS", endpoint=None)
314303
mocker.patch(
315304
"gateway_api.sds.SdsClient.get_org_details",
316-
side_effect=[provider_sds_results, consumer_sds_results],
305+
side_effect=[
306+
SdsSearchResults(asid="asid_PROV", endpoint=provider_endpoint),
307+
SdsSearchResults(asid="asid_CONS", endpoint=None),
308+
],
317309
)
318310

319-
# Mock GpProviderClient to capture initialization arguments
320-
mock_gp_provider = mocker.patch("gateway_api.controller.GpProviderClient")
321311

322-
# Mock the access_structured_record method to return a response
323-
provider_response = FakeResponse(
312+
def _setup_provider_mock(
313+
mocker: MockerFixture,
314+
valid_simple_response_payload: dict[str, Any],
315+
) -> Any:
316+
mock_gp_provider = mocker.patch("gateway_api.controller.GpProviderClient")
317+
mock_gp_provider.return_value.access_structured_record.return_value = FakeResponse(
324318
status_code=200,
325319
headers={"Content-Type": "application/fhir+json"},
326320
_json=valid_simple_response_payload,
327321
)
328-
mock_gp_provider.return_value.access_structured_record.return_value = (
329-
provider_response
330-
)
322+
return mock_gp_provider
323+
324+
325+
def test_controller_creates_jwt_token_with_correct_claims(
326+
mocker: MockerFixture,
327+
valid_simple_request_payload: dict[str, Any],
328+
valid_simple_response_payload: dict[str, Any],
329+
) -> None:
330+
"""
331+
Test that the controller creates a JWT token with the correct claims,
332+
taking issuer, requestingDevice, and requestingPractitioner from the
333+
identity section of the request body.
334+
"""
335+
nhs_number = "9000000009"
336+
provider_ods = "PROVIDER"
337+
consumer_ods = "CONSUMER"
338+
provider_endpoint = "https://provider.example/ep"
339+
340+
_setup_pds_sds_mocks(mocker, nhs_number, provider_ods, provider_endpoint)
341+
mock_gp_provider = _setup_provider_mock(mocker, valid_simple_response_payload)
331342

332-
# Create request and run controller
333343
request = create_mock_request(
334344
headers={"ODS-From": consumer_ods, "Ssp-TraceID": "test-trace-id"},
335345
body=valid_simple_request_payload,
336346
)
337347

338-
get_structured_record_request = GetStructuredRecordRequest(request)
339-
340348
controller = Controller()
341-
controller.run(get_structured_record_request)
349+
controller.run(GetStructuredRecordRequest(request))
342350

343-
# Verify that GpProviderClient was called and extract the JWT token
344351
mock_gp_provider.assert_called_once()
345352
jwt_token = mock_gp_provider.call_args.kwargs["token"]
346353

347-
# Verify the standard JWT claims
354+
# Issuer, subject and audience come from the identity section and provider endpoint
348355
assert jwt_token.issuer == "https://clinical-data-gateway-api.sandbox.nhs.uk"
349356
assert jwt_token.subject == "10019"
350357
assert jwt_token.audience == provider_endpoint
351358

352-
# Verify the requesting organization matches the consumer ODS
359+
# Requesting organisation is built from the consumer ODS code
353360
assert jwt_token.requesting_organization["identifier"][0]["value"] == consumer_ods
361+
362+
363+
def test_controller_strips_identity_from_forwarded_body(
364+
mocker: MockerFixture,
365+
valid_simple_request_payload: dict[str, Any],
366+
valid_simple_response_payload: dict[str, Any],
367+
) -> None:
368+
"""
369+
Test that the identity parameter is removed from the body sent to the provider.
370+
"""
371+
nhs_number = "9000000009"
372+
provider_ods = "PROVIDER"
373+
consumer_ods = "CONSUMER"
374+
provider_endpoint = "https://provider.example/ep"
375+
376+
_setup_pds_sds_mocks(mocker, nhs_number, provider_ods, provider_endpoint)
377+
mock_gp_provider = _setup_provider_mock(mocker, valid_simple_response_payload)
378+
379+
request = create_mock_request(
380+
headers={"ODS-From": consumer_ods, "Ssp-TraceID": "test-trace-id"},
381+
body=valid_simple_request_payload,
382+
)
383+
384+
controller = Controller()
385+
controller.run(GetStructuredRecordRequest(request))
386+
387+
mock_gp_provider.return_value.access_structured_record.assert_called_once()
388+
forwarded_body = json.loads(
389+
mock_gp_provider.return_value.access_structured_record.call_args.kwargs["body"]
390+
)
391+
392+
parameter_names = [p.get("name") for p in forwarded_body.get("parameter", [])]
393+
assert "identity" not in parameter_names
394+
# The patientNHSNumber parameter has no "name" key after Pydantic serialisation;
395+
# verify that exactly one parameter remains and it carries the NHS number value.
396+
remaining = forwarded_body.get("parameter", [])
397+
assert len(remaining) == 1
398+
assert remaining[0].get("valueIdentifier", {}).get("value") == "9999999999"
399+
400+
401+
def _make_request_without_identity_field(
402+
consumer_ods: str,
403+
base_payload: dict[str, Any],
404+
field_to_remove: str,
405+
) -> Request:
406+
"""Return a mock request with the named field removed from the identity part."""
407+
payload = deepcopy(base_payload)
408+
identity = next(p for p in payload["parameter"] if p["name"] == "identity")
409+
identity["part"] = [p for p in identity["part"] if p["name"] != field_to_remove]
410+
return create_mock_request(
411+
headers={"ODS-From": consumer_ods, "Ssp-TraceID": "test-trace-id"},
412+
body=payload,
413+
)
414+
415+
416+
def _make_request_without_identity(
417+
consumer_ods: str,
418+
base_payload: dict[str, Any],
419+
) -> Request:
420+
"""Return a mock request with the entire identity parameter removed."""
421+
payload = deepcopy(base_payload)
422+
payload["parameter"] = [
423+
p for p in payload["parameter"] if p.get("name") != "identity"
424+
]
425+
return create_mock_request(
426+
headers={"ODS-From": consumer_ods, "Ssp-TraceID": "test-trace-id"},
427+
body=payload,
428+
)
429+
430+
431+
@pytest.mark.parametrize(
432+
("missing_field", "expected_message"),
433+
[
434+
("issuer", "Missing 'issuer' in identity in request body"),
435+
(
436+
"requestingOrgName",
437+
"Missing 'requestingOrgName' in identity in request body",
438+
),
439+
("requestingDevice", "Missing 'requestingDevice' in identity in request body"),
440+
(
441+
"requestingPractitioner",
442+
"Missing 'requestingPractitioner' in identity in request body",
443+
),
444+
],
445+
)
446+
def test_controller_run_raises_value_error_when_identity_field_missing(
447+
mocker: MockerFixture,
448+
valid_simple_request_payload: dict[str, Any],
449+
valid_simple_response_payload: dict[str, Any],
450+
missing_field: str,
451+
expected_message: str,
452+
) -> None:
453+
"""
454+
Test that a ValueError is raised when a required identity field is absent.
455+
"""
456+
nhs_number = "9000000009"
457+
provider_ods = "PROVIDER"
458+
consumer_ods = "CONSUMER"
459+
provider_endpoint = "https://provider.example/ep"
460+
461+
_setup_pds_sds_mocks(mocker, nhs_number, provider_ods, provider_endpoint)
462+
_setup_provider_mock(mocker, valid_simple_response_payload)
463+
464+
request = _make_request_without_identity_field(
465+
consumer_ods, valid_simple_request_payload, missing_field
466+
)
467+
468+
controller = Controller()
469+
with pytest.raises(ValueError, match=expected_message):
470+
controller.run(GetStructuredRecordRequest(request))
471+
472+
473+
def test_controller_run_raises_value_error_when_identity_parameter_absent(
474+
mocker: MockerFixture,
475+
valid_simple_request_payload: dict[str, Any],
476+
valid_simple_response_payload: dict[str, Any],
477+
) -> None:
478+
"""
479+
Test that a ValueError is raised when the identity parameter is entirely absent.
480+
"""
481+
nhs_number = "9000000009"
482+
provider_ods = "PROVIDER"
483+
consumer_ods = "CONSUMER"
484+
provider_endpoint = "https://provider.example/ep"
485+
486+
_setup_pds_sds_mocks(mocker, nhs_number, provider_ods, provider_endpoint)
487+
_setup_provider_mock(mocker, valid_simple_response_payload)
488+
489+
request = _make_request_without_identity(consumer_ods, valid_simple_request_payload)
490+
491+
controller = Controller()
492+
with pytest.raises(
493+
ValueError, match="Missing 'issuer' in identity in request body"
494+
):
495+
controller.run(GetStructuredRecordRequest(request))

gateway-api/tests/conftest.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,56 @@
2121
}
2222

2323

24+
_IDENTITY_PARAMETER: dict[str, Any] = {
25+
"name": "identity",
26+
"part": [
27+
{
28+
"name": "issuer",
29+
"valueString": "https://clinical-data-gateway-api.sandbox.nhs.uk",
30+
},
31+
{
32+
"name": "requestingOrgName",
33+
"valueString": "Example Consumer Organization",
34+
},
35+
{
36+
"name": "requestingDevice",
37+
"resource": {
38+
"resourceType": "Device",
39+
"identifier": [
40+
{
41+
"system": "https://orange.testlab.nhs.uk/gpconnect-demonstrator/Id/local-system-instance-id",
42+
"value": "gpcdemonstrator-1-orange",
43+
}
44+
],
45+
"model": "GP Connect Demonstrator",
46+
"version": "1.5.0",
47+
},
48+
},
49+
{
50+
"name": "requestingPractitioner",
51+
"resource": {
52+
"resourceType": "Practitioner",
53+
"id": "10019",
54+
"name": [{"family": "Doe", "given": ["John"], "prefix": ["Mr"]}],
55+
"identifier": [
56+
{
57+
"system": "https://fhir.nhs.uk/Id/sds-user-id",
58+
"value": "111222333444",
59+
},
60+
{
61+
"system": "https://fhir.nhs.uk/Id/sds-role-profile-id",
62+
"value": "444555666777",
63+
},
64+
{
65+
"system": "https://orange.testlab.nhs.uk/gpconnect-demonstrator/Id/local-user-id",
66+
"value": "98ed4f78-814d-4266-8d5b-cde742f3093c",
67+
},
68+
],
69+
},
70+
},
71+
],
72+
}
73+
2474
SIMPLE_PAYLOAD = {
2575
"resourceType": "Parameters",
2676
"parameter": [
@@ -31,6 +81,7 @@
3181
"value": "9999999999",
3282
},
3383
},
84+
_IDENTITY_PARAMETER,
3485
],
3586
}
3687

gateway-api/tests/contract/pacts/GatewayAPIConsumer-GatewayAPIProvider.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,65 @@
2828
"system": "https://fhir.nhs.uk/Id/nhs-number",
2929
"value": "9999999999"
3030
}
31+
},
32+
{
33+
"name": "identity",
34+
"part": [
35+
{
36+
"name": "issuer",
37+
"valueString": "https://clinical-data-gateway-api.sandbox.nhs.uk"
38+
},
39+
{
40+
"name": "requestingOrgName",
41+
"valueString": "Example Consumer Organization"
42+
},
43+
{
44+
"name": "requestingDevice",
45+
"resource": {
46+
"identifier": [
47+
{
48+
"system": "https://orange.testlab.nhs.uk/gpconnect-demonstrator/Id/local-system-instance-id",
49+
"value": "gpcdemonstrator-1-orange"
50+
}
51+
],
52+
"model": "GP Connect Demonstrator",
53+
"resourceType": "Device",
54+
"version": "1.5.0"
55+
}
56+
},
57+
{
58+
"name": "requestingPractitioner",
59+
"resource": {
60+
"id": "10019",
61+
"identifier": [
62+
{
63+
"system": "https://fhir.nhs.uk/Id/sds-user-id",
64+
"value": "111222333444"
65+
},
66+
{
67+
"system": "https://fhir.nhs.uk/Id/sds-role-profile-id",
68+
"value": "444555666777"
69+
},
70+
{
71+
"system": "https://orange.testlab.nhs.uk/gpconnect-demonstrator/Id/local-user-id",
72+
"value": "98ed4f78-814d-4266-8d5b-cde742f3093c"
73+
}
74+
],
75+
"name": [
76+
{
77+
"family": "Doe",
78+
"given": [
79+
"John"
80+
],
81+
"prefix": [
82+
"Mr"
83+
]
84+
}
85+
],
86+
"resourceType": "Practitioner"
87+
}
88+
}
89+
]
3190
}
3291
],
3392
"resourceType": "Parameters"

0 commit comments

Comments
 (0)