Skip to content

Commit 30e7cbc

Browse files
Merge pull request #40 from NHSDigital/feature/eli-136-fhir-operation-outcome-for-errors
ELI-136: Use FHIR OperationOutcome for problem reporting.
2 parents 07b1b9d + a8b9048 commit 30e7cbc

File tree

9 files changed

+369
-304
lines changed

9 files changed

+369
-304
lines changed

.github/workflows/stage-3-build.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ jobs:
4747
- name: "Build lambda artefact"
4848
run: |
4949
make dependencies install-python
50-
poetry self add poetry-plugin-lambda-build poetry-plugin-export
5150
make build
5251
- name: "Upload lambda artefact"
5352
uses: actions/upload-artifact@v4

poetry.lock

Lines changed: 309 additions & 263 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ eval-type-backport = "^0.2.2"
3232
mangum = "^0.19.0"
3333
wireup = "^1.0.1"
3434
python-json-logger = "^3.3.0"
35+
fhir-resources = "^8.0.0"
3536

3637
[tool.poetry.group.dev.dependencies]
3738
ruff = "^0.11.0"
@@ -55,7 +56,7 @@ pytest-docker = "^3.2.0"
5556
stamina = "^25.1.0"
5657

5758
[tool.poetry-plugin-lambda-build]
58-
docker-image = "public.ecr.aws/sam/build-python3.13:1.135-x86_64"
59+
docker-image = "public.ecr.aws/sam/build-python3.13:1.137-x86_64"
5960
docker-network = "host"
6061
docker-platform = "linux/x86_64"
6162
package-artifact-path = "dist/lambda.zip"

scripts/dependencies.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ if ! [ -x "$(command -v poetry)" ]; then
99
fi
1010
pipx install poetry
1111
fi
12-
poetry self add poetry-plugin-lambda-build poetry-plugin-export
12+
poetry self add poetry-plugin-lambda-build@2.1.0 poetry-plugin-export@1.9.0

src/eligibility_signposting_api/error_handler.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
import traceback
33
from http import HTTPStatus
44

5+
from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
56
from flask import make_response
67
from flask.typing import ResponseReturnValue
78
from werkzeug.exceptions import HTTPException
89

9-
from eligibility_signposting_api.views.response_models import Problem
10-
1110
logger = logging.getLogger(__name__)
1211

1312

@@ -18,10 +17,11 @@ def handle_exception(e: Exception) -> ResponseReturnValue | HTTPException:
1817
if isinstance(e, HTTPException):
1918
return e
2019

21-
problem = Problem(
22-
title="Unexpected Exception",
23-
type=str(type(e)),
24-
status=HTTPStatus.INTERNAL_SERVER_ERROR,
25-
detail="".join(traceback.format_exception(e)),
20+
problem = OperationOutcome(
21+
issue=[
22+
OperationOutcomeIssue(
23+
severity="severe", code="unexpected", diagnostics="".join(traceback.format_exception(e))
24+
) # pyright: ignore[reportCallIssue]
25+
]
2626
)
2727
return make_response(problem.model_dump(), HTTPStatus.INTERNAL_SERVER_ERROR)

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import logging
22
from http import HTTPStatus
33

4+
from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
45
from flask import Blueprint, make_response, request
56
from flask.typing import ResponseReturnValue
67
from wireup import Injected
78

89
from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
910
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
10-
from eligibility_signposting_api.views.response_models import EligibilityResponse, Problem
11+
from eligibility_signposting_api.views.response_models import EligibilityResponse
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -22,8 +23,14 @@ def check_eligibility(eligibility_service: Injected[EligibilityService]) -> Resp
2223
eligibility = eligibility_service.get_eligibility(nhs_number)
2324
except UnknownPersonError:
2425
logger.debug("nhs_number %r not found", nhs_number, extra={"nhs_number": nhs_number})
25-
problem = Problem(
26-
title="nhs_number not found", status=HTTPStatus.NOT_FOUND, detail=f"nhs_number {nhs_number} not found."
26+
problem = OperationOutcome(
27+
issue=[
28+
OperationOutcomeIssue(
29+
severity="information",
30+
code="nhs-number-not-found",
31+
diagnostics=f'NHS Number "{nhs_number}" not found.',
32+
) # pyright: ignore[reportCallIssue]
33+
]
2734
)
2835
return make_response(problem.model_dump(), HTTPStatus.NOT_FOUND)
2936
else:
Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
from pydantic import BaseModel
22

33

4-
class HelloResponse(BaseModel):
5-
status: int
6-
message: str
7-
8-
9-
class Error(BaseModel):
10-
name: str
11-
reason: str
12-
13-
14-
class Problem(BaseModel):
15-
"""RFC 9457 problem detail - see https://pinboard.in/u:brunns/t:rfc-9457/"""
16-
17-
type: str | None = None
18-
title: str | None = None
19-
status: int | None = None
20-
detail: str | None = None
21-
instance: str | None = None
22-
errors: list[Error] | None = None
23-
24-
254
class EligibilityResponse(BaseModel):
265
processed_suggestions: list[dict]

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from brunns.matchers.data import json_matching as is_json_that
1111
from brunns.matchers.response import is_response
1212
from faker import Faker
13-
from hamcrest import assert_that, contains_string, has_entries, has_item
13+
from hamcrest import assert_that, contains_exactly, contains_string, has_entries, has_item
1414
from yarl import URL
1515

1616
from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode
@@ -94,9 +94,14 @@ def test_install_and_call_flask_lambda_with_unknown_nhs_number(
9494
.and_body(
9595
is_json_that(
9696
has_entries(
97-
title="nhs_number not found",
98-
detail=f"nhs_number {nhs_number} not found.",
99-
status=HTTPStatus.NOT_FOUND,
97+
resourceType="OperationOutcome",
98+
issue=contains_exactly(
99+
has_entries(
100+
severity="information",
101+
code="nhs-number-not-found",
102+
diagnostics=f'NHS Number "{nhs_number}" not found.',
103+
)
104+
),
100105
)
101106
)
102107
),

tests/unit/views/test_eligibility.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from brunns.matchers.werkzeug import is_werkzeug_response as is_response
66
from flask import Flask
77
from flask.testing import FlaskClient
8-
from hamcrest import assert_that, has_entries
8+
from hamcrest import assert_that, contains_exactly, has_entries
99
from wireup.integration.flask import get_app_container
1010

1111
from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
@@ -58,11 +58,39 @@ def test_no_nhs_number_given(app: Flask, client: FlaskClient):
5858
response = client.get("/eligibility/")
5959

6060
# Then
61-
assert_that(response, is_response().with_status_code(HTTPStatus.NOT_FOUND))
61+
assert_that(
62+
response,
63+
is_response()
64+
.with_status_code(HTTPStatus.NOT_FOUND)
65+
.and_text(
66+
is_json_that(
67+
has_entries(
68+
resourceType="OperationOutcome",
69+
issue=contains_exactly(
70+
has_entries(
71+
severity="information", code="nhs-number-not-found", diagnostics='NHS Number "" not found.'
72+
)
73+
),
74+
)
75+
)
76+
),
77+
)
6278

6379

6480
def test_unexpected_error(app: Flask, client: FlaskClient):
6581
# Given
6682
with get_app_container(app).override.service(EligibilityService, new=FakeUnexpectedErrorEligibilityService()):
6783
response = client.get("/eligibility/?nhs_number=12345")
68-
assert_that(response, is_response().with_status_code(HTTPStatus.INTERNAL_SERVER_ERROR))
84+
assert_that(
85+
response,
86+
is_response()
87+
.with_status_code(HTTPStatus.INTERNAL_SERVER_ERROR)
88+
.and_text(
89+
is_json_that(
90+
has_entries(
91+
resourceType="OperationOutcome",
92+
issue=contains_exactly(has_entries(severity="severe", code="unexpected")),
93+
)
94+
)
95+
),
96+
)

0 commit comments

Comments
 (0)