From 27df4a5bcecfc0bb87bcb91e639014b3f3caaf8e Mon Sep 17 00:00:00 2001 From: Megan Date: Fri, 13 Mar 2026 17:08:23 +0000 Subject: [PATCH 1/3] NDR-404 Add fhir handler get unit tests --- ...t_fhir_document_reference_by_id_handler.py | 137 +++++++++++++++++- 1 file changed, 129 insertions(+), 8 deletions(-) diff --git a/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py b/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py index 147e7b089..8167aab8f 100644 --- a/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py +++ b/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py @@ -1,5 +1,9 @@ +import json +from copy import deepcopy + import pytest +from enums.lambda_error import LambdaError from enums.mtls import MtlsCommonNames from enums.snomed_codes import SnomedCodes from handlers.get_fhir_document_reference_handler import ( @@ -9,6 +13,7 @@ from models.document_reference import DocumentReference from tests.unit.conftest import TEST_UUID from tests.unit.helpers.data.dynamo.dynamo_responses import MOCK_SEARCH_RESPONSE +from utils.lambda_exceptions import GetFhirDocumentReferenceException from utils.lambda_handler_utils import extract_bearer_token SNOMED_CODE = SnomedCodes.PATIENT_DATA.value.code @@ -27,7 +32,7 @@ "userAgent": "curl/7.64.1", "clientCert": { "clientCertPem": "-----BEGIN CERTIFICATE-----...", - "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", + "subjectDN": "CN=ndrclient.main.dev.pdm.national.nhs.uk,O=NHS,C=UK", "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", "serialNumber": "12:34:56", "validity": { @@ -49,8 +54,7 @@ def mock_config_service(mocker): mock_config = mocker.patch( "handlers.get_fhir_document_reference_handler.DynamicConfigurationService", ) - mock_config_instance = mock_config.return_value - return mock_config_instance + return mock_config.return_value @pytest.fixture @@ -58,11 +62,11 @@ def mock_document_service(mocker): mock_service = mocker.patch( "handlers.get_fhir_document_reference_handler.GetFhirDocumentReferenceService", ) - mock_service_instance = mock_service.return_value - mock_service_instance.handle_get_document_reference_request.return_value = ( + instance = mock_service.return_value + instance.handle_get_document_reference_request.return_value = ( MOCK_DOCUMENT_REFERENCE ) - return mock_service_instance + return instance @pytest.fixture @@ -70,10 +74,42 @@ def mock_mtls_common_names(monkeypatch): monkeypatch.setattr( MtlsCommonNames, "_get_mtls_common_names", - classmethod(lambda cls: {"PDM": ["ndrclient.main.int.pdm.national.nhs.uk"]}), + classmethod(lambda cls: {"PDM": ["ndrclient.main.dev.pdm.national.nhs.uk"]}), + ) + + +@pytest.fixture +def mock_mtls_disallow_all(monkeypatch): + monkeypatch.setattr( + MtlsCommonNames, + "_get_mtls_common_names", + classmethod(lambda cls: {"PDM": []}), ) +@pytest.fixture +def unauthorized_cn_event(): + ev = deepcopy(MOCK_MTLS_VALID_EVENT) + ev["requestContext"]["identity"]["clientCert"][ + "subjectDN" + ] = "CN=unauthorised.client.nhs.uk,O=NHS,C=UK" + return ev + + +@pytest.fixture +def event_missing_client_cert(): + ev = deepcopy(MOCK_MTLS_VALID_EVENT) + ev["requestContext"]["identity"].pop("clientCert", None) + return ev + + +@pytest.fixture +def event_malformed_subject_dn(): + ev = deepcopy(MOCK_MTLS_VALID_EVENT) + ev["requestContext"]["identity"]["clientCert"]["subjectDN"] = "O=NHS,C=UK" + return ev + + def test_lambda_handler_happy_path_with_mtls_pdm_login( set_env, mock_mtls_common_names, @@ -88,7 +124,7 @@ def test_lambda_handler_happy_path_with_mtls_pdm_login( assert response["statusCode"] == 200 assert response["body"] == "test_document_reference" - # Verify correct method calls + mock_document_service.handle_get_document_reference_request.assert_called_once_with( SNOMED_CODE, TEST_UUID, @@ -107,3 +143,88 @@ def test_extract_document_parameters_valid_pdm(): document_id, snomed_code = extract_document_parameters(MOCK_MTLS_VALID_EVENT) assert snomed_code is None assert document_id == TEST_UUID + + +def test_lambda_handler_mtls_unauthorised_cn_returns_400( + set_env, + mock_mtls_disallow_all, + mock_document_service, + unauthorized_cn_event, + context, +): + resp = lambda_handler(unauthorized_cn_event, context) + assert resp["statusCode"] == 400 + mock_document_service.handle_get_document_reference_request.assert_not_called() + + +def test_lambda_handler_mtls_missing_client_cert_returns_401( + set_env, + mock_mtls_disallow_all, + mock_document_service, + event_missing_client_cert, + context, +): + resp = lambda_handler(event_missing_client_cert, context) + assert resp["statusCode"] == 401 + mock_document_service.handle_get_document_reference_request.assert_not_called() + + +def test_lambda_handler_mtls_malformed_subject_dn_returns_401( + set_env, + mock_mtls_disallow_all, + mock_document_service, + event_malformed_subject_dn, + context, +): + resp = lambda_handler(event_malformed_subject_dn, context) + assert resp["statusCode"] == 401 + mock_document_service.handle_get_document_reference_request.assert_not_called() + + +def test_lambda_handler_mtls_invalid_path_parameters_returns_400( + set_env, + mock_mtls_common_names, + mock_document_service, + context, +): + ev = deepcopy(MOCK_MTLS_VALID_EVENT) + ev["pathParameters"] = {"id": "invalid_format_no_tilde"} + resp = lambda_handler(ev, context) + assert resp["statusCode"] == 400 + mock_document_service.handle_get_document_reference_request.assert_not_called() + + +@pytest.mark.parametrize( + "status, lambda_error", + [ + (404, LambdaError.DocumentReferenceNotFound), + (403, LambdaError.DocumentReferenceForbidden), + (400, LambdaError.DocumentReferenceMissingParameters), + (500, LambdaError.DocumentReferenceGeneralError), + ], +) +def test_lambda_handler_mtls_service_errors( + set_env, + mock_mtls_common_names, + mock_document_service, + context, + status, + lambda_error, +): + mock_document_service.handle_get_document_reference_request.side_effect = ( + GetFhirDocumentReferenceException(status, lambda_error) + ) + + resp = lambda_handler(MOCK_MTLS_VALID_EVENT, context) + assert resp["statusCode"] == status + + body = json.loads(resp["body"]) + assert body["resourceType"] == "OperationOutcome" + assert ( + body["issue"][0]["details"]["coding"][0]["code"] + == lambda_error.value.get("fhir_coding").code + ) + assert ( + body["issue"][0]["details"]["coding"][0]["display"] + == lambda_error.value.get("fhir_coding").display + ) From 9f5080839f25a3ea5edddfde8f0ce7680eea966a Mon Sep 17 00:00:00 2001 From: Megan Date: Tue, 24 Mar 2026 12:50:36 +0000 Subject: [PATCH 2/3] NDR-404 Add get service unit tests --- ...t_fhir_document_reference_by_id_service.py | 155 ++++++++++++++++-- 1 file changed, 141 insertions(+), 14 deletions(-) diff --git a/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py index 04d4cfe17..1c0b2a1e9 100644 --- a/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py +++ b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py @@ -36,13 +36,11 @@ def test_get_document_reference_service(patched_service): def test_handle_get_document_reference_request(patched_service, mocker, set_env): documents = create_test_doc_store_refs() - expected = documents[0] - mock_document_ref = documents[0] mocker.patch.object( patched_service, "get_core_document_references", - return_value=mock_document_ref, + return_value=expected, ) actual = patched_service.handle_get_document_reference_request( @@ -54,18 +52,13 @@ def test_handle_get_document_reference_request(patched_service, mocker, set_env) def test_get_dynamo_table_for_patient_data_doc_type(patched_service): - """Test _get_dynamo_table_for_doc_type method with a non-Lloyd George document type.""" - patient_data_code = SnomedCodes.PATIENT_DATA.value - result = patched_service._get_dynamo_table_for_doc_type(patient_data_code) assert result == str(DynamoTables.CORE) -# Not PDM however the code that this relates to was introduced because of PDM +# The following two tests are not PDM however the code that this relates to was introduced because of PDM def test_get_dynamo_table_for_unsupported_doc_type(patched_service): - """Test _get_dynamo_table_for_doc_type method with a non-Lloyd George document type.""" - non_lg_code = SnomedCode(code="non-lg-code", display_name="Non Lloyd George") with pytest.raises(InvalidDocTypeException) as exc_info: @@ -75,19 +68,15 @@ def test_get_dynamo_table_for_unsupported_doc_type(patched_service): assert exc_info.value.error == LambdaError.DocTypeDB -# Not PDM however the code that this relates to was introduced because of PDM def test_get_dynamo_table_for_lloyd_george_doc_type(patched_service): - """Test _get_dynamo_table_for_doc_type method with Lloyd George document type.""" lg_code = SnomedCodes.LLOYD_GEORGE.value - result = patched_service._get_dynamo_table_for_doc_type(lg_code) - assert result == str(DynamoTables.LLOYD_GEORGE) def test_get_document_references_empty_result(patched_service): - # Test when no documents are found patched_service.document_service.get_item.return_value = None + patched_service.document_service.fetch_documents_from_table.return_value = [] with pytest.raises(GetFhirDocumentReferenceException) as exc_info: patched_service.get_core_document_references( @@ -96,3 +85,141 @@ def test_get_document_references_empty_result(patched_service): ) assert exc_info.value.status_code == 404 assert exc_info.value.error == LambdaError.DocumentReferenceNotFound + + +def test_get_core_document_references_raises_on_doc_service_error(patched_service): + patched_service.document_service.fetch_documents_from_table.side_effect = Exception( + "Dynamo error", + ) + + with pytest.raises(Exception) as exc: + patched_service.get_core_document_references( + document_id="test-id", + snomed_code=SnomedCodes.PATIENT_DATA.value.code, + table=MOCK_PDM_TABLE_NAME, + ) + + assert "Dynamo error" in str(exc.value) + + +def test_handle_request_propagates_core_exception(patched_service, mocker): + mocker.patch.object( + patched_service, + "get_core_document_references", + side_effect=GetFhirDocumentReferenceException( + error=LambdaError.DocumentReferenceNotFound, + status_code=404, + ), + ) + + with pytest.raises(GetFhirDocumentReferenceException): + patched_service.handle_get_document_reference_request( + SnomedCodes.PATIENT_DATA.value.code, + "test-id", + ) + + +def test_get_core_document_references_returns_first_item_only(patched_service): + docs = create_test_doc_store_refs() + extra = create_test_doc_store_refs()[0] + docs.append(extra) + + patched_service.document_service.fetch_documents_from_table.return_value = docs + + result = patched_service.get_core_document_references( + document_id="abc", + snomed_code=SnomedCodes.PATIENT_DATA.value.code, + table=MOCK_PDM_TABLE_NAME, + ) + assert result == docs[0] + + +def test_get_core_document_references_calls_document_service_with_correct_params( + patched_service, +): + docs = create_test_doc_store_refs() + patched_service.document_service.fetch_documents_from_table.return_value = docs + + snomed = SnomedCodes.PATIENT_DATA.value.code + + patched_service.get_core_document_references( + document_id="ZZZ", + snomed_code=snomed, + table=MOCK_PDM_TABLE_NAME, + ) + + patched_service.document_service.fetch_documents_from_table.assert_called_once() + + # Extract actual call arguments + actual_kwargs = ( + patched_service.document_service.fetch_documents_from_table.call_args.kwargs + ) + assert actual_kwargs["table_name"] == MOCK_PDM_TABLE_NAME + assert actual_kwargs["index_name"] == "idx_gsi_snomed_code" + assert actual_kwargs["search_key"] == ["DocumentSnomedCodeType", "ID"] + assert actual_kwargs["search_condition"] == [snomed, "ZZZ"] + assert "query_filter" in actual_kwargs # Can't fully match Dynamo condition object + + +def test_get_core_document_references_rejects_empty_document_id(patched_service): + with pytest.raises(Exception): + patched_service.get_core_document_references( + document_id="", + snomed_code=SnomedCodes.PATIENT_DATA.value.code, + table=MOCK_PDM_TABLE_NAME, + ) + + +def test_get_core_document_references_wrong_table_provided(patched_service): + docs = create_test_doc_store_refs() + patched_service.document_service.fetch_documents_from_table.return_value = docs + + snomed = SnomedCodes.PATIENT_DATA.value.code + wrong_table = "IncorrectTable" + + result = patched_service.get_core_document_references( + document_id="123", + snomed_code=snomed, + table=wrong_table, + ) + assert result == docs[0] + + actual_kwargs = ( + patched_service.document_service.fetch_documents_from_table.call_args.kwargs + ) + assert actual_kwargs["table_name"] == wrong_table + assert actual_kwargs["search_condition"] == [snomed, "123"] + + +def test_get_core_document_references_invalid_snomed_value(patched_service): + bad_snomed = "INVALID_CODE" # simulate bad enum + + with pytest.raises(GetFhirDocumentReferenceException): + patched_service.get_core_document_references( + document_id="123", + snomed_code=bad_snomed, + table=MOCK_PDM_TABLE_NAME, + ) + + +def test_get_core_document_references_snomed_case_sensitivity(patched_service): + wrong_case = SnomedCodes.PATIENT_DATA.value.code.lower() + + if wrong_case != SnomedCodes.PATIENT_DATA.value.code: + with pytest.raises(InvalidDocTypeException): + patched_service.get_core_document_references( + document_id="123", + snomed_code=wrong_case, + table=MOCK_PDM_TABLE_NAME, + ) + + +def test_get_core_document_references_document_service_returns_none(patched_service): + patched_service.document_service.fetch_documents_from_table.return_value = None + + with pytest.raises(TypeError): + patched_service.get_core_document_references( + document_id="xyz", + snomed_code=SnomedCodes.PATIENT_DATA.value.code, + table=MOCK_PDM_TABLE_NAME, + ) From 39a1968a55b81876df92da899b2d1759a75c0eb8 Mon Sep 17 00:00:00 2001 From: Megan Date: Tue, 24 Mar 2026 13:10:51 +0000 Subject: [PATCH 3/3] NDR-404 Rebase conflicts --- ...t_fhir_document_reference_by_id_handler.py | 6 +- ...t_fhir_document_reference_by_id_service.py | 139 ------------------ 2 files changed, 3 insertions(+), 142 deletions(-) diff --git a/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py b/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py index 8167aab8f..37b6080ab 100644 --- a/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py +++ b/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py @@ -62,11 +62,11 @@ def mock_document_service(mocker): mock_service = mocker.patch( "handlers.get_fhir_document_reference_handler.GetFhirDocumentReferenceService", ) - instance = mock_service.return_value - instance.handle_get_document_reference_request.return_value = ( + mock_service_instance = mock_service.return_value + mock_service_instance.handle_get_document_reference_request.return_value = ( MOCK_DOCUMENT_REFERENCE ) - return instance + return mock_service_instance @pytest.fixture diff --git a/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py index 1c0b2a1e9..eeef2f8ca 100644 --- a/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py +++ b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py @@ -76,7 +76,6 @@ def test_get_dynamo_table_for_lloyd_george_doc_type(patched_service): def test_get_document_references_empty_result(patched_service): patched_service.document_service.get_item.return_value = None - patched_service.document_service.fetch_documents_from_table.return_value = [] with pytest.raises(GetFhirDocumentReferenceException) as exc_info: patched_service.get_core_document_references( @@ -85,141 +84,3 @@ def test_get_document_references_empty_result(patched_service): ) assert exc_info.value.status_code == 404 assert exc_info.value.error == LambdaError.DocumentReferenceNotFound - - -def test_get_core_document_references_raises_on_doc_service_error(patched_service): - patched_service.document_service.fetch_documents_from_table.side_effect = Exception( - "Dynamo error", - ) - - with pytest.raises(Exception) as exc: - patched_service.get_core_document_references( - document_id="test-id", - snomed_code=SnomedCodes.PATIENT_DATA.value.code, - table=MOCK_PDM_TABLE_NAME, - ) - - assert "Dynamo error" in str(exc.value) - - -def test_handle_request_propagates_core_exception(patched_service, mocker): - mocker.patch.object( - patched_service, - "get_core_document_references", - side_effect=GetFhirDocumentReferenceException( - error=LambdaError.DocumentReferenceNotFound, - status_code=404, - ), - ) - - with pytest.raises(GetFhirDocumentReferenceException): - patched_service.handle_get_document_reference_request( - SnomedCodes.PATIENT_DATA.value.code, - "test-id", - ) - - -def test_get_core_document_references_returns_first_item_only(patched_service): - docs = create_test_doc_store_refs() - extra = create_test_doc_store_refs()[0] - docs.append(extra) - - patched_service.document_service.fetch_documents_from_table.return_value = docs - - result = patched_service.get_core_document_references( - document_id="abc", - snomed_code=SnomedCodes.PATIENT_DATA.value.code, - table=MOCK_PDM_TABLE_NAME, - ) - assert result == docs[0] - - -def test_get_core_document_references_calls_document_service_with_correct_params( - patched_service, -): - docs = create_test_doc_store_refs() - patched_service.document_service.fetch_documents_from_table.return_value = docs - - snomed = SnomedCodes.PATIENT_DATA.value.code - - patched_service.get_core_document_references( - document_id="ZZZ", - snomed_code=snomed, - table=MOCK_PDM_TABLE_NAME, - ) - - patched_service.document_service.fetch_documents_from_table.assert_called_once() - - # Extract actual call arguments - actual_kwargs = ( - patched_service.document_service.fetch_documents_from_table.call_args.kwargs - ) - assert actual_kwargs["table_name"] == MOCK_PDM_TABLE_NAME - assert actual_kwargs["index_name"] == "idx_gsi_snomed_code" - assert actual_kwargs["search_key"] == ["DocumentSnomedCodeType", "ID"] - assert actual_kwargs["search_condition"] == [snomed, "ZZZ"] - assert "query_filter" in actual_kwargs # Can't fully match Dynamo condition object - - -def test_get_core_document_references_rejects_empty_document_id(patched_service): - with pytest.raises(Exception): - patched_service.get_core_document_references( - document_id="", - snomed_code=SnomedCodes.PATIENT_DATA.value.code, - table=MOCK_PDM_TABLE_NAME, - ) - - -def test_get_core_document_references_wrong_table_provided(patched_service): - docs = create_test_doc_store_refs() - patched_service.document_service.fetch_documents_from_table.return_value = docs - - snomed = SnomedCodes.PATIENT_DATA.value.code - wrong_table = "IncorrectTable" - - result = patched_service.get_core_document_references( - document_id="123", - snomed_code=snomed, - table=wrong_table, - ) - assert result == docs[0] - - actual_kwargs = ( - patched_service.document_service.fetch_documents_from_table.call_args.kwargs - ) - assert actual_kwargs["table_name"] == wrong_table - assert actual_kwargs["search_condition"] == [snomed, "123"] - - -def test_get_core_document_references_invalid_snomed_value(patched_service): - bad_snomed = "INVALID_CODE" # simulate bad enum - - with pytest.raises(GetFhirDocumentReferenceException): - patched_service.get_core_document_references( - document_id="123", - snomed_code=bad_snomed, - table=MOCK_PDM_TABLE_NAME, - ) - - -def test_get_core_document_references_snomed_case_sensitivity(patched_service): - wrong_case = SnomedCodes.PATIENT_DATA.value.code.lower() - - if wrong_case != SnomedCodes.PATIENT_DATA.value.code: - with pytest.raises(InvalidDocTypeException): - patched_service.get_core_document_references( - document_id="123", - snomed_code=wrong_case, - table=MOCK_PDM_TABLE_NAME, - ) - - -def test_get_core_document_references_document_service_returns_none(patched_service): - patched_service.document_service.fetch_documents_from_table.return_value = None - - with pytest.raises(TypeError): - patched_service.get_core_document_references( - document_id="xyz", - snomed_code=SnomedCodes.PATIENT_DATA.value.code, - table=MOCK_PDM_TABLE_NAME, - )