Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions .github/workflows/preview-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ jobs:
APIM_KEY_ID=$KEY_ID, \
APIM_TOKEN_URL=$MOCK_URL/apim/oauth2/token, \
PDM_BUNDLE_URL=$MOCK_URL/apim/check_auth, \
MNS_EVENT_URL=$MOCK_URL/mns, \
MNS_EVENT_URL=$MOCK_URL/mns/events, \
CLIENT_TIMEOUT=$CLIENT_TIMEOUT, \
JWKS_SECRET_NAME=$JWKS_SECRET}" || true
wait_for_lambda_ready
Expand All @@ -206,7 +206,7 @@ jobs:
APIM_MTLS_KEY_NAME=$MTLS_KEY, \
APIM_TOKEN_URL=$MOCK_URL/apim/oauth2/token, \
PDM_BUNDLE_URL=$MOCK_URL/apim/check_auth, \
MNS_EVENT_URL=$MOCK_URL/mns, \
MNS_EVENT_URL=$MOCK_URL/mns/events, \
CLIENT_TIMEOUT=$CLIENT_TIMEOUT, \
JWKS_SECRET_NAME=$JWKS_SECRET}" \
--publish
Expand All @@ -229,13 +229,15 @@ jobs:
- name: Create or update preview Lambda with integration (on open/sync/reopen)
if: github.event.action != 'closed' && github.event.pull_request.user.login != 'dependabot[bot]'
env:
MOCK_URL: ${{ steps.names.outputs.mock_preview_url }}
APIM_INT_URL: ${{ vars.APIM_INT_URL }}
TOKEN_EXPIRY_THRESHOLD: ${{ secrets.APIM_TOKEN_EXPIRY_THRESHOLD }}
JWKS_SECRET_NAME: ${{ secrets.JWKS_SECRET }}
APIM_PRIVATE_KEY: ${{ secrets.APIM_PRIVATE_KEY }}
APIM_APIKEY: ${{ secrets.APIM_APIKEY }}
API_MTLS_CERT: ${{ secrets.API_MTLS_CERT }}
API_MTLS_KEY: ${{ secrets.API_MTLS_KEY }}
APIM_KEY_ID: ${{ secrets.APIM_KEY_ID }}
CLIENT_REQUEST_TIMEOUT: ${{ secrets.CLIENT_REQUEST_TIMEOUT }}
run: |
cd pathology-api/target/
FN="${{ steps.names.outputs.int_function_name }}"
Expand All @@ -245,6 +247,8 @@ jobs:
API_KEY="${APIM_APIKEY:-/cds/pathology/dev/apim/api-key}"
MTLS_CERT="${API_MTLS_CERT:-/cds/pathology/dev/mtls/client1-key-public}"
MTLS_KEY="${API_MTLS_KEY:-/cds/pathology/dev/mtls/client1-key-secret}"
KEY_ID="${APIM_KEY_ID:-DEV-1}"
CLIENT_TIMEOUT="${CLIENT_REQUEST_TIMEOUT:-10s}"
echo "Deploying preview function: $FN"
wait_for_lambda_ready() {
while true; do
Expand All @@ -266,14 +270,18 @@ jobs:
wait_for_lambda_ready
aws lambda update-function-configuration --function-name "$FN" \
--handler "${{ env.LAMBDA_HANDLER }}" \
--memory-size 512 \
--timeout 30 \
--environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
APIM_API_KEY_NAME=$API_KEY, \
APIM_MTLS_CERT_NAME=$MTLS_CERT, \
APIM_MTLS_KEY_NAME=$MTLS_KEY, \
APIM_TOKEN_URL=$MOCK_URL/apim, \
PDM_BUNDLE_URL=$MOCK_URL/pdm, \
MNS_EVENT_URL=$MOCK_URL/mns, \
APIM_KEY_ID=$KEY_ID, \
APIM_TOKEN_URL=$APIM_INT_URL/oauth2/token, \
PDM_BUNDLE_URL=$APIM_INT_URL/patient-data-manager/FHIR/R4/Bundle, \
MNS_EVENT_URL=$APIM_INT_URL/multicast-notification-service/events, \
CLIENT_TIMEOUT=$CLIENT_TIMEOUT, \
JWKS_SECRET_NAME=$JWKS_SECRET}" || true
wait_for_lambda_ready
aws lambda update-function-code --function-name "$FN" \
Expand All @@ -285,14 +293,18 @@ jobs:
--handler "${{ env.LAMBDA_HANDLER }}" \
--zip-file "fileb://artifact.zip" \
--role "${{ steps.role-select.outputs.lambda_role }}" \
--memory-size 512 \
--timeout 30 \
--environment "Variables={APIM_TOKEN_EXPIRY_THRESHOLD=$EXPIRY_THRESHOLD, \
APIM_PRIVATE_KEY_NAME=$PRIVATE_KEY, \
APIM_API_KEY_NAME=$API_KEY, \
APIM_MTLS_CERT_NAME=$MTLS_CERT, \
APIM_MTLS_KEY_NAME=$MTLS_KEY, \
APIM_TOKEN_URL=$MOCK_URL/apim, \
PDM_BUNDLE_URL=$MOCK_URL/pdm, \
MNS_EVENT_URL=$MOCK_URL/mns, \
APIM_KEY_ID=$KEY_ID, \
APIM_TOKEN_URL=$APIM_INT_URL/oauth2/token, \
PDM_BUNDLE_URL=$APIM_INT_URL/patient-data-manager/FHIR/R4/Bundle, \
MNS_EVENT_URL=$APIM_INT_URL/multicast-notification-service/events, \
CLIENT_TIMEOUT=$CLIENT_TIMEOUT, \
JWKS_SECRET_NAME=$JWKS_SECRET}" \
--publish
wait_for_lambda_ready
Expand Down
79 changes: 79 additions & 0 deletions bruno/APIM/Post_Document_Bundle_via_APIM_INT.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
meta {
name: Post Document Bundle via APIM - INT
type: http
seq: 4
}

post {
url: https://{{APIM_ENV}}.api.service.nhs.uk/pathology-laboratory-reporting-pri-{{PR_NUMBER}}/FHIR/R4/Bundle
body: json
auth: inherit
}

headers {
Content-Type: application/fhir+json
}

body:json {
{
"resourceType": "Bundle",
"type": "document",
"entry": [
{
"fullUrl": "composition",
"resource": {
"resourceType": "Composition",
"extension": [
{
"url": "http://hl7.eu/fhir/StructureDefinition/composition-basedOn-order-or-requisition",
"valueReference": {
"reference": "servicerequest"
}
}
],
"subject": {
"identifier": {
"system": "https://fhir.nhs.uk/Id/nhs-number",
"value": "test-nhs-number"
}
}
}
},
{
"fullUrl": "servicerequest",
"resource": {
"resourceType": "ServiceRequest",
"requester": {
"reference": "practitionerrole"
}
}
},
{
"fullUrl": "practitionerrole",
"resource": {
"resourceType": "PractitionerRole",
"organization": {
"reference": "organization"
}
}
},
{
"fullUrl": "organization",
"resource": {
"resourceType": "Organization",
"identifier": [
{
"system": "https://fhir.nhs.uk/Id/ods-organization-code",
"value": "testOrg"
}
]
}
}
]
}
}

settings {
encodeUrl: true
timeout: 0
}
42 changes: 23 additions & 19 deletions mocks/src/mns_mock/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

class MNSResponse(TypedDict):
status_code: int
response: dict[str, Any]
response: dict[str, Any] | None


class EventItem(BaseMockItem):
Expand All @@ -35,27 +35,25 @@ class EventItem(BaseMockItem):
type RequestHandler = Callable[[], MNSResponse]


def _create_operation_outcome(
status_code: int, response: dict[str, Any]
def _create_response(
status_code: int, response: dict[str, Any] | None = None
) -> MNSResponse:
return {"status_code": status_code, "response": response}


def _raise_validation_error(event_type: str) -> RequestHandler:
return lambda: _create_operation_outcome(
400, {"validationErrors": {"type": event_type}}
)
return lambda: _create_response(400, {"validationErrors": {"type": event_type}})


def _raise_authentication_error(fault_string: str, error_code: str) -> RequestHandler:
return lambda: _create_operation_outcome(
return lambda: _create_response(
401,
{"fault": {"faultstring": fault_string, "detail": {"errorcode": error_code}}},
)


def _raise_server_error(status_code: int, errors: str) -> RequestHandler:
return lambda: _create_operation_outcome(status_code, {"errors": errors})
def _raise_error(status_code: int, errors: str) -> RequestHandler:
return lambda: _create_response(status_code, {"errors": errors})


REQUEST_HANDLERS: dict[str, RequestHandler] = {
Expand All @@ -65,7 +63,12 @@ def _raise_server_error(status_code: int, errors: str) -> RequestHandler:
"MNS_AUTHENTICATION_ERROR": _raise_authentication_error(
"Invalid access token", "oauth.v2.InvalidAccessToken"
),
"MNS_SERVER_ERROR": _raise_server_error(500, "Internal server error"),
"MNS_AUTHORIZATION_ERROR": _raise_error(
403, "User is not authorized to handle the requested event type"
),
"MNS_SERVER_ERROR": _raise_error(500, "Internal server error"),
"MNS_BAD_GATEWAY_ERROR": lambda: _create_response(502),
"MNS_GATEWAY_TIMEOUT_ERROR": lambda: _create_response(504),
}


Expand Down Expand Up @@ -102,11 +105,14 @@ def _find_events_in_table(subject: str) -> list[EventItem]:


def _with_default_headers(response: MNSResponse) -> Response[str]:
return Response(
body=json.dumps(response["response"]),
status_code=response["status_code"],
headers={"Content-Type": "application/fhir+json"},
)
if response["response"] is not None:
body = json.dumps(response["response"])
headers = {"Content-Type": "application/fhir+json"}
else:
body = None
headers = None

return Response(body=body, status_code=response["status_code"], headers=headers)


@mns_routes.post("/mns/events")
Expand All @@ -122,17 +128,15 @@ def create_event() -> Response[str]:
except json.JSONDecodeError as err:
_logger.error("Error decoding JSON payload. Error: %s", err)
return _with_default_headers(
_create_operation_outcome(
_create_response(
400, {"validationErrors": {"type": "Invalid payload provided"}}
)
)

if not payload:
_logger.error("No payload provided.")
return _with_default_headers(
_create_operation_outcome(
400, {"validationErrors": {"type": "No payload provided"}}
)
_create_response(400, {"validationErrors": {"type": "No payload provided"}})
)

_logger.debug(
Expand Down
31 changes: 31 additions & 0 deletions mocks/src/mns_mock/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ def test_handle_post_request_success(
"MNS_SERVER_ERROR",
{"status_code": 500, "response": {"errors": "Internal server error"}},
),
(
"MNS_BAD_GATEWAY_ERROR",
{"status_code": 502, "response": None},
),
(
"MNS_GATEWAY_TIMEOUT_ERROR",
{"status_code": 504, "response": None},
),
],
)
def test_handle_post_request_error_responses(
Expand Down Expand Up @@ -196,6 +204,29 @@ def test_create_event_success(
assert response["headers"] == {"Content-Type": "application/fhir+json"}
assert json.loads(response["body"]) == {"id": basic_event_payload["id"]}

@patch("mns_mock.handler.check_authenticated", new=MagicMock(return_value=None))
def test_create_event_without_response_payload(
self,
basic_event_payload: dict[str, Any],
lambda_app: APIGatewayHttpResolver,
) -> None:
payload = basic_event_payload | {"subject": "MNS_BAD_GATEWAY_ERROR"}

event = self._create_test_event(
body=json.dumps(payload),
path_params="mns/events",
request_method="POST",
headers={"Authorization": "Bearer token"},
)
context = LambdaContext()

with patch("mns_mock.handler.storage_helper"):
response = lambda_app.resolve(event, context)

assert response["statusCode"] == 502
assert not response["headers"]
assert response["body"] is None

@patch("mns_mock.handler.check_authenticated")
def test_create_event_fails_authentication(
self,
Expand Down
10 changes: 10 additions & 0 deletions pathology-api/lambda_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from pathology_api.fhir.r4.resources import Bundle, OperationOutcome
from pathology_api.handler import handle_request
from pathology_api.logging import get_logger
from pathology_api.mns import MnsException
from pathology_api.request_context import reset_correlation_id, set_correlation_id

_logger = get_logger(__name__)
Expand Down Expand Up @@ -111,6 +112,15 @@ def handle_exception(exception: Exception) -> Response[str]:
)


@_exception_handler(MnsException)
def handle_mns_exception(exception: MnsException) -> Response[str]:
_logger.exception("Failed to publish MNS event: %s", exception)
return _with_default_headers(
status_code=500,
body=OperationOutcome.create_server_error("Failed to publish an event"),
)


@app.get("/_status")
def status() -> Response[str]:
_logger.debug("Status check endpoint called")
Expand Down
Loading
Loading