diff --git a/Makefile b/Makefile index 93f9f17e5..5e5894aae 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ default: help help: ## This is a help message @grep -E --no-filename '^[a-zA-Z-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-42s\033[0m %s\n", $$1, $$2}' +aws-login: ## Login to AWS. Usage: make aws-login AWS_PROFILE= + aws sso login --profile $(PROFILE) && export AWS_PROFILE=$(PROFILE) + clean: clean-build clean-py clean-test clean-build: @@ -96,14 +99,19 @@ test-unit-collect: cd ./lambdas && ./venv/bin/python3 -m pytest tests/unit --collect-only env: - rm -rf lambdas/venv || true - python3 -m venv ./lambdas/venv - ./lambdas/venv/bin/pip3 install --upgrade pip - ./lambdas/venv/bin/pip3 install -r $(TEST_REQUIREMENTS) --no-cache-dir - ./lambdas/venv/bin/pip3 install -r $(CORE_REQUIREMENTS) --no-cache-dir - ./lambdas/venv/bin/pip3 install -r $(DATA_REQUIREMENTS) --no-cache-dir - ./lambdas/venv/bin/pip3 install -r $(REPORTS_REQUIREMENTS) --no-cache-dir - ./lambdas/venv/bin/pip3 install -r $(ALERTING_REQUIREMENTS) --no-cache-dir + @echo "Removing old venv." + @rm -rf lambdas/venv || true + @echo "Building new venv and installing requirements." + @python3 -m venv ./lambdas/venv + @./lambdas/venv/bin/pip3 install --upgrade pip + @./lambdas/venv/bin/pip3 install -r $(TEST_REQUIREMENTS) --no-cache-dir + @./lambdas/venv/bin/pip3 install -r $(CORE_REQUIREMENTS) --no-cache-dir + @./lambdas/venv/bin/pip3 install -r $(DATA_REQUIREMENTS) --no-cache-dir + @./lambdas/venv/bin/pip3 install -r $(REPORTS_REQUIREMENTS) --no-cache-dir + @./lambdas/venv/bin/pip3 install -r $(ALERTING_REQUIREMENTS) --no-cache-dir + @echo " " + @echo " " + @echo "Now activate your venv." github_env: rm -rf lambdas/venv || true diff --git a/lambdas/tests/unit/handlers/test_get_fhir_document_reference_handler.py b/lambdas/tests/unit/handlers/test_get_fhir_document_reference_handler.py index 1534ad44c..c3af83435 100644 --- a/lambdas/tests/unit/handlers/test_get_fhir_document_reference_handler.py +++ b/lambdas/tests/unit/handlers/test_get_fhir_document_reference_handler.py @@ -33,32 +33,6 @@ "requestContext": {}, } -MOCK_MTLS_VALID_EVENT = { - "httpMethod": "GET", - "headers": {}, - "pathParameters": {"id": f"{SNOMED_CODE}~{TEST_UUID}"}, - "body": None, - "requestContext": { - "accountId": "123456789012", - "apiId": "abc123", - "domainName": "api.example.com", - "identity": { - "sourceIp": "1.2.3.4", - "userAgent": "curl/7.64.1", - "clientCert": { - "clientCertPem": "-----BEGIN CERTIFICATE-----...", - "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", - "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", - "serialNumber": "12:34:56", - "validity": { - "notBefore": "May 10 00:00:00 2024 GMT", - "notAfter": "May 10 00:00:00 2025 GMT", - }, - }, - }, - }, -} - MOCK_INVALID_EVENT_ID_MALFORMED = deepcopy(MOCK_CIS2_VALID_EVENT) MOCK_INVALID_EVENT_ID_MALFORMED["pathParameters"]["id"] = f"~{TEST_UUID}" @@ -171,40 +145,12 @@ def test_lambda_handler_happy_path_with_application_login( ) -def test_lambda_handler_happy_path_with_mtls_pdm_login( - set_env, - mock_document_service, - mock_search_patient_service, - context, -): - mock_document_service.create_document_reference_fhir_response.return_value = ( - "test_document_reference" - ) - - response = lambda_handler(MOCK_MTLS_VALID_EVENT, context) - - 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 - ) - mock_document_service.create_document_reference_fhir_response.assert_called_once_with( - MOCK_DOCUMENT_REFERENCE - ) - - def test_extract_bearer_token(context): context.function_name = "GetDocumentReference" token = extract_bearer_token(MOCK_CIS2_VALID_EVENT, context) assert token == f"Bearer {TEST_UUID}" -def test_extract_bearer_token_when_pdm(context): - token = extract_bearer_token(MOCK_MTLS_VALID_EVENT, context) - assert token is None - - def test_extract_missing_bearer_token(context): context.function_name = "GetDocumentReference" event_without_auth = {"headers": {}} @@ -220,12 +166,6 @@ def test_extract_document_parameters_valid(): assert snomed_code == SNOMED_CODE -def test_extract_document_parameters_valid_pdm(): - document_id, snomed_code = extract_document_parameters(MOCK_MTLS_VALID_EVENT) - assert document_id == TEST_UUID - assert snomed_code == SNOMED_CODE - - def test_extract_document_parameters_invalid(): with pytest.raises(GetFhirDocumentReferenceException) as e: extract_document_parameters(MOCK_INVALID_EVENT_ID_MALFORMED) 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 new file mode 100644 index 000000000..01c2bb148 --- /dev/null +++ b/lambdas/tests/unit/handlers/test_pdm_get_fhir_document_reference_by_id_handler.py @@ -0,0 +1,96 @@ +import pytest +from enums.snomed_codes import SnomedCodes +from handlers.get_fhir_document_reference_handler import ( + extract_document_parameters, + lambda_handler, +) +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_handler_utils import extract_bearer_token + +SNOMED_CODE = SnomedCodes.PATIENT_DATA.value.code + +MOCK_MTLS_VALID_EVENT = { + "httpMethod": "GET", + "headers": {}, + "pathParameters": {"id": f"{SNOMED_CODE}~{TEST_UUID}"}, + "body": None, + "requestContext": { + "accountId": "123456789012", + "apiId": "abc123", + "domainName": "api.example.com", + "identity": { + "sourceIp": "1.2.3.4", + "userAgent": "curl/7.64.1", + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----...", + "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", + "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", + "serialNumber": "12:34:56", + "validity": { + "notBefore": "May 10 00:00:00 2024 GMT", + "notAfter": "May 10 00:00:00 2025 GMT", + }, + }, + }, + }, +} + +MOCK_DOCUMENT_REFERENCE = DocumentReference.model_validate( + MOCK_SEARCH_RESPONSE["Items"][0] +) + + +@pytest.fixture +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 + + +@pytest.fixture +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 = ( + MOCK_DOCUMENT_REFERENCE + ) + return mock_service_instance + + +def test_lambda_handler_happy_path_with_mtls_pdm_login( + set_env, + mock_document_service, + context, +): + mock_document_service.create_document_reference_fhir_response.return_value = ( + "test_document_reference" + ) + + response = lambda_handler(MOCK_MTLS_VALID_EVENT, context) + + 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 + ) + mock_document_service.create_document_reference_fhir_response.assert_called_once_with( + MOCK_DOCUMENT_REFERENCE + ) + + +def test_extract_bearer_token_when_pdm(context): + token = extract_bearer_token(MOCK_MTLS_VALID_EVENT, context) + assert token is None + + +def test_extract_document_parameters_valid_pdm(): + document_id, snomed_code = extract_document_parameters(MOCK_MTLS_VALID_EVENT) + assert document_id == TEST_UUID + assert snomed_code == SNOMED_CODE diff --git a/lambdas/tests/unit/handlers/test_pdm_post_fhir_document_reference_handler.py b/lambdas/tests/unit/handlers/test_pdm_post_fhir_document_reference_handler.py new file mode 100644 index 000000000..e1393b7c1 --- /dev/null +++ b/lambdas/tests/unit/handlers/test_pdm_post_fhir_document_reference_handler.py @@ -0,0 +1,75 @@ +import json + +import pytest +from handlers.post_fhir_document_reference_handler import lambda_handler + + +@pytest.fixture +def valid_mtls_event(): + return { + "body": json.dumps( + { + "resourceType": "DocumentReference", + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + } + }, + } + ), + "headers": json.dumps( + { + "Accept": "text/json", + "Host": "example.com", + } + ), + "requestContext": json.dumps( + { + "accountId": "123456789012", + "apiId": "abc123", + "domainName": "api.example.com", + "identity": { + "sourceIp": "1.2.3.4", + "userAgent": "curl/7.64.1", + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----...", + "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", + "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", + "serialNumber": "12:34:56", + "validity": { + "notBefore": "May 10 00:00:00 2024 GMT", + "notAfter": "May 10 00:00:00 2025 GMT", + }, + }, + }, + } + ), + } + + +@pytest.fixture +def mock_service(mocker): + mock_service = mocker.patch( + "handlers.post_fhir_document_reference_handler.PostFhirDocumentReferenceService" + ) + mock_service_instance = mock_service.return_value + return mock_service_instance + + +def test_mtls_lambda_handler_success(valid_mtls_event, context, mock_service): + """Test successful lambda execution.""" + mock_response = {"resourceType": "DocumentReference", "id": "test-id"} + + mock_service.process_fhir_document_reference.return_value = json.dumps( + mock_response + ) + + result = lambda_handler(valid_mtls_event, context) + + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == mock_response + + mock_service.process_fhir_document_reference.assert_called_once_with( + valid_mtls_event["body"], valid_mtls_event["requestContext"] + ) diff --git a/lambdas/tests/unit/handlers/test_post_fhir_document_reference_handler.py b/lambdas/tests/unit/handlers/test_post_fhir_document_reference_handler.py index 94b221afc..3da0a0c06 100644 --- a/lambdas/tests/unit/handlers/test_post_fhir_document_reference_handler.py +++ b/lambdas/tests/unit/handlers/test_post_fhir_document_reference_handler.py @@ -5,42 +5,9 @@ from handlers.post_fhir_document_reference_handler import lambda_handler from utils.lambda_exceptions import DocumentRefException -@pytest.fixture -def valid_event(): - return { - "body": json.dumps( - { - "resourceType": "DocumentReference", - "subject": { - "identifier": { - "system": "https://fhir.nhs.uk/Id/nhs-number", - "value": "9000000009", - } - }, - } - ), - "headers": json.dumps( - { - "Accept": "text/json", - "Host": "example.com", - } - ), - "requestContext": json.dumps( - { - "accountId": "123456789012", - "apiId": "abc123", - "domainName": "api.example.com", - "identity": { - "sourceIp": "1.2.3.4", - "userAgent": "curl/7.64.1", - }, - } - ), - } - @pytest.fixture -def valid_mtls_event(): +def valid_event(): return { "body": json.dumps( { @@ -67,16 +34,6 @@ def valid_mtls_event(): "identity": { "sourceIp": "1.2.3.4", "userAgent": "curl/7.64.1", - "clientCert": { - "clientCertPem": "-----BEGIN CERTIFICATE-----...", - "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", - "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", - "serialNumber": "12:34:56", - "validity": { - "notBefore": "May 10 00:00:00 2024 GMT", - "notAfter": "May 10 00:00:00 2025 GMT", - }, - }, }, } ), @@ -124,21 +81,3 @@ def test_lambda_handler_exception_handling(valid_event, context, mock_service): mock_service.process_fhir_document_reference.assert_called_once_with( valid_event["body"], valid_event["requestContext"] ) - - -def test_mtls_lambda_handler_success(valid_mtls_event, context, mock_service): - """Test successful lambda execution.""" - mock_response = {"resourceType": "DocumentReference", "id": "test-id"} - - mock_service.process_fhir_document_reference.return_value = json.dumps( - mock_response - ) - - result = lambda_handler(valid_mtls_event, context) - - assert result["statusCode"] == 200 - assert json.loads(result["body"]) == mock_response - - mock_service.process_fhir_document_reference.assert_called_once_with( - valid_mtls_event["body"], valid_mtls_event["requestContext"] - ) diff --git a/lambdas/tests/unit/services/test_document_reference_search_service.py b/lambdas/tests/unit/services/test_document_reference_search_service.py index 1098d561b..73a16b6e7 100644 --- a/lambdas/tests/unit/services/test_document_reference_search_service.py +++ b/lambdas/tests/unit/services/test_document_reference_search_service.py @@ -13,12 +13,11 @@ from models.document_reference import DocumentReference from pydantic import ValidationError from services.document_reference_search_service import DocumentReferenceSearchService -from tests.unit.conftest import APIM_API_URL from tests.unit.helpers.data.dynamo.dynamo_responses import MOCK_SEARCH_RESPONSE +from tests.unit.conftest import APIM_API_URL from utils.common_query_filters import NotDeleted, UploadCompleted from utils.exceptions import DynamoServiceException from utils.lambda_exceptions import DocumentRefSearchException -from utils.lambda_header_utils import validate_common_name_in_mtls MOCK_DOCUMENT_REFERENCE = [ DocumentReference.model_validate(MOCK_SEARCH_RESPONSE["Items"][0]) @@ -32,7 +31,7 @@ "virusScannerResult": "Clean", "id": "3d8683b9-1665-40d2-8499-6e8302d507ff", "fileSize": MOCK_FILE_SIZE, - "version": "1" + "version": "1", } @@ -135,40 +134,6 @@ def test_get_document_references_dynamo_return_successful_response_single_table( assert actual == expected_results -@pytest.mark.parametrize( - "common_name, expected", - [ - ( - { - "accountId": "123456789012", - "apiId": "abc123", - "domainName": "api.example.com", - "identity": { - "sourceIp": "1.2.3.4", - "userAgent": "curl/7.64.1", - "clientCert": { - "clientCertPem": "-----BEGIN CERTIFICATE-----...", - "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", - "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", - "serialNumber": "12:34:56", - "validity": { - "notBefore": "May 10 00:00:00 2024 GMT", - "notAfter": "May 10 00:00:00 2025 GMT", - }, - }, - }, - }, - ["test_pdm_dynamoDB_table"], - ), - ({}, ["test_pdm_dynamoDB_table", "test_lg_dynamoDB_table"]), - ], -) -def test_get_pdm_table(set_env, mock_document_service, common_name, expected): - cn = validate_common_name_in_mtls(common_name) - tables = mock_document_service._get_table_names(cn) - assert tables == expected - - def test_build_document_model_response(mock_document_service, monkeypatch): expected_results = [EXPECTED_RESPONSE] actual = mock_document_service._process_documents(MOCK_DOCUMENT_REFERENCE, False) @@ -338,7 +303,7 @@ def test_create_document_reference_fhir_response(mock_document_service, mocker): mock_document_reference.document_scan_creation = "2023-05-01" mock_document_reference.id = "Y05868-1634567890" mock_document_reference.current_gp_ods = "Y12345" - mock_document_reference.document_snomed_code_type = "717391000000106" + mock_document_reference.document_snomed_code_type = "16521000000101" mock_attachment = mocker.patch( "services.document_reference_search_service.Attachment" @@ -358,7 +323,7 @@ def test_create_document_reference_fhir_response(mock_document_service, mocker): ) expected_fhir_response = { - "id": "717391000000106~Y05868-1634567890", + "id": "16521000000101~Y05868-1634567890", "resourceType": "DocumentReference", "status": "current", "docStatus": "final", @@ -403,14 +368,14 @@ def test_create_document_reference_fhir_response(mock_document_service, mocker): mock_attachment.assert_called_once_with( title=mock_document_reference.file_name, creation=mock_document_reference.document_scan_creation, - url=f"{APIM_API_URL}/DocumentReference/{SnomedCodes.PATIENT_DATA.value.code}~{mock_document_reference.id}", + url=f"{APIM_API_URL}/DocumentReference/{SnomedCodes.LLOYD_GEORGE.value.code}~{mock_document_reference.id}", ) mock_doc_ref_info.assert_called_once_with( nhs_number=mock_document_reference.nhs_number, attachment=mock_attachment_instance, custodian=mock_document_reference.current_gp_ods, - snomed_code_doc_type=SnomedCodes.PATIENT_DATA.value, + snomed_code_doc_type=SnomedCodes.LLOYD_GEORGE.value, ) mock_doc_ref_info_instance.create_fhir_document_reference_object.assert_called_once() @@ -433,11 +398,11 @@ def test_create_document_reference_fhir_response_integration( mock_document_reference.author = "Y12345" mock_document_reference.doc_status = "final" mock_document_reference.custodian = "Y12345" - mock_document_reference.document_snomed_code_type = "717391000000106" + mock_document_reference.document_snomed_code_type = "16521000000101" mock_document_reference.version = "1" expected_fhir_response = { - "id": "717391000000106~Y05868-1634567890", + "id": "16521000000101~Y05868-1634567890", "resourceType": "DocumentReference", "status": "current", "docStatus": "final", @@ -455,7 +420,7 @@ def test_create_document_reference_fhir_response_integration( "language": "en-GB", "title": "test_document.pdf", "creation": "2023-05-01", - "url": f"{APIM_API_URL}/DocumentReference/717391000000106~Y05868-1634567890", + "url": f"{APIM_API_URL}/DocumentReference/16521000000101~Y05868-1634567890", } } ], @@ -477,14 +442,12 @@ def test_create_document_reference_fhir_response_integration( "coding": [ { "system": "http://snomed.info/sct", - "code": "717391000000106", - "display": "Confidential patient data", + "code": "16521000000101", + "display": "Lloyd George record folder", } ] }, - "meta": { - "versionId": "1" - } + "meta": {"versionId": "1"}, } result = mock_document_service.create_document_reference_fhir_response( diff --git a/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py b/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py index 955de9d70..5a84dce1d 100644 --- a/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py +++ b/lambdas/tests/unit/services/test_get_fhir_document_reference_service.py @@ -5,9 +5,9 @@ import pytest from enums.lambda_error import LambdaError -from enums.snomed_codes import SnomedCode, SnomedCodes +from enums.snomed_codes import SnomedCodes, SnomedCode from services.get_fhir_document_reference_service import GetFhirDocumentReferenceService -from tests.unit.conftest import MOCK_LG_TABLE_NAME, MOCK_PDM_TABLE_NAME +from tests.unit.conftest import MOCK_LG_TABLE_NAME from tests.unit.helpers.data.test_documents import create_test_doc_store_refs from utils.lambda_exceptions import ( GetFhirDocumentReferenceException, @@ -53,15 +53,6 @@ def test_handle_get_document_reference_request(patched_service, mocker, set_env) assert expected == actual -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 == MOCK_PDM_TABLE_NAME - - 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.""" 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 new file mode 100644 index 000000000..2efe5a44f --- /dev/null +++ b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_by_id_service.py @@ -0,0 +1,90 @@ +import pytest +from enums.lambda_error import LambdaError +from enums.snomed_codes import SnomedCode, SnomedCodes +from services.get_fhir_document_reference_service import GetFhirDocumentReferenceService +from tests.unit.conftest import MOCK_PDM_TABLE_NAME, MOCK_LG_TABLE_NAME +from tests.unit.helpers.data.test_documents import create_test_doc_store_refs +from utils.lambda_exceptions import ( + GetFhirDocumentReferenceException, + InvalidDocTypeException, +) + + +@pytest.fixture +def patched_service(mocker, set_env, context): + mocker.patch("services.base.s3_service.IAMService") + mocker.patch("services.get_fhir_document_reference_service.S3Service") + mocker.patch("services.get_fhir_document_reference_service.SSMService") + mocker.patch("services.get_fhir_document_reference_service.DocumentService") + service = GetFhirDocumentReferenceService() + + yield service + + +def test_get_document_reference_service(patched_service): + documents = create_test_doc_store_refs() + patched_service.document_service.fetch_documents_from_table.return_value = documents + + actual = patched_service.get_document_references( + "3d8683b9-1665-40d2-8499-6e8302d507ff", MOCK_PDM_TABLE_NAME + ) + assert actual == documents[0] + + +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_document_references", return_value=mock_document_ref + ) + + actual = patched_service.handle_get_document_reference_request( + SnomedCodes.PATIENT_DATA.value.code, "test-id" + ) + + assert expected == actual + + +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 == MOCK_PDM_TABLE_NAME + + +# 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 excinfo: + patched_service._get_dynamo_table_for_doc_type(non_lg_code) + + assert excinfo.value.status_code == 400 + assert excinfo.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 == MOCK_LG_TABLE_NAME + + +def test_get_document_references_empty_result(patched_service): + # Test when no documents are found + patched_service.document_service.fetch_documents_from_table.return_value = [] + + with pytest.raises(GetFhirDocumentReferenceException) as exc_info: + patched_service.get_document_references("test-id", MOCK_PDM_TABLE_NAME) + + assert exc_info.value.error == LambdaError.DocumentReferenceNotFound + assert exc_info.value.status_code == 404 diff --git a/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_search_service.py b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_search_service.py new file mode 100644 index 000000000..b4234f9a4 --- /dev/null +++ b/lambdas/tests/unit/services/test_pdm_get_fhir_document_reference_search_service.py @@ -0,0 +1,241 @@ +import pytest +from enums.snomed_codes import SnomedCodes +from freezegun import freeze_time +from models.document_reference import DocumentReference +from services.document_reference_search_service import DocumentReferenceSearchService +from tests.unit.conftest import APIM_API_URL +from tests.unit.helpers.data.dynamo.dynamo_responses import MOCK_SEARCH_RESPONSE +from utils.lambda_header_utils import validate_common_name_in_mtls + +MOCK_DOCUMENT_REFERENCE = [ + DocumentReference.model_validate(MOCK_SEARCH_RESPONSE["Items"][0]) +] + +MOCK_FILE_SIZE = 24000 + +EXPECTED_RESPONSE = { + "created": "2024-01-01T12:00:00.000Z", + "fileName": "document.csv", + "virusScannerResult": "Clean", + "id": "3d8683b9-1665-40d2-8499-6e8302d507ff", + "fileSize": MOCK_FILE_SIZE, + "version": "1", +} + + +@pytest.fixture +def mock_document_service(mocker, set_env): + service = DocumentReferenceSearchService() + mock_s3_service = mocker.patch.object(service, "s3_service") + mocker.patch.object(mock_s3_service, "get_file_size", return_value=MOCK_FILE_SIZE) + mocker.patch.object(service, "dynamo_service") + mocker.patch.object(service, "fetch_documents_from_table_with_nhs_number") + mocker.patch.object(service, "is_upload_in_process", return_value=False) + return service + + +@pytest.fixture +def mock_filter_builder(mocker): + mock_filter = mocker.MagicMock() + mocker.patch( + "services.document_reference_search_service.DynamoQueryFilterBuilder", + return_value=mock_filter, + ) + return mock_filter + + +@pytest.mark.parametrize( + "common_name, expected", + [ + ( + { + "accountId": "123456789012", + "apiId": "abc123", + "domainName": "api.example.com", + "identity": { + "sourceIp": "1.2.3.4", + "userAgent": "curl/7.64.1", + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----...", + "subjectDN": "CN=ndrclient.main.int.pdm.national.nhs.uk,O=NHS,C=UK", + "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", + "serialNumber": "12:34:56", + "validity": { + "notBefore": "May 10 00:00:00 2024 GMT", + "notAfter": "May 10 00:00:00 2025 GMT", + }, + }, + }, + }, + ["test_pdm_dynamoDB_table"], + ), + ({}, ["test_pdm_dynamoDB_table", "test_lg_dynamoDB_table"]), + ], +) +def test_get_pdm_table(set_env, mock_document_service, common_name, expected): + cn = validate_common_name_in_mtls(common_name) + tables = mock_document_service._get_table_names(cn) + assert tables == expected + + +def test_create_document_reference_fhir_response(mock_document_service, mocker): + mock_document_reference = mocker.MagicMock() + mock_document_reference.nhs_number = "9000000009" + mock_document_reference.file_name = "test_document.pdf" + mock_document_reference.created = "2023-05-01T12:00:00Z" + mock_document_reference.document_scan_creation = "2023-05-01" + mock_document_reference.id = "Y05868-1634567890" + mock_document_reference.current_gp_ods = "Y12345" + mock_document_reference.document_snomed_code_type = "717391000000106" + + mock_attachment = mocker.patch( + "services.document_reference_search_service.Attachment" + ) + mock_attachment_instance = mocker.MagicMock() + mock_attachment.return_value = mock_attachment_instance + + mock_doc_ref_info = mocker.patch( + "services.document_reference_search_service.DocumentReferenceInfo" + ) + mock_doc_ref_info_instance = mocker.MagicMock() + mock_doc_ref_info.return_value = mock_doc_ref_info_instance + + mock_fhir_doc_ref = mocker.MagicMock() + mock_doc_ref_info_instance.create_fhir_document_reference_object.return_value = ( + mock_fhir_doc_ref + ) + + expected_fhir_response = { + "id": "717391000000106~Y05868-1634567890", + "resourceType": "DocumentReference", + "status": "current", + "docStatus": "final", + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + } + }, + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "language": "en-GB", + "title": "test_document.pdf", + "creation": "2023-05-01", + "url": f"{APIM_API_URL}/DocumentReference/123", + } + } + ], + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y05868", + } + } + ], + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y05868", + } + }, + } + mock_fhir_doc_ref.model_dump.return_value = expected_fhir_response + + result = mock_document_service.create_document_reference_fhir_response( + mock_document_reference + ) + + mock_attachment.assert_called_once_with( + title=mock_document_reference.file_name, + creation=mock_document_reference.document_scan_creation, + url=f"{APIM_API_URL}/DocumentReference/{SnomedCodes.PATIENT_DATA.value.code}~{mock_document_reference.id}", + ) + + mock_doc_ref_info.assert_called_once_with( + nhs_number=mock_document_reference.nhs_number, + attachment=mock_attachment_instance, + custodian=mock_document_reference.current_gp_ods, + snomed_code_doc_type=SnomedCodes.PATIENT_DATA.value, + ) + + mock_doc_ref_info_instance.create_fhir_document_reference_object.assert_called_once() + mock_fhir_doc_ref.model_dump.assert_called_once_with(exclude_none=True) + + assert result == expected_fhir_response + + +@freeze_time("2023-05-01T12:00:00Z") +def test_create_document_reference_fhir_response_integration( + mock_document_service, mocker +): + mock_document_reference = mocker.MagicMock() + mock_document_reference.nhs_number = "9000000009" + mock_document_reference.file_name = "test_document.pdf" + mock_document_reference.created = "2023-05-01T12:00:00" + mock_document_reference.document_scan_creation = "2023-05-01" + mock_document_reference.id = "Y05868-1634567890" + mock_document_reference.current_gp_ods = "Y12345" + mock_document_reference.author = "Y12345" + mock_document_reference.doc_status = "final" + mock_document_reference.custodian = "Y12345" + mock_document_reference.document_snomed_code_type = "717391000000106" + mock_document_reference.version = "1" + + expected_fhir_response = { + "id": "717391000000106~Y05868-1634567890", + "resourceType": "DocumentReference", + "status": "current", + "docStatus": "final", + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + } + }, + "date": "2023-05-01T12:00:00", + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "language": "en-GB", + "title": "test_document.pdf", + "creation": "2023-05-01", + "url": f"{APIM_API_URL}/DocumentReference/717391000000106~Y05868-1634567890", + } + } + ], + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + } + } + ], + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "Y12345", + } + }, + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "717391000000106", + "display": "Confidential patient data", + } + ] + }, + "meta": {"versionId": "1"}, + } + + result = mock_document_service.create_document_reference_fhir_response( + mock_document_reference + ) + + assert isinstance(result, dict) + assert result == expected_fhir_response diff --git a/lambdas/tests/unit/services/test_pdm_post_fhir_document_reference_service.py b/lambdas/tests/unit/services/test_pdm_post_fhir_document_reference_service.py new file mode 100644 index 000000000..8419e26c3 --- /dev/null +++ b/lambdas/tests/unit/services/test_pdm_post_fhir_document_reference_service.py @@ -0,0 +1,405 @@ +import json + +import pytest +from enums.lambda_error import LambdaError +from enums.mtls import MtlsCommonNames +from enums.snomed_codes import SnomedCode, SnomedCodes +from models.fhir.R4.base_models import Identifier, Reference +from models.fhir.R4.fhir_document_reference import Attachment +from models.fhir.R4.fhir_document_reference import ( + DocumentReference as FhirDocumentReference, +) +from models.fhir.R4.fhir_document_reference import DocumentReferenceContent +from services.post_fhir_document_reference_service import ( + PostFhirDocumentReferenceService, +) +from tests.unit.conftest import APIM_API_URL +from tests.unit.conftest import ( + EXPECTED_PARSED_PATIENT_BASE_CASE as mock_pds_patient_details, +) +from tests.unit.conftest import MOCK_LG_TABLE_NAME, MOCK_PDM_TABLE_NAME +from utils.lambda_exceptions import InvalidDocTypeException + + +@pytest.fixture +def mock_pds_service_fetch(mocker): + mock_service_object = mocker.MagicMock() + mocker.patch( + "services.post_fhir_document_reference_service.get_pds_service", + return_value=mock_service_object, + ) + mock_service_object.fetch_patient_details.return_value = mock_pds_patient_details + + +@pytest.fixture +def mock_service(set_env, mocker, mock_pds_service_fetch): + mock_s3 = mocker.patch("services.post_fhir_document_reference_service.S3Service") + mock_dynamo = mocker.patch( + "services.post_fhir_document_reference_service.DynamoDBService" + ) + service = PostFhirDocumentReferenceService() + service.s3_service = mock_s3.return_value + service.dynamo_service = mock_dynamo.return_value + + yield service + + +@pytest.fixture +def valid_non_mtls_request_context(): + return { + "accountId": "123456789012", + "apiId": "abc123", + "domainName": "api.example.com", + "identity": { + "sourceIp": "1.2.3.4", + "userAgent": "curl/7.64.1", + }, + } + + +@pytest.fixture +def valid_fhir_doc_json(): + return json.dumps( + { + "resourceType": "DocumentReference", + "docStatus": "final", + "status": "current", + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + } + }, + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": SnomedCodes.LLOYD_GEORGE.value.code, + "display": SnomedCodes.LLOYD_GEORGE.value.display_name, + } + ] + }, + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "A12345", + } + }, + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "A12345", + } + } + ], + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "language": "en-GB", + "title": "test-file.pdf", + "creation": "2023-01-01T12:00:00Z", + } + } + ], + } + ) + + +@pytest.fixture +def valid_mtls_header(): + return { + "Accept": "text/json", + "Host": "example.com", + } + + +@pytest.fixture +def valid_mtls_request_context(): + return { + "accountId": "123456789012", + "apiId": "abc123", + "domainName": "api.example.com", + "identity": { + "sourceIp": "1.2.3.4", + "userAgent": "curl/7.64.1", + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----...", + "subjectDN": "CN=client.dev.ndr.national.nhs.uk,O=NHS,C=UK", + "issuerDN": "CN=NHS Root CA,O=NHS,C=UK", + "serialNumber": "12:34:56", + "validity": { + "notBefore": "May 10 00:00:00 2024 GMT", + "notAfter": "May 10 00:00:00 2025 GMT", + }, + }, + }, + } + + +@pytest.fixture +def valid_mtls_fhir_doc_json(): + return json.dumps( + { + "resourceType": "DocumentReference", + "docStatus": "final", + "status": "current", + "subject": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "9000000009", + } + }, + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": SnomedCodes.PATIENT_DATA.value.code, + "display": SnomedCodes.PATIENT_DATA.value.display_name, + } + ] + }, + "custodian": { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "A12345", + } + }, + "author": [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "A12345", + } + } + ], + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "language": "en-GB", + "title": "test-file.pdf", + "creation": "2023-01-01T12:00:00Z", + } + } + ], + } + ) + + +@pytest.fixture +def valid_fhir_doc_object(valid_fhir_doc_json): + return FhirDocumentReference.model_validate_json(valid_fhir_doc_json) + + +@pytest.fixture +def valid_fhir_doc_with_binary(valid_fhir_doc_json): + doc = json.loads(valid_fhir_doc_json) + doc["content"][0]["attachment"][ + "data" + ] = "SGVsbG8gV29ybGQ=" # Base64 encoded "Hello World" + return json.dumps(doc) + + +@pytest.fixture +def valid_mtls_fhir_doc_object(valid_mtls_fhir_doc_json): + return FhirDocumentReference.model_validate_json(valid_mtls_fhir_doc_json) + + +@pytest.fixture +def valid_mtls_fhir_doc_with_binary(valid_mtls_fhir_doc_json): + doc = json.loads(valid_mtls_fhir_doc_json) + doc["content"][0]["attachment"][ + "data" + ] = "SGVsbG8gV29ybGQ=" # Base64 encoded "Hello World" + return json.dumps(doc) + + +def test_get_dynamo_table_for_patient_data_doc_type(mock_service): + """Test _get_dynamo_table_for_doc_type method with a non-Lloyd George document type.""" + + patient_data_code = SnomedCodes.PATIENT_DATA.value + + result = mock_service._get_dynamo_table_for_doc_type(patient_data_code) + assert result == MOCK_PDM_TABLE_NAME + + +def test_get_dynamo_table_for_unsupported_doc_type(mock_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 excinfo: + mock_service._get_dynamo_table_for_doc_type(non_lg_code) + + assert excinfo.value.status_code == 400 + assert excinfo.value.error == LambdaError.DocTypeDB + + +def test_get_dynamo_table_for_lloyd_george_doc_type(mock_service): + """Test _get_dynamo_table_for_doc_type method with Lloyd George document type.""" + lg_code = SnomedCodes.LLOYD_GEORGE.value + + result = mock_service._get_dynamo_table_for_doc_type(lg_code) + + assert result == MOCK_LG_TABLE_NAME + + +def test_process_mtls_fhir_document_reference_with_binary( + mock_service, valid_mtls_fhir_doc_with_binary, valid_mtls_request_context +): + """Test a happy path with binary data in the request.""" + custom_endpoint = f"{APIM_API_URL}/DocumentReference" + + result = mock_service.process_fhir_document_reference( + valid_mtls_fhir_doc_with_binary, valid_mtls_request_context + ) + + assert isinstance(result, str) + result_json = json.loads(result) + assert result_json["resourceType"] == "DocumentReference" + attachment_url = result_json["content"][0]["attachment"]["url"] + assert custom_endpoint in attachment_url + + mock_service.s3_service.upload_file_obj.assert_called_once() + mock_service.dynamo_service.create_item.assert_called_once() + mock_service.s3_service.create_upload_presigned_url.assert_not_called() + + +def test_determine_document_type_with_correct_common_name(mock_service, mocker): + """Test _determine_document_type method when type is missing entirely.""" + fhir_doc = mocker.MagicMock(spec=FhirDocumentReference) + fhir_doc.type = None + + result = mock_service._determine_document_type(fhir_doc, MtlsCommonNames.PDM) + assert result == SnomedCodes.PATIENT_DATA.value + + +def test_s3_file_key_for_pdm(mock_service, mocker): + """Test _create_document_reference method without custodian information.""" + + fhir_doc = mocker.MagicMock(spec=FhirDocumentReference) + fhir_doc.content = [ + DocumentReferenceContent( + attachment=Attachment( + contentType="application/pdf", + title="test-file.pdf", + creation="2023-01-01T12:00:00Z", + ) + ) + ] + fhir_doc.author = [ + Reference( + identifier=Identifier( + system="https://fhir.nhs.uk/Id/ods-organization-code", value="B67890" + ) + ) + ] + fhir_doc.custodian = None + + doc_type = SnomedCodes.PATIENT_DATA.value + current_gp_ods = "C13579" + + result = mock_service._create_document_reference( + nhs_number="9000000009", + doc_type=doc_type, + fhir_doc=fhir_doc, + current_gp_ods=current_gp_ods, + raw_fhir_doc=json.dumps({"foo": "bar"}), + ) + + assert ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/9000000009" + in result.s3_upload_key + ) + assert f"9000000009/{result.id}" in result.s3_file_key + assert result.sub_folder == f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}" + + +def test_create_pdm_document_reference_with_raw_request(mock_service, mocker): + """Test _create_document_reference method with raw_request included (pdm).""" + + fhir_doc = mocker.MagicMock(spec=FhirDocumentReference) + fhir_doc.content = [ + DocumentReferenceContent( + attachment=Attachment( + contentType="application/pdf", + title="test-file.pdf", + creation="2023-01-01T12:00:00Z", + ) + ) + ] + fhir_doc.custodian = Reference( + identifier=Identifier( + system="https://fhir.nhs.uk/Id/ods-organization-code", value="A12345" + ) + ) + fhir_doc.author = [ + Reference( + identifier=Identifier( + system="https://fhir.nhs.uk/Id/ods-organization-code", value="B67890" + ) + ) + ] + + doc_type = SnomedCodes.PATIENT_DATA.value + + result = mock_service._create_document_reference( + nhs_number="9000000009", + doc_type=doc_type, + fhir_doc=fhir_doc, + current_gp_ods="C13579", + raw_fhir_doc=json.dumps({"foo": "bar"}), + ) + + assert result.raw_request == json.dumps({"foo": "bar"}) + assert result.nhs_number == "9000000009" + assert result.document_snomed_code_type == SnomedCodes.PATIENT_DATA.value.code + assert result.custodian == "A12345" + assert result.current_gp_ods == "C13579" + assert result.author == "B67890" # Verify author is set + + +def test_create_lg_document_reference_with_raw_request(mock_service, mocker): + """Test _create_document_reference method with raw_request included (LG, should be empty).""" + + fhir_doc = mocker.MagicMock(spec=FhirDocumentReference) + fhir_doc.content = [ + DocumentReferenceContent( + attachment=Attachment( + contentType="application/pdf", + title="test-file.pdf", + creation="2023-01-01T12:00:00Z", + ) + ) + ] + fhir_doc.custodian = Reference( + identifier=Identifier( + system="https://fhir.nhs.uk/Id/ods-organization-code", value="A12345" + ) + ) + fhir_doc.author = [ + Reference( + identifier=Identifier( + system="https://fhir.nhs.uk/Id/ods-organization-code", value="B67890" + ) + ) + ] + + doc_type = SnomedCodes.LLOYD_GEORGE.value + + result = mock_service._create_document_reference( + nhs_number="9000000009", + doc_type=doc_type, + fhir_doc=fhir_doc, + current_gp_ods="C13579", + raw_fhir_doc=json.dumps({"foo": "bar"}), + ) + + assert result.raw_request is None + assert result.nhs_number == "9000000009" + assert result.document_snomed_code_type == SnomedCodes.LLOYD_GEORGE.value.code + assert result.custodian == "A12345" + assert result.current_gp_ods == "C13579" + assert result.author == "B67890" # Verify author is set diff --git a/lambdas/tests/unit/services/test_pdm_upload_document_reference_service.py b/lambdas/tests/unit/services/test_pdm_upload_document_reference_service.py new file mode 100644 index 000000000..f7e4f858d --- /dev/null +++ b/lambdas/tests/unit/services/test_pdm_upload_document_reference_service.py @@ -0,0 +1,624 @@ +from unittest.mock import Mock, patch + +import pytest +from botocore.exceptions import ClientError +from enums.virus_scan_result import VirusScanResult +from lambdas.enums.snomed_codes import SnomedCodes +from models.document_reference import DocumentReference +from services.mock_virus_scan_service import MockVirusScanService +from services.upload_document_reference_service import UploadDocumentReferenceService +from tests.unit.conftest import ( + MOCK_LG_BUCKET, + MOCK_LG_TABLE_NAME, + MOCK_STAGING_STORE_BUCKET, + MOCK_PDM_TABLE_NAME, + MOCK_PDM_BUCKET, +) +from utils.common_query_filters import ( + FinalOrPreliminaryAndNotSuperseded, + PreliminaryStatus, +) +from utils.exceptions import DocumentServiceException, FileProcessingException +from utils.lambda_exceptions import InvalidDocTypeException + + +@pytest.fixture +def mock_document_reference(): + """Create a mock document reference""" + doc_ref = Mock(spec=DocumentReference) + doc_ref.id = "test-doc-id" + doc_ref.nhs_number = "9000000001" + doc_ref.s3_file_key = "original/test-key" + doc_ref.s3_bucket_name = "original-bucket" + doc_ref.file_location = "original-location" + doc_ref.virus_scanner_result = None + doc_ref.file_size = 1234567890 + doc_ref.doc_status = "uploading" + doc_ref.version = "1" + doc_ref._build_s3_location = Mock( + return_value="s3://test-lg-bucket/9000000001/test-doc-id" + ) + return doc_ref + + +@pytest.fixture +def mock_virus_scan_service( + mocker, +): + mock = mocker.patch( + "services.upload_document_reference_service.get_virus_scan_service" + ) + yield mock + + +@pytest.fixture +def service(set_env, mock_virus_scan_service): + with patch.multiple( + "services.upload_document_reference_service", + DocumentService=Mock(), + DynamoDBService=Mock(), + S3Service=Mock(), + ): + service = UploadDocumentReferenceService() + service.document_service = Mock() + service.dynamo_service = Mock() + service.virus_scan_service = MockVirusScanService() + service.s3_service = Mock() + return service + + +@pytest.fixture +def mock_pdm_document_reference(): + """Create a mock document reference""" + doc_ref = Mock(spec=DocumentReference) + doc_ref.id = "test-doc-id" + doc_ref.nhs_number = "9000000001" + doc_ref.s3_file_key = ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/9000000001/test-doc-id" + ) + doc_ref.s3_bucket_name = "test-staging-bucket" + doc_ref.virus_scanner_result = None + doc_ref.file_size = 1234567890 + doc_ref.doc_status = "uploading" + doc_ref._build_s3_location = Mock( + return_value=f"s3://test-staging-bucket/fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/9000000001/test-doc-id" + ) + return doc_ref + + +@pytest.fixture +def pdm_service(set_env, mock_virus_scan_service): + with patch.multiple( + "services.upload_document_reference_service", + DocumentService=Mock(), + DynamoDBService=Mock(), + S3Service=Mock(), + ): + service = UploadDocumentReferenceService() + service.document_service = Mock() + service.dynamo_service = Mock() + service.virus_scan_service = MockVirusScanService() + service.s3_service = Mock() + service.table_name = MOCK_PDM_TABLE_NAME + service.destination_bucket_name = MOCK_PDM_BUCKET + service.doc_type = SnomedCodes.PATIENT_DATA.value + return service + + +def test_handle_upload_document_reference_request_with_empty_object_key(pdm_service): + """Test handling of an empty object key""" + pdm_service.handle_upload_document_reference_request("", 122) + + pdm_service.document_service.fetch_documents_from_table.assert_not_called() + + +def test_handle_upload_document_reference_request_with_none_object_key(pdm_service): + """Test handling of a None object key""" + pdm_service.handle_upload_document_reference_request(None, 122) + + pdm_service.document_service.fetch_documents_from_table.assert_not_called() + + +def test_handle_upload_document_reference_request_success( + service, mock_pdm_document_reference, mocker +): + """Test successful handling of the upload document reference request""" + object_key = ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/9000000001/test-doc-id" + ) + object_size = 1111 + + service.document_service.fetch_documents_from_table.side_effect = [ + [mock_pdm_document_reference], + ] + service.virus_scan_service.scan_file = mocker.MagicMock( + return_value=VirusScanResult.CLEAN + ) + + service.handle_upload_document_reference_request(object_key, object_size) + + service.document_service.fetch_documents_from_table.assert_called_once() + service.s3_service.copy_across_bucket.assert_called_once() + service.s3_service.delete_object.assert_called_once() + service.virus_scan_service.scan_file.assert_called_once() + + +def test_handle_upload_document_reference_request_with_exception(pdm_service): + """Test handling of exceptions during processing""" + object_key = "staging/test-doc-id" + + pdm_service.document_service.fetch_documents_from_table.side_effect = Exception( + "Test error" + ) + + pdm_service.handle_upload_document_reference_request(object_key) + + +def test_fetch_preliminary_document_reference_success( + pdm_service, mock_pdm_document_reference +): + """Test successful document reference fetching""" + document_key = "test-doc-id" + pdm_service.document_service.fetch_documents_from_table.return_value = [ + mock_pdm_document_reference + ] + + result = pdm_service._fetch_preliminary_document_reference(document_key) + + assert result == mock_pdm_document_reference + pdm_service.document_service.fetch_documents_from_table.assert_called_once_with( + table_name=MOCK_PDM_TABLE_NAME, + search_condition=document_key, + search_key="ID", + query_filter=PreliminaryStatus, + ) + + +def test_fetch_preliminary_document_reference_no_documents_found(pdm_service): + """Test handling when no documents are found""" + document_key = "test-doc-id" + pdm_service.document_service.fetch_documents_from_table.return_value = [] + + result = pdm_service._fetch_preliminary_document_reference(document_key) + + assert result is None + + +def test_fetch_preliminary_document_reference_multiple_documents_warning( + pdm_service, mock_document_reference +): + """Test handling when multiple documents are found""" + document_key = "test-doc-id" + mock_doc_2 = Mock(spec=DocumentReference) + pdm_service.document_service.fetch_documents_from_table.return_value = [ + mock_document_reference, + mock_doc_2, + ] + + result = pdm_service._fetch_preliminary_document_reference(document_key) + + assert result == mock_document_reference + + +def test_fetch_preliminary_document_reference_exception(pdm_service): + """Test handling of exceptions during document fetching""" + document_key = "test-doc-id" + pdm_service.document_service.fetch_documents_from_table.side_effect = ( + ClientError({"error": "test error message"}, "test"), + ) + + with pytest.raises(DocumentServiceException): + pdm_service._fetch_preliminary_document_reference(document_key) + + +def test__process_preliminary_document_reference_clean_virus_scan( + pdm_service, mock_pdm_document_reference, mocker +): + """Test processing document reference with a clean virus scan""" + object_key = "staging/test-doc-id" + + mocker.patch.object( + pdm_service, "_perform_virus_scan", return_value=VirusScanResult.CLEAN + ) + mock_process_clean = mocker.patch.object(pdm_service, "_process_clean_document") + mock_finalize_transaction = mocker.patch.object( + pdm_service, "_finalize_and_supersede_with_transaction" + ) + + pdm_service._process_preliminary_document_reference( + mock_pdm_document_reference, object_key, 1222 + ) + + mock_process_clean.assert_called_once() + mock_finalize_transaction.assert_not_called() + assert mock_pdm_document_reference.doc_status == "final" + assert mock_pdm_document_reference.uploaded is True + assert mock_pdm_document_reference.uploading is False + + +def test__process_preliminary_document_reference_infected_virus_scan( + pdm_service, mock_document_reference, mocker +): + """Test processing document reference with an infected virus scan""" + object_key = "staging/test-doc-id" + + mocker.patch.object( + pdm_service, "_perform_virus_scan", return_value=VirusScanResult.INFECTED + ) + mock_process_clean = mocker.patch.object(pdm_service, "_process_clean_document") + mock_update_dynamo = mocker.patch.object(pdm_service, "_update_dynamo_table") + pdm_service._process_preliminary_document_reference( + mock_document_reference, object_key, 1222 + ) + + mock_process_clean.assert_not_called() + mock_update_dynamo.assert_called_once() + + +def test_perform_virus_scan_returns_clean_hardcoded( + pdm_service, mock_document_reference +): + """Test virus scan returns hardcoded CLEAN result""" + object_key = "staging/test-doc-id" + result = pdm_service._perform_virus_scan(mock_document_reference, object_key) + assert result == VirusScanResult.CLEAN + + +def test_perform_virus_scan_exception_returns_infected( + pdm_service, mock_document_reference, mocker +): + """Test virus scan exception handling returns INFECTED for safety""" + mock_virus_service = mocker.patch.object(pdm_service, "virus_scan_service") + mock_virus_service.scan_file.side_effect = Exception("Scan error") + object_key = "staging/test-doc-id" + + result = pdm_service._perform_virus_scan(mock_document_reference, object_key) + + assert result == VirusScanResult.ERROR + + +def test_process_clean_document_success(pdm_service, mock_document_reference, mocker): + """Test successful processing of a clean document""" + object_key = "staging/test-doc-id" + + mock_copy = mocker.patch.object(pdm_service, "copy_files_from_staging_bucket") + mock_delete = mocker.patch.object(pdm_service, "delete_file_from_staging_bucket") + + pdm_service._process_clean_document( + mock_document_reference, + object_key, + ) + + mock_copy.assert_called_once_with(mock_document_reference, object_key) + mock_delete.assert_called_once_with(object_key) + + +def test_process_clean_document_exception_restores_original_values( + pdm_service, mock_document_reference, mocker +): + """Test that original values are restored when processing fails""" + object_key = "staging/test-doc-id" + original_s3_key = "original/test-key" + original_bucket = "original-bucket" + original_location = "original-location" + + mocker.patch.object( + pdm_service, + "copy_files_from_staging_bucket", + side_effect=Exception("Copy failed"), + ) + with pytest.raises(FileProcessingException): + pdm_service._process_clean_document( + mock_document_reference, + object_key, + ) + + assert mock_document_reference.s3_file_key == original_s3_key + assert mock_document_reference.s3_bucket_name == original_bucket + assert mock_document_reference.file_location == original_location + assert mock_document_reference.doc_status == "cancelled" + + +def test_copy_files_from_staging_bucket_success( + pdm_service, mock_pdm_document_reference +): + """Test successful file copying from staging bucket""" + source_file_key = ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/9000000001/test-doc-id" + ) + expected_dest_key = "9000000001/test-doc-id" + + pdm_service.copy_files_from_staging_bucket( + mock_pdm_document_reference, source_file_key + ) + + pdm_service.s3_service.copy_across_bucket.assert_called_once_with( + source_bucket=MOCK_STAGING_STORE_BUCKET, + source_file_key=source_file_key, + dest_bucket=MOCK_PDM_BUCKET, + dest_file_key=expected_dest_key, + ) + + assert mock_pdm_document_reference.s3_file_key == expected_dest_key + assert mock_pdm_document_reference.s3_bucket_name == MOCK_PDM_BUCKET + + +def test_copy_files_from_staging_bucket_client_error( + pdm_service, mock_document_reference +): + """Test handling of ClientError during file copying""" + source_file_key = "staging/test-doc-id" + client_error = ClientError( + error_response={ + "Error": {"Code": "NoSuchBucket", "Message": "Bucket does not exist"} + }, + operation_name="CopyObject", + ) + pdm_service.s3_service.copy_across_bucket.side_effect = client_error + + with pytest.raises(FileProcessingException): + pdm_service.copy_files_from_staging_bucket( + mock_document_reference, source_file_key + ) + + +def test_delete_file_from_staging_bucket_success(pdm_service): + """Test successful file deletion from staging bucket""" + source_file_key = "staging/test-doc-id" + + pdm_service.delete_file_from_staging_bucket(source_file_key) + + pdm_service.s3_service.delete_object.assert_called_once_with( + MOCK_STAGING_STORE_BUCKET, source_file_key + ) + + +def test_delete_pdm_file_from_staging_bucket_success(pdm_service): + """Test successful file deletion from staging bucket""" + source_file_key = ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/staging/test-doc-id" + ) + + pdm_service.delete_file_from_staging_bucket(source_file_key) + + pdm_service.s3_service.delete_object.assert_called_once_with( + MOCK_STAGING_STORE_BUCKET, source_file_key + ) + + +def test_delete_file_from_staging_bucket_client_error(pdm_service): + """Test handling of ClientError during file deletion""" + source_file_key = "staging/test-doc-id" + client_error = ClientError( + error_response={ + "Error": {"Code": "NoSuchKey", "Message": "Key does not exist"} + }, + operation_name="DeleteObject", + ) + pdm_service.s3_service.delete_object.side_effect = client_error + + # Should not raise exception, just log the error + try: + pdm_service.delete_file_from_staging_bucket(source_file_key) + except Exception as e: + assert False, f"Unexpected exception: {e}" + + +def test_update_dynamo_table_clean_scan_result( + pdm_service, mock_pdm_document_reference +): + """Test updating DynamoDB table with a clean scan result""" + pdm_service._update_dynamo_table(mock_pdm_document_reference) + + pdm_service.document_service.update_document.assert_called_once_with( + table_name=MOCK_PDM_TABLE_NAME, + document=mock_pdm_document_reference, + update_fields_name={ + "virus_scanner_result", + "doc_status", + "file_location", + "file_size", + "uploaded", + "uploading", + "s3_file_key", + }, + ) + + +def test_update_dynamo_table_infected_scan_result(pdm_service, mock_document_reference): + """Test updating DynamoDB table with an infected scan result""" + pdm_service._update_dynamo_table(mock_document_reference) + + pdm_service.document_service.update_document.assert_called_once() + + +def test_update_dynamo_table_client_error(pdm_service, mock_document_reference): + """Test handling of ClientError during DynamoDB update""" + client_error = ClientError( + error_response={ + "Error": {"Code": "ResourceNotFoundException", "Message": "Table not found"} + }, + operation_name="UpdateItem", + ) + pdm_service.document_service.update_document.side_effect = client_error + + with pytest.raises(DocumentServiceException): + pdm_service._update_dynamo_table(mock_document_reference) + + +def test_handle_upload_document_reference_request_no_document_found(pdm_service): + """Test handling when no preliminary document is found in database""" + object_key = "staging/test-doc-id" + object_size = 1234 + + pdm_service.document_service.fetch_documents_from_table.return_value = [] + + pdm_service.handle_upload_document_reference_request(object_key, object_size) + + # Should fetch but not proceed with processing + pdm_service.document_service.fetch_documents_from_table.assert_called_once() + pdm_service.s3_service.copy_across_bucket.assert_not_called() + pdm_service.document_service.update_document.assert_not_called() + + +def test_process_preliminary_document_reference_exception_during_processing( + pdm_service, mock_document_reference, mocker +): + """Test that exceptions during processing are properly raised""" + object_key = "staging/test-doc-id" + + mocker.patch.object( + pdm_service, "_perform_virus_scan", return_value=VirusScanResult.CLEAN + ) + mocker.patch.object( + pdm_service, + "_process_clean_document", + side_effect=Exception("Processing failed"), + ) + + with pytest.raises(Exception) as exc_info: + pdm_service._process_preliminary_document_reference( + mock_document_reference, object_key, 1222 + ) + + assert "Processing failed" in str(exc_info.value) + + +def test_get_infrastructure_for_document_key_pdm(service): + assert service.table_name == MOCK_LG_TABLE_NAME + assert service.destination_bucket_name == MOCK_LG_BUCKET + service._get_infrastructure_for_document_key( + object_parts=["fhir_upload", SnomedCodes.PATIENT_DATA.value.code, "1234"] + ) + assert service.table_name == MOCK_PDM_TABLE_NAME + assert service.destination_bucket_name == MOCK_PDM_BUCKET + + +def test_get_infrastructure_for_document_key_non_pdm(service): + infra = service._get_infrastructure_for_document_key(object_parts=["1234", "123"]) + assert infra is None + + +def test_get_infra_invalid_doc_type(monkeypatch, service): + # Create a fake doc_type object + fake_doc_type = Mock() + fake_doc_type.code = "999999" + fake_doc_type.display_name = "Fake Doc" + + # Mock SnomedCodes.find_by_code so doc_type is NOT None + monkeypatch.setattr( + "services.upload_document_reference_service.SnomedCodes.find_by_code", + lambda code: fake_doc_type, + ) + + # Mock routers + mock_table_router = Mock() + mock_table_router.resolve.side_effect = KeyError("nope") + + mock_bucket_router = Mock() + + # Force KeyError inside the try block + mock_table_router.resolve.side_effect = KeyError("not found") + service.table_router = mock_table_router + service.bucket_router = mock_bucket_router + # Call function and assert the exception is raised + with pytest.raises(InvalidDocTypeException): + service._get_infrastructure_for_document_key(["fhir_upload", "999999"]) + + +@pytest.mark.parametrize( + "object_key,expected_table,expected_s3_bucket,expected_doctype", + [ + ( + "staging/documents/test-doc-123", + MOCK_LG_TABLE_NAME, + MOCK_LG_BUCKET, + SnomedCodes.LLOYD_GEORGE.value, + ), + ( + "folder/subfolder/another-doc", + MOCK_LG_TABLE_NAME, + MOCK_LG_BUCKET, + SnomedCodes.LLOYD_GEORGE.value, + ), + ( + "simple-doc", + MOCK_LG_TABLE_NAME, + MOCK_LG_BUCKET, + SnomedCodes.LLOYD_GEORGE.value, + ), + ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/staging/test-doc-123", + MOCK_PDM_TABLE_NAME, + MOCK_PDM_BUCKET, + SnomedCodes.PATIENT_DATA.value, + ), + ( + f"{SnomedCodes.LLOYD_GEORGE.value.code}/staging/test-doc-123", + MOCK_LG_TABLE_NAME, + MOCK_LG_BUCKET, + SnomedCodes.LLOYD_GEORGE.value, + ), + ( + f"fhir_upload/{SnomedCodes.LLOYD_GEORGE.value.code}/staging/test-doc-123", + MOCK_LG_TABLE_NAME, + MOCK_LG_BUCKET, + SnomedCodes.LLOYD_GEORGE.value, + ), + ], +) +def test_document_type_extraction_from_object_key( + service, object_key, expected_table, expected_s3_bucket, expected_doctype +): + """Test extraction of a document key from various object key formats""" + service.handle_upload_document_reference_request(object_key) + assert service.table_name == expected_table + assert service.destination_bucket_name == expected_s3_bucket + assert service.doc_type.code == expected_doctype.code + + +def test_handle_upload_pdm_document_reference_request_success( + service, mock_document_reference, mocker +): + """Test successful handling of the upload document reference request""" + pdm_snomed = SnomedCodes.PATIENT_DATA.value + object_key = f"fhir_upload/{pdm_snomed.code}/staging/test-doc-id" + object_size = 1111 + service.document_service.fetch_documents_from_table.return_value = [ + mock_document_reference + ] + service.virus_scan_service.scan_file = mocker.MagicMock( + return_value=VirusScanResult.CLEAN + ) + + service.handle_upload_document_reference_request(object_key, object_size) + + service.document_service.fetch_documents_from_table.assert_called_once() + service.document_service.update_document.assert_called_once() + service.s3_service.copy_across_bucket.assert_called_once() + service.s3_service.delete_object.assert_called_once() + service.virus_scan_service.scan_file.assert_called_once() + + +def test_copy_files_from_staging_bucket_to_pdm_success( + pdm_service, mock_pdm_document_reference +): + """Test successful file copying from staging bucket""" + source_file_key = ( + f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/staging/test-doc-id" + ) + expected_dest_key = ( + f"{mock_pdm_document_reference.nhs_number}/{mock_pdm_document_reference.id}" + ) + pdm_service.copy_files_from_staging_bucket( + mock_pdm_document_reference, source_file_key + ) + pdm_service.s3_service.copy_across_bucket.assert_called_once_with( + source_bucket=MOCK_STAGING_STORE_BUCKET, + source_file_key=source_file_key, + dest_bucket=MOCK_PDM_BUCKET, + dest_file_key=expected_dest_key, + ) + + assert mock_pdm_document_reference.s3_file_key == expected_dest_key + assert mock_pdm_document_reference.s3_bucket_name == MOCK_PDM_BUCKET diff --git a/lambdas/tests/unit/services/test_upload_document_reference_service.py b/lambdas/tests/unit/services/test_upload_document_reference_service.py index ffecdf6d9..bdca4d3ad 100644 --- a/lambdas/tests/unit/services/test_upload_document_reference_service.py +++ b/lambdas/tests/unit/services/test_upload_document_reference_service.py @@ -720,16 +720,6 @@ def test_process_preliminary_document_reference_exception_during_processing( assert "Processing failed" in str(exc_info.value) -def test_get_infrastructure_for_document_key_pdm(service): - assert service.table_name == MOCK_LG_TABLE_NAME - assert service.destination_bucket_name == MOCK_LG_BUCKET - service._get_infrastructure_for_document_key( - object_parts=["fhir_upload", SnomedCodes.PATIENT_DATA.value.code, "1234"] - ) - assert service.table_name == MOCK_PDM_TABLE_NAME - assert service.destination_bucket_name == MOCK_PDM_BUCKET - - def test_get_infrastructure_for_document_key_non_pdm(service): infra = service._get_infrastructure_for_document_key(object_parts=["1234", "123"]) assert infra is None @@ -760,110 +750,3 @@ def test_get_infra_invalid_doc_type(monkeypatch, service): # Call function and assert the exception is raised with pytest.raises(InvalidDocTypeException): service._get_infrastructure_for_document_key(["fhir_upload", "999999"]) - - -@pytest.mark.parametrize( - "object_key,expected_document_key,expected_table", - [ - ( - "staging/documents/test-doc-123", - "test-doc-123", - MOCK_LG_TABLE_NAME, - ), - ("folder/subfolder/another-doc", "another-doc", MOCK_LG_TABLE_NAME), - ("simple-doc", "simple-doc", MOCK_LG_TABLE_NAME), - ( - f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/staging/test-doc-123", - "test-doc-123", - MOCK_PDM_TABLE_NAME, - ), - ( - f"{SnomedCodes.LLOYD_GEORGE.value.code}/staging/test-doc-123", - "test-doc-123", - MOCK_LG_TABLE_NAME, - ), - ( - f"fhir_upload/{SnomedCodes.LLOYD_GEORGE.value.code}/staging/test-doc-123", - "test-doc-123", - MOCK_LG_TABLE_NAME, - ), - ], -) -def test_document_type_extraction_from_object_key( - service, - mock_document_reference, - object_key, - expected_document_key, - expected_table, -): - """Test extraction of a document key from various object key formats""" - service.document_service.fetch_documents_from_table.return_value = [ - mock_document_reference - ] - - service.handle_upload_document_reference_request(object_key) - - assert service.table_name == expected_table - - # if expected_table != MOCK_LG_TABLE_NAME: - # service.document_service.fetch_documents_from_table.assert_called_with( - # table=expected_table, - # search_condition=expected_document_key, - # search_key="ID", - # query_filter=PreliminaryStatus, - # ) - # else: - # service.document_service.fetch_documents_from_table.assert_called_with( - # table=expected_table, - # index_name="S3FileKeyIndex", - # search_condition="original/test-key", - # search_key="S3FileKey", - # query_filter=PreliminaryStatus, - # ) - - -def test_handle_upload_pdm_document_reference_request_success( - service, mock_document_reference, mocker -): - """Test successful handling of the upload document reference request""" - pdm_snomed = SnomedCodes.PATIENT_DATA.value - object_key = f"fhir_upload/{pdm_snomed.code}/staging/test-doc-id" - object_size = 1111 - service.document_service.fetch_documents_from_table.return_value = [ - mock_document_reference - ] - service.virus_scan_service.scan_file = mocker.MagicMock( - return_value=VirusScanResult.CLEAN - ) - - service.handle_upload_document_reference_request(object_key, object_size) - - service.document_service.fetch_documents_from_table.assert_called_once() - service.document_service.update_document.assert_called_once() - service.s3_service.copy_across_bucket.assert_called_once() - service.s3_service.delete_object.assert_called_once() - service.virus_scan_service.scan_file.assert_called_once() - - -def test_copy_files_from_staging_bucket_to_pdm_success( - pdm_service, mock_pdm_document_reference -): - """Test successful file copying from staging bucket""" - source_file_key = ( - f"fhir_upload/{SnomedCodes.PATIENT_DATA.value.code}/staging/test-doc-id" - ) - expected_dest_key = ( - f"{mock_pdm_document_reference.nhs_number}/{mock_pdm_document_reference.id}" - ) - pdm_service.copy_files_from_staging_bucket( - mock_pdm_document_reference, source_file_key - ) - pdm_service.s3_service.copy_across_bucket.assert_called_once_with( - source_bucket=MOCK_STAGING_STORE_BUCKET, - source_file_key=source_file_key, - dest_bucket=MOCK_PDM_BUCKET, - dest_file_key=expected_dest_key, - ) - - assert mock_pdm_document_reference.s3_file_key == expected_dest_key - assert mock_pdm_document_reference.s3_bucket_name == MOCK_PDM_BUCKET