diff --git a/lambdas/README.md b/lambdas/README.md old mode 100644 new mode 100755 index 3b69118ef..dd9ef935e --- a/lambdas/README.md +++ b/lambdas/README.md @@ -96,9 +96,7 @@ If successful, the lambda will return status code 200 with patient details as th ```json { - "givenName": [ - "Jane" - ], + "givenName": ["Jane"], "familyName": "Smith", "birthDate": "2010-10-22", "postalCode": "LS1 6AE", @@ -122,7 +120,7 @@ Testing in AWS on the lambda directly: ```json { "queryStringParameters": { - "patientId":"9449305552" + "patientId": "9449305552" } } ``` @@ -139,7 +137,6 @@ Hitting URL directly: https://{url}/SearchDocumentReferences?patientId=9449305552 ``` - #### Possible outputs Success: @@ -232,3 +229,19 @@ or... } ``` +### document_manifest_by_nhs_number_handler + +The manifest lambda expects two query string parameters called patientId and docType. + +**patientId** is to be supplied as a String, and should conform to standard NHS Number format + +**docType** is a String and expects a single or comma-seperated list of types of document you're searching for. +It can be set to the following values: + +For just Lloyd George docs "LG" + +For just ARF docs "ARF" + +For all docs "LG,ARF" + +If the parameter is not supplied, the values contain something unspecified, or it is an empty String, a 400 error will be returned diff --git a/lambdas/handlers/document_manifest_by_nhs_number_handler.py b/lambdas/handlers/document_manifest_by_nhs_number_handler.py index 078fa74a1..78795923f 100644 --- a/lambdas/handlers/document_manifest_by_nhs_number_handler.py +++ b/lambdas/handlers/document_manifest_by_nhs_number_handler.py @@ -2,49 +2,46 @@ import os from botocore.exceptions import ClientError -from enums.metadata_field_names import DocumentReferenceMetadataFields -from models.document import Document +from enums.supported_document_types import SupportedDocumentTypes +from lambdas.utils.decorators.validate_patient_id import validate_patient_id +from lambdas.utils.decorators.ensure_env_var import ensure_environment_variables +from services.manifest_dynamo_service import ManifestDynamoService from services.document_manifest_service import DocumentManifestService -from services.dynamo_service import DynamoDBService -from utils.exceptions import (DynamoDbException, InvalidResourceIdException, +from utils.decorators.validate_document_type import validate_document_type +from utils.exceptions import (DynamoDbException, ManifestDownloadException) from utils.lambda_response import ApiGatewayResponse -from utils.utilities import validate_id logger = logging.getLogger() logger.setLevel(logging.INFO) +@validate_patient_id +@validate_document_type +@ensure_environment_variables( + names=["DOCUMENT_STORE_DYNAMODB_NAME", + "LLOYD_GEORGE_DYNAMODB_NAME", + "ZIPPED_STORE_BUCKET_NAME", + "ZIPPED_STORE_DYNAMODB_NAME" + ] +) def lambda_handler(event, context): logger.info("Starting document manifest process") try: nhs_number = event["queryStringParameters"]["patientId"] - validate_id(nhs_number) + doc_type = event["queryStringParameters"]["docType"] - document_store_table_name = os.environ["DOCUMENT_STORE_DYNAMODB_NAME"] - lloyd_george_table_name = os.environ["LLOYD_GEORGE_DYNAMODB_NAME"] zip_output_bucket = os.environ["ZIPPED_STORE_BUCKET_NAME"] zip_trace_table_name = os.environ["ZIPPED_STORE_DYNAMODB_NAME"] # zip_trace_ttl = os.environ["DOCUMENT_ZIP_TRACE_TTL_IN_DAYS"] - dynamo_service = DynamoDBService() - - logger.info("Retrieving doc store documents") - ds_documents = query_documents( - dynamo_service, document_store_table_name, nhs_number - ) - - logger.info("Retrieving lloyd george documents") - lg_documents = query_documents( - dynamo_service, lloyd_george_table_name, nhs_number - ) - - documents = lg_documents + ds_documents + dynamo_service = ManifestDynamoService() + documents = dynamo_service.discover_uploaded_documents(nhs_number, doc_type) if not documents: return ApiGatewayResponse( - 204, "No documents found for given NHS number", "GET" + 204, "No documents found for given NHS number and document type", "GET" ).create_api_gateway_response() logger.info("Starting document manifest process") @@ -59,14 +56,6 @@ def lambda_handler(event, context): return ApiGatewayResponse(200, response, "GET").create_api_gateway_response() - except InvalidResourceIdException: - return ApiGatewayResponse( - 400, "Invalid NHS number", "GET" - ).create_api_gateway_response() - except KeyError as e: - return ApiGatewayResponse( - 400, f"An error occurred due to missing key: {str(e)}", "GET" - ).create_api_gateway_response() except ManifestDownloadException as e: return ApiGatewayResponse( 500, @@ -85,38 +74,3 @@ def lambda_handler(event, context): 500, "An error occurred when creating document manifest", "POST" ).create_api_gateway_response() return response - - -def query_documents( - dynamo_service: DynamoDBService, document_table: str, nhs_number: str -) -> list[Document]: - documents = [] - - response = dynamo_service.query_service( - document_table, - "NhsNumberIndex", - "NhsNumber", - nhs_number, - [ - DocumentReferenceMetadataFields.FILE_NAME, - DocumentReferenceMetadataFields.FILE_LOCATION, - DocumentReferenceMetadataFields.VIRUS_SCAN_RESULT, - ], - ) - if response is None or ("Items" not in response): - logger.error(f"Unrecognised response from DynamoDB: {response}") - raise DynamoDbException("Unrecognised response from DynamoDB") - - for item in response["Items"]: - document = Document( - nhs_number=nhs_number, - file_name=item[DocumentReferenceMetadataFields.FILE_NAME.field_name], - virus_scanner_result=item[ - DocumentReferenceMetadataFields.VIRUS_SCAN_RESULT.field_name - ], - file_location=item[ - DocumentReferenceMetadataFields.FILE_LOCATION.field_name - ], - ) - documents.append(document) - return documents diff --git a/lambdas/services/manifest_dynamo_service.py b/lambdas/services/manifest_dynamo_service.py new file mode 100644 index 000000000..18eb62037 --- /dev/null +++ b/lambdas/services/manifest_dynamo_service.py @@ -0,0 +1,55 @@ +import logging +import os + +from enums.supported_document_types import SupportedDocumentTypes +from services.dynamo_service import DynamoDBService +from enums.metadata_field_names import DocumentReferenceMetadataFields +from models.document import Document +from utils.exceptions import DynamoDbException + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +class ManifestDynamoService(DynamoDBService): + + def discover_uploaded_documents( + self, nhs_number: str, doc_types: str + ) -> list[Document]: + arf_documents = [] + lg_documents = [] + + if SupportedDocumentTypes.ARF.name in doc_types: + arf_documents = self.fetch_documents_from_table(nhs_number, os.environ["DOCUMENT_STORE_DYNAMODB_NAME"]) + if SupportedDocumentTypes.LG.name in doc_types: + lg_documents = self.fetch_documents_from_table(nhs_number, os.environ["LLOYD_GEORGE_DYNAMODB_NAME"]) + + return arf_documents + lg_documents + + def fetch_documents_from_table(self, nhs_number: str, table: str) -> list[Document]: + documents = [] + response = self.query_service( + table, + "NhsNumberIndex", + "NhsNumber", + nhs_number, + [ + DocumentReferenceMetadataFields.FILE_NAME, + DocumentReferenceMetadataFields.FILE_LOCATION, + DocumentReferenceMetadataFields.VIRUS_SCAN_RESULT, + ], + ) + + for item in response["Items"]: + document = Document( + nhs_number=nhs_number, + file_name=item[DocumentReferenceMetadataFields.FILE_NAME.field_name], + virus_scanner_result=item[ + DocumentReferenceMetadataFields.VIRUS_SCAN_RESULT.field_name + ], + file_location=item[ + DocumentReferenceMetadataFields.FILE_LOCATION.field_name + ], + ) + documents.append(document) + return documents diff --git a/lambdas/tests/unit/handlers/conftest.py b/lambdas/tests/unit/handlers/conftest.py index 0eecebf5e..2c3f9fa42 100755 --- a/lambdas/tests/unit/handlers/conftest.py +++ b/lambdas/tests/unit/handlers/conftest.py @@ -2,6 +2,8 @@ import pytest +from lambdas.enums.supported_document_types import SupportedDocumentTypes + @pytest.fixture def valid_id_event(): @@ -11,6 +13,38 @@ def valid_id_event(): return api_gateway_proxy_event +@pytest.fixture +def valid_id_and_both_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "LG,ARF"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_arf_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "ARF"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_lg_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "LG"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_invalid_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "MANGO"}, + } + return api_gateway_proxy_event + + @pytest.fixture def invalid_id_event(): api_gateway_proxy_event = { diff --git a/lambdas/tests/unit/handlers/test_create_document_manifest_by_nhs_number_handler.py b/lambdas/tests/unit/handlers/test_create_document_manifest_by_nhs_number_handler.py index eb2c35f9b..a3c3521b2 100644 --- a/lambdas/tests/unit/handlers/test_create_document_manifest_by_nhs_number_handler.py +++ b/lambdas/tests/unit/handlers/test_create_document_manifest_by_nhs_number_handler.py @@ -1,11 +1,9 @@ +from unittest.mock import call + from enums.metadata_field_names import DocumentReferenceMetadataFields -from handlers.document_manifest_by_nhs_number_handler import (lambda_handler, - query_documents) -from services.dynamo_service import DynamoDBService -from tests.unit.conftest import MOCK_TABLE_NAME, TEST_NHS_NUMBER -from tests.unit.helpers.data.dynamo_responses import \ - MOCK_MANIFEST_QUERY_RESPONSE -from tests.unit.helpers.data.test_documents import TEST_DS_DOCS, TEST_LG_DOCS +from enums.supported_document_types import SupportedDocumentTypes +from handlers.document_manifest_by_nhs_number_handler import lambda_handler +from tests.unit.helpers.data.test_documents import TEST_ARF_DOCS, TEST_LG_DOCS from utils.lambda_response import ApiGatewayResponse TEST_METADATA_FIELDS = [ @@ -16,31 +14,56 @@ def test_lambda_handler_returns_204_when_no_documents_returned_from_dynamo_response( - mocker, set_env, valid_id_event, context + mocker, set_env, valid_id_and_arf_doctype_event, context ): mock_document_query = mocker.patch( - "handlers.document_manifest_by_nhs_number_handler.query_documents" + "services.manifest_dynamo_service.ManifestDynamoService.discover_uploaded_documents" ) mock_document_query.return_value = [] expected = ApiGatewayResponse( - 204, "No documents found for given NHS number", "GET" + 204, "No documents found for given NHS number and document type", "GET" ).create_api_gateway_response() - actual = lambda_handler(valid_id_event, context) + actual = lambda_handler(valid_id_and_arf_doctype_event, context) + + assert expected == actual + + +def test_lambda_handler_returns_400_when_doc_type_invalid_response( + mocker, set_env, valid_id_and_invalid_doctype_event, context +): + mock_document_query = mocker.patch( + "services.manifest_dynamo_service.ManifestDynamoService.discover_uploaded_documents" + ) + mock_document_query.return_value = [] + + expected = ApiGatewayResponse( + 400, "Invalid document type requested", "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_invalid_doctype_event, context) assert expected == actual -def test_lambda_handler_valid_parameters_returns_200( - mocker, set_env, valid_id_event, context +def manifest_service_side_effect(nhs_number, doc_types): + if SupportedDocumentTypes.ARF.name in doc_types: + return [TEST_ARF_DOCS] + if SupportedDocumentTypes.LG.name in doc_types: + return [TEST_LG_DOCS] + return [] + + +def test_lambda_handler_valid_parameters_arf_doc_type_request_returns_200( + mocker, set_env, valid_id_and_arf_doctype_event, context ): expected_url = "test-url" mock_dynamo = mocker.patch( - "handlers.document_manifest_by_nhs_number_handler.query_documents" + "services.manifest_dynamo_service.ManifestDynamoService.discover_uploaded_documents" ) - mock_dynamo.side_effect = [TEST_DS_DOCS, TEST_LG_DOCS] + mock_dynamo.side_effect = manifest_service_side_effect mock_doc_manifest_url = mocker.patch( "services.document_manifest_service.DocumentManifestService.create_document_manifest_presigned_url" @@ -51,21 +74,71 @@ def test_lambda_handler_valid_parameters_returns_200( 200, expected_url, "GET" ).create_api_gateway_response() - actual = lambda_handler(valid_id_event, context) + actual = lambda_handler(valid_id_and_arf_doctype_event, context) + mock_dynamo.assert_called_once_with("9000000009", "ARF") + assert expected == actual + + +def test_lambda_handler_valid_parameters_lg_doc_type_request_returns_200( + mocker, set_env, valid_id_and_lg_doctype_event, context +): + expected_url = "test-url" + mock_dynamo = mocker.patch( + "services.manifest_dynamo_service.ManifestDynamoService.discover_uploaded_documents" + ) + mock_dynamo.side_effect = manifest_service_side_effect + + mock_doc_manifest_url = mocker.patch( + "services.document_manifest_service.DocumentManifestService.create_document_manifest_presigned_url" + ) + mock_doc_manifest_url.return_value = expected_url + + expected = ApiGatewayResponse( + 200, expected_url, "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_lg_doctype_event, context) + mock_dynamo.assert_called_once_with("9000000009", "LG") + assert expected == actual + + +def test_lambda_handler_valid_parameters_both_doc_type_request_returns_200( + mocker, set_env, valid_id_and_both_doctype_event, context +): + expected_url = "test-url" + + mock_dynamo = mocker.patch( + "services.manifest_dynamo_service.ManifestDynamoService.discover_uploaded_documents" + ) + mock_dynamo.side_effect = manifest_service_side_effect + + mock_doc_manifest_url = mocker.patch( + "services.document_manifest_service.DocumentManifestService.create_document_manifest_presigned_url" + ) + mock_doc_manifest_url.return_value = expected_url + + expected = ApiGatewayResponse( + 200, expected_url, "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_both_doctype_event, context) + mock_dynamo.assert_has_calls([ + call("9000000009", "LG,ARF"), + ]) assert expected == actual -def test_lambda_handler_missing_environment_variables_returns_400( - set_env, monkeypatch, valid_id_event, context +def test_lambda_handler_missing_environment_variables_returns_500( + set_env, monkeypatch, valid_id_and_arf_doctype_event, context ): monkeypatch.delenv("DOCUMENT_STORE_DYNAMODB_NAME") expected = ApiGatewayResponse( - 400, + 500, "An error occurred due to missing key: 'DOCUMENT_STORE_DYNAMODB_NAME'", "GET", ).create_api_gateway_response() - actual = lambda_handler(valid_id_event, context) + actual = lambda_handler(valid_id_and_arf_doctype_event, context) assert expected == actual @@ -78,7 +151,7 @@ def test_lambda_handler_id_not_valid_returns_400(set_env, invalid_id_event, cont def test_lambda_handler_when_id_not_supplied_returns_400( - set_env, missing_id_event, context + set_env, missing_id_event, context ): expected = ApiGatewayResponse( 400, "An error occurred due to missing key: 'patientId'", "GET" @@ -87,13 +160,11 @@ def test_lambda_handler_when_id_not_supplied_returns_400( assert expected == actual -def test_query_documents(set_env, mocker): - mock_dynamo_service = DynamoDBService() - query_patch = mocker.patch("services.dynamo_service.DynamoDBService.query_service") - query_patch.return_value = MOCK_MANIFEST_QUERY_RESPONSE - - expected = TEST_DS_DOCS - - response = query_documents(mock_dynamo_service, MOCK_TABLE_NAME, TEST_NHS_NUMBER) - - assert response == expected +def test_lambda_handler_returns_400_when_doc_type_not_supplied( + set_env, valid_id_event, context +): + expected = ApiGatewayResponse( + 400, "An error occurred due to missing key: 'docType'", "GET" + ).create_api_gateway_response() + actual = lambda_handler(valid_id_event, context) + assert expected == actual diff --git a/lambdas/tests/unit/helpers/data/test_documents.py b/lambdas/tests/unit/helpers/data/test_documents.py index 20a54881f..a54edc0bf 100644 --- a/lambdas/tests/unit/helpers/data/test_documents.py +++ b/lambdas/tests/unit/helpers/data/test_documents.py @@ -2,7 +2,7 @@ NHS_NUMBER = "1111111111" -TEST_DS_DOCS = [ +TEST_ARF_DOCS = [ Document( NHS_NUMBER, "document.csv", diff --git a/lambdas/tests/unit/services/test_document_manifest_service.py b/lambdas/tests/unit/services/test_document_manifest_service.py old mode 100644 new mode 100755 index 758a338ed..a8cd857c9 --- a/lambdas/tests/unit/services/test_document_manifest_service.py +++ b/lambdas/tests/unit/services/test_document_manifest_service.py @@ -1,3 +1,5 @@ +import os + from models.document import Document from services.document_manifest_service import DocumentManifestService from tests.unit.conftest import (MOCK_BUCKET, MOCK_ZIP_OUTPUT_BUCKET, @@ -88,9 +90,9 @@ def test_download_documents_to_be_zipped_calls_download_file(set_env, mocker): assert mock_s3_service_download_file.call_count == 3 + def test_download_documents_to_be_zipped_creates_download_path(set_env, mocker): mocker.patch("boto3.client") - mock_document = [ Document( "123456789", @@ -109,9 +111,8 @@ def test_download_documents_to_be_zipped_creates_download_path(set_env, mocker): service.download_documents_to_be_zipped() - expected_download_path = ( - f"{service.temp_downloads_dir}/{MOCK_DOCUMENTS[0].file_name}" - ) + expected_download_path = os.path.join(service.temp_downloads_dir, MOCK_DOCUMENTS[0].file_name) + document_file_key = MOCK_DOCUMENTS[0].file_key mock_s3_service_download_file.assert_called_with( diff --git a/lambdas/tests/unit/services/test_manifest_dynamo_service.py b/lambdas/tests/unit/services/test_manifest_dynamo_service.py new file mode 100644 index 000000000..5cf937b64 --- /dev/null +++ b/lambdas/tests/unit/services/test_manifest_dynamo_service.py @@ -0,0 +1,78 @@ +import os + +import boto3 +import pytest + +from unittest.mock import MagicMock, patch + +from enums.supported_document_types import SupportedDocumentTypes +from tests.unit.helpers.data.dynamo_responses import MOCK_EMPTY_RESPONSE, MOCK_MANIFEST_QUERY_RESPONSE +from lambdas.services.manifest_dynamo_service import ManifestDynamoService +from models.document import Document + + +@pytest.fixture +def nhs_number(): + return "9000000009" + + +def test_returns_list_of_documents_when_results_are_returned(nhs_number): + expected_table = "expected_table_name" + os.environ["LLOYD_GEORGE_DYNAMODB_NAME"] = expected_table + os.environ["DOCUMENT_STORE_DYNAMODB_NAME"] = "no-table" + + with patch.object(boto3, "resource", return_value=MagicMock()) as mock_dynamo: + mock_table = MagicMock() + mock_dynamo.return_value.Table.return_value = mock_table + mock_table.query.return_value = MOCK_MANIFEST_QUERY_RESPONSE + result = ManifestDynamoService().discover_uploaded_documents(nhs_number, "LG") + + mock_dynamo.return_value.Table.assert_called_with(expected_table) + + assert len(result) == 3 + assert type(result[0]) == Document + + +def test_only_retrieves_documents_from_lloyd_george_table(nhs_number): + expected_table = "expected_table_name" + os.environ["LLOYD_GEORGE_DYNAMODB_NAME"] = expected_table + os.environ["DOCUMENT_STORE_DYNAMODB_NAME"] = "no-table" + + with patch.object(boto3, "resource", return_value=MagicMock()) as mock_dynamo: + mock_table = MagicMock() + mock_dynamo.return_value.Table.return_value = mock_table + mock_table.query.return_value = MOCK_EMPTY_RESPONSE + result = ManifestDynamoService().discover_uploaded_documents(nhs_number, "LG") + + mock_dynamo.return_value.Table.assert_called_with(expected_table) + + assert len(result) == 0 + + +def test_only_retrieves_documents_from_electronic_health_record_table(nhs_number): + expected_table = "expected_table_name" + os.environ["LLOYD_GEORGE_DYNAMODB_NAME"] = "no-table" + os.environ["DOCUMENT_STORE_DYNAMODB_NAME"] = expected_table + + with patch.object(boto3, "resource", return_value=MagicMock()) as mock_dynamo: + mock_table = MagicMock() + mock_dynamo.return_value.Table.return_value = mock_table + mock_table.query.return_value = MOCK_EMPTY_RESPONSE + result = ManifestDynamoService().discover_uploaded_documents(nhs_number, "ARF") + + mock_dynamo.return_value.Table.assert_called_with(expected_table) + + assert len(result) == 0 + + +def test_nothing_returned_when_invalid_doctype_supplied(nhs_number): + expected_table = "expected_table_name" + os.environ["LLOYD_GEORGE_DYNAMODB_NAME"] = "no-table" + os.environ["DOCUMENT_STORE_DYNAMODB_NAME"] = expected_table + + with patch.object(boto3, "resource", return_value=MagicMock()) as mock_dynamo: + mock_table = MagicMock() + mock_dynamo.return_value.Table.return_value = mock_table + result = ManifestDynamoService().discover_uploaded_documents(nhs_number, "") + + assert len(result) == 0 diff --git a/lambdas/tests/unit/utils/decorators/conftest.py b/lambdas/tests/unit/utils/decorators/conftest.py index 0eecebf5e..84a085a07 100644 --- a/lambdas/tests/unit/utils/decorators/conftest.py +++ b/lambdas/tests/unit/utils/decorators/conftest.py @@ -38,3 +38,59 @@ class LambdaContext: ) return LambdaContext() + + +@pytest.fixture +def valid_id_and_arf_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "ARF"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_lg_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "LG"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_both_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "LG,ARF"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_invalid_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "MANGO"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_nonsense_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": "sdfjfvsjhfvsukjARFfjdhtgdkjughLG"}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_empty_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": ""}, + } + return api_gateway_proxy_event + + +@pytest.fixture +def valid_id_and_none_doctype_event(): + api_gateway_proxy_event = { + "queryStringParameters": {"patientId": "9000000009", "docType": None}, + } + return api_gateway_proxy_event diff --git a/lambdas/tests/unit/utils/decorators/test_validate_document_type.py b/lambdas/tests/unit/utils/decorators/test_validate_document_type.py new file mode 100755 index 000000000..cd96a2286 --- /dev/null +++ b/lambdas/tests/unit/utils/decorators/test_validate_document_type.py @@ -0,0 +1,89 @@ +from lambdas.tests.unit.utils.decorators.conftest import (context, + valid_id_event, + valid_id_and_invalid_doctype_event, + valid_id_and_none_doctype_event, + valid_id_and_empty_doctype_event, + valid_id_and_lg_doctype_event, + valid_id_and_arf_doctype_event, + valid_id_and_both_doctype_event) +from lambdas.utils.decorators.validate_document_type import validate_document_type +from utils.lambda_response import ApiGatewayResponse + + +@validate_document_type +def lambda_handler(event, context): + return "200 OK" + + +def test_runs_lambda_when_receiving_arf_doc_type(valid_id_and_arf_doctype_event, context): + expected = "200 OK" + + actual = lambda_handler(valid_id_and_arf_doctype_event, context) + + assert actual == expected + + +def test_runs_lambda_when_receiving_lg_doc_type(valid_id_and_lg_doctype_event, context): + expected = "200 OK" + + actual = lambda_handler(valid_id_and_lg_doctype_event, context) + + assert actual == expected + + +def test_runs_lambda_when_receiving_both_doc_types(valid_id_and_both_doctype_event, context): + expected = "200 OK" + + actual = lambda_handler(valid_id_and_both_doctype_event, context) + + assert actual == expected + + +def test_returns_400_response_when_doctype_not_supplied(valid_id_event, context): + expected = ApiGatewayResponse( + 400, "An error occurred due to missing key: 'docType'", "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_event, context) + + assert actual == expected + + +def test_returns_400_response_when_invalid_doctype_supplied(valid_id_and_invalid_doctype_event, context): + expected = ApiGatewayResponse( + 400, "Invalid document type requested", "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_invalid_doctype_event, context) + + assert actual == expected + + +def test_returns_400_response_when_nonsense_doctype_supplied(valid_id_and_nonsense_doctype_event, context): + expected = ApiGatewayResponse( + 400, "Invalid document type requested", "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_nonsense_doctype_event, context) + + assert actual == expected + + +def test_returns_400_response_when_empty_doctype_supplied(valid_id_and_empty_doctype_event, context): + expected = ApiGatewayResponse( + 400, "Invalid document type requested", "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_empty_doctype_event, context) + + assert actual == expected + + +def test_returns_400_response_when_doctype_field_not_in_event(valid_id_and_none_doctype_event, context): + expected = ApiGatewayResponse( + 400, "docType not supplied", "GET" + ).create_api_gateway_response() + + actual = lambda_handler(valid_id_and_none_doctype_event, context) + + assert actual == expected diff --git a/lambdas/utils/decorators/validate_document_type.py b/lambdas/utils/decorators/validate_document_type.py new file mode 100755 index 000000000..46447a0fb --- /dev/null +++ b/lambdas/utils/decorators/validate_document_type.py @@ -0,0 +1,45 @@ +from typing import Callable + +from enums.supported_document_types import SupportedDocumentTypes +from utils.lambda_response import ApiGatewayResponse + + +def validate_document_type(lambda_func: Callable): + """A decorator for lambda handler. + Verify that the incoming event contains a valid document Type (ARF or LG) + If not, returns a 400 Bad request response before the lambda triggers. + + Usage: + @validate_patient_id + def lambda_handler(event, context): + ... + """ + + def interceptor(event, context): + try: + doc_type = event["queryStringParameters"]["docType"] + if doc_type is None: + return ApiGatewayResponse( + 400, "docType not supplied", "GET" + ).create_api_gateway_response() + if not doc_type_is_valid(doc_type): + return ApiGatewayResponse( + 400, "Invalid document type requested", "GET" + ).create_api_gateway_response() + except KeyError as e: + return ApiGatewayResponse( + 400, f"An error occurred due to missing key: {str(e)}", "GET" + ).create_api_gateway_response() + + # Validation done. Return control flow to original lambda handler + return lambda_func(event, context) + + return interceptor + + +def doc_type_is_valid(doc_types: str) -> bool: + doc_types_requested = doc_types.split(",") + for doc_type_requested in doc_types_requested: + if SupportedDocumentTypes.get_from_field_name(doc_type_requested) is None: + return False + return True