Skip to content

Commit a0070df

Browse files
NPA-4511: Sandbox for POST Consent (#147)
* NPA-4511: Updated Makefile to check consent endpoint schemas * NPA-4511: Added sandbox data for post consent * NPA-4511: Added post consent to validate workflow * NPA-4511: Corrected paths in schema
1 parent 7fe091d commit a0070df

20 files changed

+511
-377
lines changed

.github/workflows/openapi-validate.yml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,33 @@ jobs:
2929
3030
- name: Run Python script for all files
3131
run: |
32-
make schema-consent
32+
make schema-get-consent
33+
34+
POST_Consent:
35+
name: POST Consent test
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Checkout repository
39+
uses: actions/checkout@v4
40+
41+
- name: Set up Python
42+
uses: actions/setup-python@v5
43+
with:
44+
python-version: 3.9
45+
46+
- name: Install Poetry
47+
shell: bash
48+
run: |
49+
pipx install poetry==1.8.5
50+
51+
- name: Install Script Packages with Poetry
52+
shell: bash
53+
run: |
54+
poetry install --all-extras
55+
56+
- name: Run Python script for all files
57+
run: |
58+
make schema-post-consent
3359
3460
GET_RelatedPerson:
3561
name: GET Related Person test
@@ -83,7 +109,6 @@ jobs:
83109
run: |
84110
make schema-questionnaire
85111
86-
87112
Errors:
88113
name: Error schema test
89114
runs-on: ubuntu-latest

Makefile

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ lint:
2222
find . -name '*.py' -not -path '**/.venv/*' | xargs poetry run flake8
2323

2424
format:
25-
find . -name '*.py' -not -path '**/.venv/*' -not -path './sandbox/api/constants.py' | xargs poetry run black --check
25+
find . -name '*.py' -not -path '**/.venv/*' | xargs poetry run black --check --line-length 120
2626

2727
#Removes build/ + dist/ directories
2828
clean:
@@ -91,18 +91,34 @@ RESET := \033[0m
9191

9292

9393
schema-all:
94-
make schema-consent \
94+
make schema-get-consent \
95+
schema-post-consent \
96+
schema-patch-consent \
9597
schema-related-person \
9698
schema-questionnaire \
9799
schema-errors
98100

99-
schema-consent:
101+
schema-get-consent:
100102
@for file in specification/examples/responses/GET_Consent/*.yaml; do \
101103
echo "Processing $$file"; \
102104
poetry run python scripts/validate_schema.py consent "$$(realpath $$file)"; \
103105
echo -e "$(GREEN)Success!$(RESET)"; \
104106
done
105107

108+
schema-post-consent:
109+
@for file in specification/examples/responses/POST_Consent/*.yaml; do \
110+
echo "Processing $$file"; \
111+
poetry run python scripts/validate_schema.py operationoutcome "$$(realpath $$file)"; \
112+
echo -e "$(GREEN)Success!$(RESET)"; \
113+
done
114+
115+
schema-patch-consent:
116+
@for file in specification/examples/responses/PATCH_Consent/*.yaml; do \
117+
echo "Processing $$file"; \
118+
poetry run python scripts/validate_schema.py operationoutcome "$$(realpath $$file)"; \
119+
echo -e "$(GREEN)Success!$(RESET)"; \
120+
done
121+
106122
schema-related-person:
107123
@for file in specification/examples/responses/GET_RelatedPerson/*.yaml; do \
108124
echo "Processing $$file"; \

sandbox/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ load-examples: # Load the examples from the specification (for local development
1414
cp -r ../specification/examples/responses/ ./api/examples
1515

1616
format: # Formats the code using black
17-
poetry run black . --exclude ./api/constants.py
17+
poetry run black . --line-length 120
1818

1919
start: load-examples # Starts the Flask app (Don't use this in production)
2020
poetry run flask --app api.app:app run -p 9000

sandbox/api/app.py

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from logging import INFO, basicConfig, getLogger
22
from typing import Union
3-
43
from flask import Flask, request
54

65
from .constants import (
@@ -13,18 +12,24 @@
1312
RELATED__VERIFY_RELATIONSHIP_25,
1413
RELATED__VERIFY_RELATIONSHIP_09_WITH_INCLUDE,
1514
RELATED__VERIFY_RELATIONSHIP_25_WITH_INCLUDE,
16-
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP,
17-
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH,
18-
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP,
19-
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH,
20-
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH,
21-
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER,
22-
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT,
23-
CONSENT__MULTIPLE_RELATIONSHIPS,
24-
CONSENT__NO_RELATIONSHIPS,
25-
CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE,
26-
CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE,
27-
CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE,
15+
GET_CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP,
16+
GET_CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH,
17+
GET_CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP,
18+
GET_CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH,
19+
GET_CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH,
20+
GET_CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER,
21+
GET_CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT,
22+
GET_CONSENT__MULTIPLE_RELATIONSHIPS,
23+
GET_CONSENT__NO_RELATIONSHIPS,
24+
GET_CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE,
25+
GET_CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE,
26+
GET_CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE,
27+
POST_CONSENT__SUCCESS,
28+
POST_CONSENT__DUPLICATE_RELATIONSHIP_ERROR,
29+
POST_CONSENT__INVALID_ACCESS_LEVEL_ERROR,
30+
POST_CONSENT__INVALID_EVIDENCE_ERROR,
31+
POST_CONSENT__INVALID_PATIENT_AGE_ERROR,
32+
POST_CONSENT__PERFORMER_IDENTIFIER_ERROR,
2833
)
2934
from .utils import (
3035
check_for_empty,
@@ -41,6 +46,7 @@
4146
app = Flask(__name__)
4247
basicConfig(level=INFO, format="%(asctime)s - %(message)s")
4348
logger = getLogger(__name__)
49+
APP_BASE_PATH = "https://sandbox.api.service.nhs.uk/validated-relationships/FHIR/R4/Consent"
4450
COMMON_PATH = "FHIR/R4"
4551

4652

@@ -107,8 +113,8 @@ def get_related_persons() -> Union[dict, tuple]:
107113

108114
raise ValueError("Invalid request")
109115

110-
except Exception as e:
111-
logger.error(e)
116+
except Exception:
117+
logger.exception("GET related person failed")
112118
return generate_response_from_example(INTERNAL_SERVER_ERROR_EXAMPLE, 500)
113119

114120

@@ -122,8 +128,8 @@ def post_questionnaire_response() -> Union[dict, tuple]:
122128

123129
try:
124130
return generate_response_from_example(QUESTIONNAIRE_RESPONSE__SUCCESS, 200)
125-
except Exception as e:
126-
logger.error(e)
131+
except Exception:
132+
logger.exception("POST questionnaire response failed")
127133
return generate_response_from_example(INTERNAL_SERVER_ERROR_EXAMPLE, 500)
128134

129135

@@ -147,40 +153,92 @@ def get_consent() -> Union[dict, tuple]:
147153
if performer_identifier == "9000000010":
148154
return check_for_consent_include_params(
149155
_include,
150-
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP,
151-
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH,
156+
GET_CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP,
157+
GET_CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH,
152158
)
153159
# Single mother child relationship
154160
elif performer_identifier == "9000000019":
155161
return check_for_consent_include_params(
156162
_include,
157-
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP,
158-
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH,
163+
GET_CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP,
164+
GET_CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH,
159165
)
160166
# Filtering
161167
elif performer_identifier == "9000000017":
162168
return check_for_consent_filtering(
163169
status,
164170
_include,
165-
CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE,
166-
CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE,
167-
CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE,
171+
GET_CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE,
172+
GET_CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE,
173+
GET_CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE,
168174
)
169175
elif performer_identifier == "9000000022":
170176
return check_for_consent_include_params(
171177
_include,
172-
CONSENT__MULTIPLE_RELATIONSHIPS,
173-
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH,
174-
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT,
175-
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER,
178+
GET_CONSENT__MULTIPLE_RELATIONSHIPS,
179+
GET_CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH,
180+
GET_CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT,
181+
GET_CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER,
176182
)
177183
# No relationships
178184
elif performer_identifier == "9000000025":
179-
return generate_response_from_example(CONSENT__NO_RELATIONSHIPS, 200)
185+
return generate_response_from_example(GET_CONSENT__NO_RELATIONSHIPS, 200)
180186
else:
181187
logger.error("Performer identifier does not match examples")
182188
return generate_response_from_example(INVALIDATED_RESOURCE, 404)
183189

184-
except Exception as e:
185-
logger.error(e)
190+
except Exception:
191+
logger.exception("GET Consent failed")
192+
return generate_response_from_example(INTERNAL_SERVER_ERROR_EXAMPLE, 500)
193+
194+
195+
@app.route(f"/{COMMON_PATH}/Consent", methods=["POST"])
196+
def post_consent() -> Union[dict, tuple]:
197+
"""Sandbox API for POST /Consent
198+
199+
Returns:
200+
Union[dict, tuple]: Response for POST /Consent
201+
"""
202+
try:
203+
logger.debug("Received request to POST consent")
204+
# Validate body - beyond the scope of sandbox - assume body is valid for scenario
205+
json = request.get_json()
206+
patient_identifier = json["performer"][0]["identifier"]["value"]
207+
response = None
208+
209+
# Successful parent-child proxy creation
210+
# Successful adult-adult proxy creation
211+
if patient_identifier == "9000000009" or patient_identifier == "9000000017":
212+
header = {"location": f"{APP_BASE_PATH}/{patient_identifier}"}
213+
response = generate_response_from_example(POST_CONSENT__SUCCESS, 201, headers=header)
214+
215+
# Invalid access level
216+
elif patient_identifier == "9000000025":
217+
response = generate_response_from_example(POST_CONSENT__INVALID_ACCESS_LEVEL_ERROR, 400)
218+
219+
# Missing required evidence
220+
elif patient_identifier == "9000000033":
221+
response = generate_response_from_example(POST_CONSENT__INVALID_EVIDENCE_ERROR, 422)
222+
223+
# Patient age validation failure
224+
elif patient_identifier == "9000000041":
225+
response = generate_response_from_example(POST_CONSENT__INVALID_PATIENT_AGE_ERROR, 422)
226+
227+
# Duplicate relationship
228+
elif patient_identifier == "9000000049":
229+
response = generate_response_from_example(POST_CONSENT__DUPLICATE_RELATIONSHIP_ERROR, 409)
230+
231+
# Invalid performer NHS number
232+
elif patient_identifier == "9000000000":
233+
response = generate_response_from_example(POST_CONSENT__PERFORMER_IDENTIFIER_ERROR, 400)
234+
235+
else:
236+
# Out of scope errors
237+
raise ValueError("Invalid Request")
238+
239+
return response
240+
241+
except Exception:
242+
# Handle any general error
243+
logger.exception("POST Consent failed")
186244
return generate_response_from_example(INTERNAL_SERVER_ERROR_EXAMPLE, 500)

0 commit comments

Comments
 (0)