Skip to content

Commit 69191ba

Browse files
ELI-363 added status endpoint(#378)
* moving the validation to api layer * fix flaky test. * error handler - removed make response * Unit test fix * Unit test and Integration test * NHS json minimum response for status endpoint * status http -> https and lint fix * removed TODO * lint fixes * api-domain name as env * api-gateway moved status into payment check * lint fix * sonar fix * sonar fix * added test for checking headers in status reponse * integration test fix * formatting. --------- Co-authored-by: karthikeyannhs <[email protected]>
1 parent 973ddff commit 69191ba

File tree

17 files changed

+347
-199
lines changed

17 files changed

+347
-199
lines changed

infrastructure/modules/lambda/lambda.tf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
2323
ENV = var.environment
2424
LOG_LEVEL = var.log_level
2525
ENABLE_XRAY_PATCHING = var.enable_xray_patching
26+
API_DOMAIN_NAME = var.api_domain_name
2627
}
2728
}
2829

@@ -36,7 +37,9 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
3637
}
3738

3839
layers = compact([
39-
var.environment == "prod" ? "arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:${var.lambda_insights_extension_version}" : null
40+
var.environment == "prod" ?
41+
"arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:${var.lambda_insights_extension_version}"
42+
: null
4043
])
4144

4245
tracing_config {

infrastructure/modules/lambda/variables.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,8 @@ variable "lambda_insights_extension_version" {
7373
description = "version number of LambdaInsightsExtension"
7474
type = number
7575
}
76+
77+
variable "api_domain_name" {
78+
description = "api domain name - env variable for status endpoint response"
79+
type = string
80+
}

infrastructure/stacks/api-layer/api_gateway.tf

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ module "eligibility_signposting_api_gateway" {
88
tags = local.tags
99
}
1010

11-
resource "aws_api_gateway_resource" "_status" {
12-
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
13-
parent_id = module.eligibility_signposting_api_gateway.root_resource_id
14-
path_part = "_status"
15-
}
16-
1711
resource "aws_api_gateway_resource" "patient_check" {
1812
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
1913
parent_id = module.eligibility_signposting_api_gateway.root_resource_id
@@ -26,6 +20,12 @@ resource "aws_api_gateway_resource" "patient" {
2620
path_part = "{id}"
2721
}
2822

23+
resource "aws_api_gateway_resource" "patient_check_status" {
24+
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
25+
parent_id = aws_api_gateway_resource.patient_check.id
26+
path_part = "_status"
27+
}
28+
2929
# deployment
3030

3131
resource "aws_api_gateway_deployment" "eligibility_signposting_api" {
@@ -34,7 +34,7 @@ resource "aws_api_gateway_deployment" "eligibility_signposting_api" {
3434
triggers = {
3535
redeployment = sha1(jsonencode([
3636
aws_api_gateway_integration.get_patient_check.id,
37-
aws_api_gateway_integration._status.id,
37+
aws_api_gateway_integration.get_patient_check_status.id
3838
]))
3939
}
4040

infrastructure/stacks/api-layer/healthcheck_status.tf

Lines changed: 0 additions & 66 deletions
This file was deleted.

infrastructure/stacks/api-layer/lambda.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module "eligibility_signposting_lambda_function" {
1818
environment = var.environment
1919
runtime = "python3.13"
2020
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
21-
security_group_ids = [data.aws_security_group.main_sg.id]
21+
security_group_ids = [data.aws_security_group.main_sg.id]
2222
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
2323
file_name = "../../../dist/lambda.zip"
2424
handler = "eligibility_signposting_api.app.lambda_handler"
@@ -30,4 +30,5 @@ module "eligibility_signposting_lambda_function" {
3030
enable_xray_patching = "true"
3131
stack_name = local.stack_name
3232
provisioned_concurrency_count = 5
33+
api_domain_name = local.api_domain_name
3334
}

infrastructure/stacks/api-layer/patient_check.tf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ resource "aws_api_gateway_integration" "get_patient_check" {
3838
]
3939
}
4040

41+
resource "aws_api_gateway_method" "get_patient_check_status" {
42+
#checkov:skip=CKV_AWS_59: API is secured via Apigee proxy with mTLS, API keys are not used
43+
#checkov:skip=CKV2_AWS_53: No request parameters to validate for static healthcheck endpoint
44+
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
45+
resource_id = aws_api_gateway_resource.patient_check_status.id
46+
http_method = "GET"
47+
authorization = "NONE"
48+
api_key_required = false
49+
50+
depends_on = [
51+
aws_api_gateway_resource.patient_check_status,
52+
aws_api_gateway_resource.patient_check,
53+
]
54+
}
55+
56+
resource "aws_api_gateway_integration" "get_patient_check_status" {
57+
rest_api_id = module.eligibility_signposting_api_gateway.rest_api_id
58+
resource_id = aws_api_gateway_resource.patient_check_status.id
59+
http_method = aws_api_gateway_method.get_patient_check_status.http_method
60+
integration_http_method = "POST" # Needed for lambda proxy integration
61+
type = "AWS_PROXY"
62+
uri = module.eligibility_signposting_lambda_function.aws_lambda_invoke_arn
63+
64+
depends_on = [
65+
aws_api_gateway_method.get_patient_check_status
66+
]
67+
}
68+
4169
resource "aws_lambda_permission" "get_patient_check" {
4270
statement_id = "AllowExecutionFromAPIGateway"
4371
action = "lambda:InvokeFunction"

src/eligibility_signposting_api/app.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from eligibility_signposting_api import audit, repos, services
1313
from eligibility_signposting_api.common.cache_manager import FLASK_APP_CACHE_KEY, cache_manager
1414
from eligibility_signposting_api.common.error_handler import handle_exception
15-
from eligibility_signposting_api.common.request_validator import validate_request_params
1615
from eligibility_signposting_api.config.config import config
16+
from eligibility_signposting_api.config.contants import URL_PREFIX
1717
from eligibility_signposting_api.logging.logs_helper import log_request_ids_from_headers
1818
from eligibility_signposting_api.logging.logs_manager import add_lambda_request_id_to_logger, init_logging
1919
from eligibility_signposting_api.logging.tracing_helper import tracing_setup
@@ -49,7 +49,6 @@ def get_or_create_app() -> Flask:
4949
@add_lambda_request_id_to_logger()
5050
@tracing_setup()
5151
@log_request_ids_from_headers()
52-
@validate_request_params()
5352
def lambda_handler(event: LambdaEvent, context: LambdaContext) -> dict[str, Any]: # pragma: no cover
5453
"""Run the Flask app as an AWS Lambda."""
5554
app = get_or_create_app()
@@ -64,7 +63,7 @@ def create_app() -> Flask:
6463
logger.info("app created")
6564

6665
# Register views & error handler
67-
app.register_blueprint(eligibility_blueprint, url_prefix="/patient-check")
66+
app.register_blueprint(eligibility_blueprint, url_prefix=f"/{URL_PREFIX}")
6867
app.register_error_handler(Exception, handle_exception)
6968

7069
# Set up dependency injection using wireup

src/eligibility_signposting_api/common/api_error_response.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from datetime import UTC, datetime
55
from enum import Enum
66
from http import HTTPStatus
7-
from typing import Any
87

98
from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
9+
from flask import make_response
10+
from flask.typing import ResponseReturnValue
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -65,7 +66,7 @@ def build_operation_outcome_issue(self, diagnostics: str, location: list[str] |
6566
details=details,
6667
) # pyright: ignore[reportCallIssue]
6768

68-
def generate_response(self, diagnostics: str, location_param: str | None = None) -> dict[str, Any]:
69+
def generate_response(self, diagnostics: str, location_param: str | None = None) -> ResponseReturnValue:
6970
issue_location = [f"parameters/{location_param}"] if location_param else None
7071

7172
problem = OperationOutcome(
@@ -75,16 +76,11 @@ def generate_response(self, diagnostics: str, location_param: str | None = None)
7576
) # pyright: ignore[reportCallIssue]
7677

7778
response_body = json.dumps(problem.model_dump(by_alias=True, mode="json"))
78-
79-
return {
80-
"statusCode": self.status_code,
81-
"headers": {"Content-Type": "application/fhir+json"},
82-
"body": response_body,
83-
}
79+
return make_response(response_body, self.status_code, {"Content-Type": "application/fhir+json"})
8480

8581
def log_and_generate_response(
8682
self, log_message: str, diagnostics: str, location_param: str | None = None
87-
) -> dict[str, Any]:
83+
) -> ResponseReturnValue:
8884
logger.error(log_message)
8985
return self.generate_response(diagnostics, location_param)
9086

src/eligibility_signposting_api/common/error_handler.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
import traceback
33

4-
from flask import make_response
54
from flask.typing import ResponseReturnValue
65
from werkzeug.exceptions import HTTPException
76

@@ -11,7 +10,7 @@
1110
logger = logging.getLogger(__name__)
1211

1312

14-
def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException:
13+
def handle_exception(e: Exception) -> ResponseReturnValue:
1514
if isinstance(e, HTTPException):
1615
return e
1716

@@ -25,7 +24,6 @@ def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException:
2524
logger.exception("Unexpected Exception", exc_info=e)
2625
log_msg = f"An unexpected error occurred: {full_traceback}"
2726

28-
response = INTERNAL_SERVER_ERROR.log_and_generate_response(
27+
return INTERNAL_SERVER_ERROR.log_and_generate_response(
2928
log_message=log_msg, diagnostics="An unexpected error occurred."
3029
)
31-
return make_response(response.get("body"), response.get("statusCode"), response.get("headers"))

src/eligibility_signposting_api/common/request_validator.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import re
33
from collections.abc import Callable
44
from functools import wraps
5-
from typing import Any
65

7-
from mangum.types import LambdaContext, LambdaEvent
6+
from flask import request
7+
from flask.typing import ResponseReturnValue
88

99
from eligibility_signposting_api.common.api_error_response import (
1010
INVALID_CATEGORY_ERROR,
@@ -21,7 +21,7 @@
2121
include_actions_pattern = re.compile(r"^\s*([YN])\s*$", re.IGNORECASE)
2222

2323

24-
def validate_query_params(query_params: dict[str, str]) -> tuple[bool, dict[str, Any] | None]:
24+
def validate_query_params(query_params: dict[str, str]) -> tuple[bool, ResponseReturnValue | None]:
2525
conditions = query_params.get("conditions", "ALL").split(",")
2626
for condition in conditions:
2727
search = re.search(condition_pattern, condition)
@@ -39,7 +39,7 @@ def validate_query_params(query_params: dict[str, str]) -> tuple[bool, dict[str,
3939
return True, None
4040

4141

42-
def validate_nhs_number(path_nhs: str, header_nhs: str) -> bool:
42+
def validate_nhs_number(path_nhs: str | None, header_nhs: str | None) -> bool:
4343
logger.info("NHS numbers from the request", extra={"header_nhs": header_nhs, "path_nhs": path_nhs})
4444

4545
if not header_nhs or not path_nhs:
@@ -55,28 +55,28 @@ def validate_nhs_number(path_nhs: str, header_nhs: str) -> bool:
5555
def validate_request_params() -> Callable:
5656
def decorator(func: Callable) -> Callable:
5757
@wraps(func)
58-
def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None:
59-
path_nhs_no = event.get("pathParameters", {}).get("id")
60-
header_nhs_no = event.get("headers", {}).get(NHS_NUMBER_HEADER)
58+
def wrapper(*args, **kwargs) -> ResponseReturnValue: # noqa:ANN002,ANN003
59+
path_nhs_number = str(kwargs.get("nhs_number"))
60+
header_nhs_no = str(request.headers.get(NHS_NUMBER_HEADER))
6161

62-
if not validate_nhs_number(path_nhs_no, header_nhs_no):
62+
if not validate_nhs_number(path_nhs_number, header_nhs_no):
6363
message = "You are not authorised to request information for the supplied NHS Number"
6464
return NHS_NUMBER_MISMATCH_ERROR.log_and_generate_response(log_message=message, diagnostics=message)
6565

66-
query_params = event.get("queryStringParameters")
66+
query_params = request.args
6767
if query_params:
6868
is_valid, problem = validate_query_params(query_params)
69-
if not is_valid:
69+
if not is_valid and problem is not None:
7070
return problem
7171

72-
return func(event, context)
72+
return func(*args, **kwargs)
7373

7474
return wrapper
7575

7676
return decorator
7777

7878

79-
def get_include_actions_error_response(include_actions: str) -> dict[str, Any]:
79+
def get_include_actions_error_response(include_actions: str) -> ResponseReturnValue:
8080
diagnostics = f"{include_actions} is not a value that is supported by the API"
8181
return INVALID_INCLUDE_ACTIONS_ERROR.log_and_generate_response(
8282
log_message=f"Invalid include actions query param: '{include_actions}'",
@@ -85,14 +85,14 @@ def get_include_actions_error_response(include_actions: str) -> dict[str, Any]:
8585
)
8686

8787

88-
def get_category_error_response(category: str) -> dict[str, Any]:
88+
def get_category_error_response(category: str) -> ResponseReturnValue:
8989
diagnostics = f"{category} is not a category that is supported by the API"
9090
return INVALID_CATEGORY_ERROR.log_and_generate_response(
9191
log_message=f"Invalid category query param: '{category}'", diagnostics=diagnostics, location_param="category"
9292
)
9393

9494

95-
def get_condition_error_response(condition: str) -> dict[str, Any]:
95+
def get_condition_error_response(condition: str) -> ResponseReturnValue:
9696
diagnostics = (
9797
f"{condition} should be a single or comma separated list of condition "
9898
f"strings with no other punctuation or special characters"

0 commit comments

Comments
 (0)