Skip to content

Commit 510b228

Browse files
authored
NPA-3566 Update Sandbox and Postman to include new /Consent endpoint (#135)
* feat: added json responses for consent * feat: wip consent responses as json * feat: working responses * feat: yaml examples wip * feat: working examples * fix: formatting * test: updated postman collection * fix: resolved logger security warning * refactor: formatting updates * fix: resolved inconsistency with active and inactive * chore: updated OAS * test: wip sandbox unit test fixes * test: passing all tests * refactor: updated formatting * refactor: formatting updates * test: updated tests * docs: updated oas and postman collection * refactor: formatting fixes * chore: updated pipelines to run postman collection separately * chore: updated sandbox checks workflows * chore: added sandbox dev variables file * chore: updated workflow * chore: ensures that flask app is stopped * chore: further updates to sandbox checks workflow * chore: reverted changes to workflows and updated pr template * fix: updated examples * tests: updated postman collection and tests * fix: wip updates to params * fix: updates to sandbox conditionals * chore: updated OAS and postman collection * refactor: formatting updated * refactor: updated test utils and formatted constants * refactor: further formatting changes * refactor: updated long lines in constants * fix: updated totals in examples for consent * test: updated postman collection tests * chore: excluded constants from formatting checks * chore: reinstated working directory on pipeline step * chore: updated main makefile * chore: updated example naming * fix: updated list[str] to List[str] * refactor: removed policy property from examples and postman collection tests * docs: updated postman collection link
1 parent 740ff01 commit 510b228

36 files changed

+5503
-744
lines changed

.github/pull_request_template.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Pull Request Checklist
1+
# Pull Request Checklist
22

33
## Ticket Link
44

@@ -14,7 +14,7 @@ https://nhsd-jira.digital.nhs.uk/browse/NPA-XXXX
1414
-
1515
-
1616

17-
# How to test?
17+
## How to test?
1818

1919
<!--- Describe in detail how you tested your changes -->
2020
<!--- Include details of your testing environment and the tests you ran to see how your change affects other areas of the code etc. -->
@@ -37,3 +37,9 @@ Stages to complete before opening the Pull Request:
3737
- [ ] I have reviewed the changes in this PR and they fill all or part of the acceptance criteria of the ticket, and the code is in a mergeable state.
3838
- [ ] If there were infrastructure, operational, or build changes, I have made sure there is sufficient evidence that the changes will work.
3939
- [ ] I have ensured the changelog has been updated by the submitter, if necessary.
40+
41+
## Post-merge
42+
43+
After merging and deploying changes to the sandbox, Postman collection or spec examples please run the `Run Postman collection` workflow.
44+
45+
This will run the tests within the collection to check that the sandbox is working as expected once deployed.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Run postman collection
2+
on:
3+
workflow_dispatch:
4+
jobs:
5+
run-postman-collection:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- name: Check out the repository
9+
uses: actions/checkout@v4
10+
- name: Set up Node.js
11+
uses: actions/setup-node@v4
12+
with:
13+
node-version: '22'
14+
- name: Install Newman
15+
run: npm install -g newman
16+
- name: Run Postman Collection
17+
run: newman run ./postman/Validate_Relationship_Service_Sandbox.postman_collection.json

.github/workflows/sandbox-checks.yaml

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,3 @@ jobs:
2424
- name: Run Sandbox Unit Tests
2525
run: make test
2626
working-directory: ./sandbox
27-
28-
run-postman-collection:
29-
runs-on: ubuntu-latest
30-
steps:
31-
- name: Check out the repository
32-
uses: actions/checkout@v4
33-
- name: Set up Node.js
34-
uses: actions/setup-node@v4
35-
with:
36-
node-version: '22'
37-
- name: Install Newman
38-
run: npm install -g newman
39-
- name: Run Postman Collection
40-
run: newman run ./postman/Validate_Relationship_Service_Sandbox.postman_collection.json

Makefile

Lines changed: 1 addition & 1 deletion
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/*' | xargs poetry run black --check
25+
find . -name '*.py' -not -path '**/.venv/*' -not -path './sandbox/api/constants.py' | xargs poetry run black --check
2626

2727
#Removes build/ + dist/ directories
2828
clean:

postman/Validate_Relationship_Service_Sandbox.postman_collection.json

Lines changed: 3599 additions & 395 deletions
Large diffs are not rendered by default.

postman/dev-variables.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
{
3+
"api_base_url": "http://127.0.0.1:9000"
4+
}
5+
]

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 .
17+
poetry run black . --exclude ./api/constants.py
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: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,41 @@
44
from flask import Flask, request
55

66
from .constants import (
7-
CONSENT__ADULT_CONSENTING_EXAMPLE,
8-
CONSENT__MIXED_EXAMPLE,
9-
CONSENT__MOTHER_CHILD_EXAMPLE,
10-
CONSENT_PERFORMER,
117
INTERNAL_ERROR_RESPONSE,
128
INTERNAL_SERVER_ERROR_EXAMPLE,
139
LIST_RELATIONSHIP,
1410
LIST_RELATIONSHIP_INCLUDE,
15-
NOT_FOUND,
11+
INVALIDATED_RESOURCE,
1612
QUESTIONNAIRE_RESPONSE_SUCCESS,
1713
VALIDATE_RELATIONSHIP_009,
1814
VALIDATE_RELATIONSHIP_025,
1915
VALIDATE_RELATIONSHIP_INCLUDE_009,
2016
VALIDATE_RELATIONSHIP_INCLUDE_025,
17+
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP,
18+
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH,
19+
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP,
20+
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH,
21+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH,
22+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER,
23+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT,
24+
CONSENT__MULTIPLE_RELATIONSHIPS,
25+
CONSENT__NO_RELATIONSHIPS,
26+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE,
27+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE,
28+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE,
2129
)
2230
from .utils import (
2331
check_for_empty,
24-
check_for_errors,
32+
check_for_consent_errors,
33+
check_for_related_person_errors,
2534
check_for_list,
2635
check_for_validate,
2736
generate_response,
2837
generate_response_from_example,
2938
load_json_file,
3039
remove_system,
40+
check_for_consent_include_params,
41+
check_for_consent_filtering,
3142
)
3243

3344
app = Flask(__name__)
@@ -57,7 +68,7 @@ def get_related_persons() -> Union[dict, tuple]:
5768

5869
try:
5970
# Check Headers
60-
if errors := check_for_errors(request):
71+
if errors := check_for_related_person_errors(request):
6172
return errors
6273

6374
identifier = remove_system(request.args.get("identifier"))
@@ -127,32 +138,52 @@ def get_consent() -> Union[dict, tuple]:
127138
Union[dict, tuple]: Response for GET /Consent
128139
"""
129140
try:
130-
performer_identifier = remove_system(request.args.get("performer:identifier"))
131-
status = request.args.get("status")
132-
_include = request.args.get("_include")
141+
# Check Headers
142+
if errors := check_for_consent_errors(request):
143+
return errors
133144

134-
if (
135-
performer_identifier == "9000000010"
136-
and status == "active"
137-
and _include == CONSENT_PERFORMER
138-
):
139-
return generate_response_from_example(
140-
CONSENT__ADULT_CONSENTING_EXAMPLE, 200
145+
performer_identifier = remove_system(request.args.get("performer:identifier"))
146+
status = request.args.getlist("status")
147+
_include = request.args.getlist("_include")
148+
149+
# Single consenting adult relationship
150+
if performer_identifier == "9000000010":
151+
return check_for_consent_include_params(
152+
_include,
153+
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP,
154+
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH,
141155
)
142-
elif (
143-
performer_identifier == "9000000017"
144-
and status == "active"
145-
and _include == CONSENT_PERFORMER
146-
):
147-
return generate_response_from_example(CONSENT__MIXED_EXAMPLE, 200)
148-
elif (
149-
performer_identifier == "9000000019"
150-
and status == "active"
151-
and _include == CONSENT_PERFORMER
152-
):
153-
return generate_response_from_example(CONSENT__MOTHER_CHILD_EXAMPLE, 200)
156+
# Single mother child relationship
157+
elif performer_identifier == "9000000019":
158+
return check_for_consent_include_params(
159+
_include,
160+
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP,
161+
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH,
162+
)
163+
# Filtering
164+
elif performer_identifier == "9000000017":
165+
return check_for_consent_filtering(
166+
status,
167+
_include,
168+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE,
169+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE,
170+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE,
171+
)
172+
elif performer_identifier == "9000000022":
173+
return check_for_consent_include_params(
174+
_include,
175+
CONSENT__MULTIPLE_RELATIONSHIPS,
176+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH,
177+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT,
178+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER,
179+
)
180+
# No relationships
181+
elif performer_identifier == "9000000025":
182+
return generate_response_from_example(CONSENT__NO_RELATIONSHIPS, 200)
154183
else:
155-
return generate_response(load_json_file(NOT_FOUND), 400)
184+
logger.error("Performer identifier does not match examples")
185+
return generate_response_from_example(INVALIDATED_RESOURCE, 404)
186+
156187
except Exception as e:
157188
logger.error(e)
158189
return generate_response_from_example(INTERNAL_SERVER_ERROR_EXAMPLE, 500)

sandbox/api/constants.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
NOT_FOUND = "./api/responses/not_found.json"
21
EMPTY_RESPONSE = "./api/responses/GET_RelatedPerson/empty_response_9000000033.json"
32
LIST_RELATIONSHIP = (
43
"./api/responses/GET_RelatedPerson/list_relationship_9000000017.json"
@@ -29,12 +28,53 @@
2928
RELATED_IDENTIFIERS = ["9000000009", "9000000025"]
3029

3130
CONSENT_PERFORMER = "Consent:performer"
31+
CONSENT_PATIENT = "Consent:patient"
3232

3333
# Example files
3434

3535
# Common examples
3636
INTERNAL_SERVER_ERROR_EXAMPLE = "./api/examples/errors/internal-server-error.yaml"
37+
BAD_REQUEST_INCLUDE_PARAM_INVALID = (
38+
"./api/examples/errors/invalid-include-parameter.yaml"
39+
)
40+
INVALIDATED_RESOURCE = "./api/examples/errors/invalidated-resource.yaml"
41+
MISSING_IDENTIFIER = "./api/examples/errors/missing-identifier.yaml"
42+
INVALID_IDENTIFIER = "./api/examples/errors/invalid-identifier.yaml"
43+
3744
# Consent examples
38-
CONSENT__ADULT_CONSENTING_EXAMPLE = "./api/examples/GET_Consent/adults-consenting.yaml"
39-
CONSENT__MIXED_EXAMPLE = "./api/examples/GET_Consent/mixed.yaml"
40-
CONSENT__MOTHER_CHILD_EXAMPLE = "./api/examples/GET_Consent/mother-child.yaml"
45+
CONSENT__DIRECTORY = "./api/examples/GET_Consent/"
46+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_ACTIVE = (
47+
f"{CONSENT__DIRECTORY}filtered-relationships-status-active-include-details.yaml"
48+
)
49+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_INACTIVE = (
50+
f"{CONSENT__DIRECTORY}filtered-relationships-status-inactive.yaml"
51+
)
52+
CONSENT__FILTERED_RELATIONSHIPS_STATUS_PROPOSED_ACTIVE = (
53+
f"{CONSENT__DIRECTORY}filtered-relationships-status-proposed-active.yaml"
54+
)
55+
CONSENT__MULTIPLE_RELATIONSHIPS = f"{CONSENT__DIRECTORY}multiple-relationships.yaml"
56+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_BOTH = (
57+
f"{CONSENT__DIRECTORY}multiple-relationships-include-performer-patient.yaml"
58+
)
59+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PATIENT = (
60+
f"{CONSENT__DIRECTORY}multiple-relationships-include-patient.yaml"
61+
)
62+
CONSENT__MULTIPLE_RELATIONSHIPS_INCLUDE_PERFORMER = (
63+
f"{CONSENT__DIRECTORY}multiple-relationships-include-performer.yaml"
64+
)
65+
CONSENT__NO_RELATIONSHIPS = f"{CONSENT__DIRECTORY}no-relationships.yaml"
66+
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP = (
67+
f"{CONSENT__DIRECTORY}single-consenting-adult-relationship.yaml"
68+
)
69+
CONSENT__SINGLE_CONSENTING_ADULT_RELATIONSHIP_INCLUDE_BOTH = (
70+
f"{CONSENT__DIRECTORY}single-consenting-adult-relationship-include-performer-patient.yaml"
71+
)
72+
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP = (
73+
f"{CONSENT__DIRECTORY}single-mother-child-relationship.yaml"
74+
)
75+
CONSENT__SINGLE_MOTHER_CHILD_RELATIONSHIP_INCLUDE_BOTH = (
76+
f"{CONSENT__DIRECTORY}single-mother-child-relationship-include-performer-patient.yaml"
77+
)
78+
CONSENT__STATUS_PARAM_INVALID = (
79+
f"{CONSENT__DIRECTORY}errors/invalid-status-parameter.yaml"
80+
)

sandbox/api/tests/test_app.py

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from json import dumps
1+
from json import dumps, loads
22
from unittest.mock import MagicMock, patch
33

44
import pytest
@@ -30,16 +30,6 @@ def test_health_check(client: object, endpoint: str) -> None:
3030
@pytest.mark.parametrize(
3131
"request_args,response_file_name,status_code",
3232
[
33-
(
34-
"identifier=9000000041",
35-
"./api/responses/not_found.json",
36-
404,
37-
),
38-
(
39-
"identifier=9000000017&patient:identifier=9000000041",
40-
"./api/responses/not_found.json",
41-
404,
42-
),
4333
(
4434
"identifier=9000000033",
4535
"./api/responses/GET_RelatedPerson/empty_response_9000000033.json",
@@ -144,19 +134,14 @@ def test_questionnaire_response(
144134
("request_args,response_file_name,status_code"),
145135
[
146136
(
147-
"performer:identifier=9000000010&status=active&_include=Consent:performer",
148-
"./api/examples/GET_Consent/adults-consenting.yaml",
137+
"performer:identifier=9000000025", # No performer record found error
138+
"./api/examples/GET_Consent/no-relationships.yaml",
149139
200,
150140
),
151141
(
152-
"performer:identifier=9000000017&status=active&_include=Consent:performer",
153-
"./api/examples/GET_Consent/mixed.yaml",
154-
200,
155-
),
156-
(
157-
"performer:identifier=9000000019&status=active&_include=Consent:performer",
158-
"./api/examples/GET_Consent/mother-child.yaml",
159-
200,
142+
"performer:identifier=9000000999", # No performer record found error
143+
"./api/examples/errors/invalidated-resource.yaml",
144+
404,
160145
),
161146
],
162147
)
@@ -165,33 +150,21 @@ def test_consent(
165150
mock_generate_response_from_example: MagicMock,
166151
request_args: str,
167152
response_file_name: str,
168-
client: object,
169153
status_code: int,
154+
client: object,
170155
) -> None:
171156
"""Test Consent endpoint."""
172-
mock_generate_response_from_example.return_value = mock_response = Response(
173-
dumps({"data": "abc"}), status_code
157+
mock_generate_response_from_example.return_value = mocked_response = Response(
158+
dumps({"data": "mocked"}), status=status_code, content_type="application/json"
174159
)
175160
# Act
176161
response = client.get(f"{CONSENT_API_ENDPOINT}?{request_args}")
177162
# Assert
178-
mock_generate_response_from_example.assert_called_once_with(response_file_name, 200)
179-
assert response.status_code == status_code
180-
assert response.json == mock_response.json
181-
182-
183-
@patch(f"{APP_FILE_PATH}.load_json_file")
184-
def test_consent__400_bad_request(
185-
mock_load_json_file: MagicMock, client: object
186-
) -> None:
187-
"""Test Consent endpoint."""
188-
mock_load_json_file.return_value = {"data": "mocked"}
189-
# Act
190-
client.get(
191-
f"{CONSENT_API_ENDPOINT}?performer:identifier=9000000012&status=active&_include=Consent:performer"
163+
mock_generate_response_from_example.assert_called_once_with(
164+
response_file_name, status_code
192165
)
193-
# Assert
194-
mock_load_json_file.assert_called_once_with("./api/responses/not_found.json")
166+
assert response.status_code == status_code
167+
assert response.json == loads(mocked_response.get_data(as_text=True))
195168

196169

197170
@patch(f"{APP_FILE_PATH}.remove_system")

0 commit comments

Comments
 (0)