Skip to content

Commit 89cd2ca

Browse files
[PRMP-594] Get Document by ID (#858)
Co-authored-by: adamwhitingnhs <[email protected]>
1 parent b3cd797 commit 89cd2ca

File tree

8 files changed

+424
-1
lines changed

8 files changed

+424
-1
lines changed

.github/workflows/base-lambdas-reusable-deploy-all.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,17 @@ jobs:
682682
lambda_layer_names: "core_lambda_layer"
683683
secrets:
684684
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
685+
686+
deploy_get_document_reference_by_id_lambda:
687+
name: Deploy get_document_reference_lambda
688+
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
689+
with:
690+
environment: ${{ inputs.environment}}
691+
python_version: ${{ inputs.python_version }}
692+
build_branch: ${{ inputs.build_branch}}
693+
sandbox: ${{ inputs.sandbox }}
694+
lambda_handler_name: get_document_reference_handler
695+
lambda_aws_name: GetDocRefLambda
696+
lambda_layer_names: "core_lambda_layer"
697+
secrets:
698+
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
2+
from utils.decorators.override_error_check import override_error_check
3+
from services.feature_flags_service import FeatureFlagService
4+
from enums.feature_flags import FeatureFlags
5+
from services.get_document_reference_service import GetDocumentReferenceService
6+
from utils.decorators.validate_patient_id import validate_patient_id
7+
from utils.lambda_exceptions import FeatureFlagsException
8+
from enums.lambda_error import LambdaError
9+
from utils.lambda_exceptions import GetDocumentRefException
10+
from utils.lambda_response import ApiGatewayResponse
11+
from utils.decorators.ensure_env_var import ensure_environment_variables
12+
from utils.decorators.set_audit_arg import set_request_context_for_logging
13+
from enums.logging_app_interaction import LoggingAppInteraction
14+
from utils.audit_logging_setup import LoggingService
15+
from utils.request_context import request_context
16+
import json
17+
18+
logger = LoggingService(__name__)
19+
20+
@validate_patient_id
21+
@handle_lambda_exceptions
22+
@set_request_context_for_logging
23+
@ensure_environment_variables(
24+
names=[
25+
"LLOYD_GEORGE_DYNAMODB_NAME",
26+
"PRESIGNED_ASSUME_ROLE",
27+
"APPCONFIG_APPLICATION",
28+
"APPCONFIG_ENVIRONMENT",
29+
"APPCONFIG_CONFIGURATION",
30+
"EDGE_REFERENCE_TABLE",
31+
"CLOUDFRONT_URL",
32+
]
33+
)
34+
@override_error_check
35+
def lambda_handler(event: dict[str, any], context):
36+
request_context.app_interaction = LoggingAppInteraction.VIEW_LG_RECORD.value
37+
38+
feature_flag_service = FeatureFlagService()
39+
feature_flag_service.validate_feature_flag(FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED)
40+
41+
logger.info("Starting document fetch by ID process")
42+
43+
try:
44+
document_id = event["pathParameters"]["id"]
45+
nhs_number = event["queryStringParameters"]["patientId"]
46+
except KeyError:
47+
raise GetDocumentRefException(400, LambdaError.DocumentReferenceMissingParameters)
48+
49+
service = GetDocumentReferenceService()
50+
51+
document_info = service.get_document_url_by_id(document_id, nhs_number)
52+
53+
return ApiGatewayResponse(
54+
status_code=200, body=json.dumps(document_info), methods="GET"
55+
).create_api_gateway_response()

lambdas/services/feature_flags_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,4 @@ def validate_feature_flag(self, flag_name: str):
155155

156156
if not flag_object.get(flag_name, False):
157157
logger.info(f"Feature flag '{flag_name}' not enabled, event will not be processed")
158-
raise FeatureFlagsException(404, LambdaError.FeatureFlagDisabled)
158+
raise FeatureFlagsException(404, LambdaError.FeatureFlagDisabled)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import os
2+
import uuid
3+
from datetime import datetime, timezone
4+
from services.get_fhir_document_reference_service import (
5+
GetFhirDocumentReferenceService,
6+
)
7+
from utils.audit_logging_setup import LoggingService
8+
from utils.lambda_exceptions import GetDocumentRefException
9+
from enums.lambda_error import LambdaError
10+
from utils.utilities import format_cloudfront_url
11+
from models.document_reference import DocumentReference
12+
from utils.dynamo_query_filter_builder import DynamoQueryFilterBuilder
13+
from enums.dynamo_filter import AttributeOperator
14+
from utils.common_query_filters import NotDeleted
15+
16+
logger = LoggingService(__name__)
17+
18+
class GetDocumentReferenceService:
19+
def __init__(self):
20+
self.fhir_doc_service = GetFhirDocumentReferenceService()
21+
self.document_service = self.fhir_doc_service.document_service
22+
self.dynamo_service = self.document_service.dynamo_service
23+
self.s3_service = self.fhir_doc_service.s3_service
24+
self.lg_table = os.environ.get("LLOYD_GEORGE_DYNAMODB_NAME")
25+
self.cloudfront_table_name = os.environ.get("EDGE_REFERENCE_TABLE")
26+
self.cloudfront_url = os.environ.get("CLOUDFRONT_URL")
27+
28+
def get_document_url_by_id(self, document_id: str, nhs_number: str):
29+
document_reference = self.get_document_reference(document_id, nhs_number)
30+
31+
presigned_s3_url = self.create_document_presigned_url(
32+
document_reference.s3_bucket_name,
33+
document_reference.s3_file_key
34+
)
35+
36+
return {
37+
"url": presigned_s3_url,
38+
"contentType": document_reference.content_type
39+
}
40+
41+
def create_document_presigned_url(self, bucket_name, file_location):
42+
presigned_url_response = self.s3_service.create_download_presigned_url(
43+
s3_bucket_name=bucket_name,
44+
file_key=file_location,
45+
)
46+
47+
presigned_id = str(uuid.uuid4())
48+
deletion_date = datetime.now(timezone.utc)
49+
50+
ttl_half_an_hour_in_seconds = self.s3_service.presigned_url_expiry
51+
dynamo_item_ttl = int(deletion_date.timestamp() + ttl_half_an_hour_in_seconds)
52+
self.dynamo_service.create_item(
53+
self.cloudfront_table_name,
54+
{
55+
"ID": presigned_id,
56+
"presignedUrl": presigned_url_response,
57+
"TTL": dynamo_item_ttl,
58+
},
59+
)
60+
return format_cloudfront_url(presigned_id, self.cloudfront_url)
61+
62+
def get_document_reference(self, document_id: str, nhs_number: str) -> DocumentReference:
63+
filter_builder = DynamoQueryFilterBuilder()
64+
filter_builder.add_condition("DocStatus", AttributeOperator.EQUAL, "final")
65+
filter_builder.add_condition("NhsNumber", AttributeOperator.EQUAL, nhs_number)
66+
67+
table_filter = filter_builder.build()
68+
69+
table_filter = table_filter & NotDeleted
70+
71+
documents = self.document_service.fetch_documents_from_table(
72+
table_name=self.lg_table,
73+
search_condition=document_id,
74+
search_key="ID",
75+
query_filter=table_filter,
76+
)
77+
if len(documents) > 0:
78+
logger.info("Document found for given id")
79+
return documents[0]
80+
else:
81+
raise GetDocumentRefException(
82+
404, LambdaError.DocumentReferenceNotFound
83+
)

lambdas/tests/unit/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@
140140
MOCK_DOCUMENT_REVIEW_BUCKET = "test_document_review_bucket"
141141
MOCK_EDGE_TABLE = "test_edge_reference_table"
142142

143+
MOCK_EDGE_REFERENCE_TABLE = "test_edge_reference_table"
144+
143145
@pytest.fixture
144146
def set_env(monkeypatch):
145147
monkeypatch.setenv("AWS_DEFAULT_REGION", REGION_NAME)
@@ -232,6 +234,7 @@ def set_env(monkeypatch):
232234
monkeypatch.setenv("EDGE_REFERENCE_TABLE", MOCK_EDGE_TABLE)
233235
monkeypatch.setenv("STAGING_STORE_BUCKET_NAME", MOCK_STAGING_STORE_BUCKET)
234236
monkeypatch.setenv("METADATA_SQS_QUEUE_URL", MOCK_LG_METADATA_SQS_QUEUE)
237+
monkeypatch.setenv("EDGE_REFERENCE_TABLE", MOCK_EDGE_REFERENCE_TABLE)
235238

236239
EXPECTED_PARSED_PATIENT_BASE_CASE = PatientDetails(
237240
givenName=["Jane"],
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import pytest
2+
import json
3+
import os
4+
from unittest.mock import patch
5+
from handlers.get_document_reference_handler import lambda_handler
6+
from utils.lambda_exceptions import GetDocumentRefException
7+
from enums.feature_flags import FeatureFlags
8+
from utils.lambda_response import ApiGatewayResponse
9+
from utils.lambda_exceptions import FeatureFlagsException
10+
from enums.lambda_error import LambdaError
11+
from utils.error_response import ErrorResponse
12+
13+
14+
@pytest.fixture
15+
def mock_feature_flag_service(mocker):
16+
yield mocker.patch("handlers.get_document_reference_handler.FeatureFlagService").return_value
17+
18+
@pytest.fixture
19+
def mock_get_document_service(mocker):
20+
yield mocker.patch(
21+
"handlers.get_document_reference_handler.GetDocumentReferenceService"
22+
).return_value
23+
24+
@pytest.fixture
25+
def mock_valid_nhs_number():
26+
yield "4407064188"
27+
28+
@pytest.fixture
29+
def feature_flag():
30+
yield FeatureFlags.UPLOAD_DOCUMENT_ITERATION_3_ENABLED
31+
32+
@pytest.fixture
33+
def mock_interaction_id():
34+
yield "88888888-4444-4444-4444-121212121212"
35+
36+
@pytest.fixture
37+
def mocked_bad_env_vars():
38+
env_vars = {
39+
#"LLOYD_GEORGE_DYNAMODB_NAME": "mock_dynamodb_name",
40+
"PRESIGNED_ASSUME_ROLE": "mock_presigned_role",
41+
"APPCONFIG_APPLICATION": "mock_value",
42+
"APPCONFIG_ENVIRONMENT": "mock_value",
43+
"APPCONFIG_CONFIGURATION": "mock_value",
44+
"EDGE_REFERENCE_TABLE": "mock_value",
45+
"CLOUDFRONT_URL": "mock_value",
46+
}
47+
48+
with patch.dict(os.environ, env_vars):
49+
yield "LLOYD_GEORGE_DYNAMODB_NAME"
50+
51+
52+
53+
def test_handler_valid_request_returns_200(
54+
valid_id_event_with_auth_header,
55+
mock_feature_flag_service,
56+
mock_get_document_service,
57+
mock_valid_nhs_number,
58+
context,
59+
set_env,
60+
feature_flag
61+
):
62+
mock_document_id = "1"
63+
valid_id_event_with_auth_header["pathParameters"] = {"id": mock_document_id}
64+
valid_id_event_with_auth_header["queryStringParameters"]["patientId"] = mock_valid_nhs_number
65+
mock_presigned_s3_url = "https://mock.url/"
66+
mock_content_type = "application/pdf"
67+
68+
expected_body = {
69+
"url": mock_presigned_s3_url,
70+
"contentType": mock_content_type
71+
}
72+
73+
expected_result = ApiGatewayResponse(
74+
status_code=200, body=json.dumps(expected_body), methods="GET"
75+
).create_api_gateway_response()
76+
77+
mock_get_document_service.get_document_url_by_id.return_value = expected_body
78+
79+
result = lambda_handler(valid_id_event_with_auth_header, context)
80+
81+
assert result == expected_result
82+
assert result["body"] == json.dumps(expected_body)
83+
84+
mock_feature_flag_service.validate_feature_flag.assert_called_once_with(
85+
feature_flag
86+
)
87+
mock_get_document_service.get_document_url_by_id.assert_called_once_with(
88+
mock_document_id,
89+
mock_valid_nhs_number)
90+
91+
def test_missing_nhs_number_errors(
92+
valid_id_event_with_auth_header,
93+
mock_feature_flag_service,
94+
context,
95+
set_env,
96+
feature_flag,
97+
):
98+
valid_id_event_with_auth_header["pathParameters"] = {"id": "1"}
99+
valid_id_event_with_auth_header["queryStringParameters"].pop("patientId")
100+
101+
expected_result = ApiGatewayResponse(
102+
status_code=400,
103+
body=LambdaError.PatientIdNoKey.create_error_body(),
104+
methods="GET",
105+
).create_api_gateway_response()
106+
107+
result = lambda_handler(valid_id_event_with_auth_header, context)
108+
109+
assert result == expected_result
110+
111+
def test_missing_document_id_errors(
112+
valid_id_event_with_auth_header,
113+
mock_feature_flag_service,
114+
mock_valid_nhs_number,
115+
context,
116+
set_env,
117+
feature_flag,
118+
mock_interaction_id
119+
):
120+
valid_id_event_with_auth_header["pathParameters"] = {}
121+
valid_id_event_with_auth_header["queryStringParameters"]["patientId"] = mock_valid_nhs_number
122+
123+
expected_error = GetDocumentRefException(400, LambdaError.DocumentReferenceMissingParameters)
124+
125+
expected_result = ApiGatewayResponse(
126+
status_code=400,
127+
body=ErrorResponse(
128+
err_code=expected_error.err_code,
129+
message=expected_error.message,
130+
interaction_id=mock_interaction_id
131+
).create(),
132+
methods="GET"
133+
).create_api_gateway_response()
134+
135+
mock_feature_flag_service.get_feature_flags_by_flag.return_value = {feature_flag: True}
136+
137+
result = lambda_handler(valid_id_event_with_auth_header, context)
138+
139+
assert result == expected_result
140+
141+
def test_env_vars_not_set_errors(
142+
valid_id_event_with_auth_header,
143+
context,
144+
mocked_bad_env_vars
145+
):
146+
expected_result = ApiGatewayResponse(
147+
status_code=500,
148+
body=LambdaError.EnvMissing.create_error_body({"name": mocked_bad_env_vars}),
149+
methods="GET"
150+
).create_api_gateway_response()
151+
152+
result = lambda_handler(valid_id_event_with_auth_header, context)
153+
154+
assert result == expected_result

0 commit comments

Comments
 (0)