Skip to content

Commit cab6ad1

Browse files
authored
NPA-5534: OAS, Sandbox, and Postman updates for GET Questionnaire Response routing (#262)
# Pull Request ## 🧾 Ticket Link <!-- Add the Jira ticket link here --> https://nhsd-jira.digital.nhs.uk/browse/NPA-5534 --- ## 📄 Description/Summary of Changes <!-- Describe the changes made in this PR. Include the purpose/scope/impact of the changes --> - Updated OAS to use path parameter instead of query parameter for GET Questionnaire Response - Update routing of GET Questionnaire Response in sandbox - Updated sandbox and postman tests to use new routing - Updated examples for OAS --- ## 🧪 Developer Testing Carried Out <!-- Describe what tests (automated/unit/manual etc.) have been done for the ticket. Include: --> <!-- - Any tests added/updated --> <!-- - Evidence that each acceptance criterion from the Jira ticket is met --> - Ran Postman collection and sandbox tests - Reviewed OAS following changes --- ## 🧪 Reviewer Testing Required <!-- Describe how to test the changes that have been made in the ticket. Include: --> <!-- - Testing environment details (e.g. sandbox/local setup) --> <!-- - Steps to verify the changes --> - [ ] Review OAS - [ ] Run Postman and sandbox tests --- ## ✅ Developer Checklist <!-- Complete before submitting the PR --> - [x] PR title follows the format: `NPA-XXXX: <short-description>` - [x] Branch name follows the convention: `<type>/NPA-XXXX/<short-description>` - [x] Commit messages follow the template: `NPA-XXXX: <short-description>` - [x] All acceptance criteria from the Jira ticket are addressed - [x] Automated tests (unit/integration/API/infrastructure etc. tests) are added or updated - [x] Assignees and appropriate labels (e.g. `terraform`, `documentation`) are added --- ## 👀 Reviewer Checklist <!-- To be completed by the reviewer --> - [ ] Changes meet the acceptance criteria of the Jira ticket - [ ] Code is able to be merged (no conflicts and adheres to coding standards) - [ ] Sufficient test evidence is provided (manual and/or automated) - [ ] Infrastructure/operational/build changes are validated (if applicable) --- ## 🚀 Post-merge <!-- Actions to complete after merging --> After merging and deploying changes to the sandbox, Postman collection or spec examples please run the Run Postman collection workflow. This will run the tests within the collection to check that the sandbox is working as expected once deployed. --------- Signed-off-by: adamclarkson <[email protected]>
1 parent 9faccaf commit cab6ad1

9 files changed

+574
-220
lines changed

postman/Validated Relationship Service Sandbox.postman_collection.json

Lines changed: 459 additions & 173 deletions
Large diffs are not rendered by default.

sandbox/api/app.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
from .get_consent import get_consent_response
77
from .get_consent_by_id import get_consent_by_id_response
8-
from .get_questionnaire_response import get_questionnaire_response_response
8+
from .get_questionnaire_response_by_path_id import get_questionnaire_response_by_path_id_response
99
from .get_related_person import get_related_person_response
1010
from .patch_consent import patch_consent_response
1111
from .post_consent import post_consent_response
1212
from .post_questionnaire_response import post_questionnaire_response_response
13+
from .utils import generate_response_from_example
14+
from .constants import METHOD_NOT_ALLOWED
1315

1416
app = Flask(__name__)
1517
basicConfig(level=INFO, format="%(asctime)s - %(message)s")
@@ -40,13 +42,24 @@ def get_related_persons() -> Union[dict, tuple]:
4042

4143

4244
@app.route(f"/{COMMON_PATH}/QuestionnaireResponse", methods=["GET"])
45+
@app.route(f"/{COMMON_PATH}/QuestionnaireResponse/", methods=["GET"])
4346
def get_questionnaire_response() -> Union[dict, tuple]:
4447
"""Sandbox API for GET /QuestionnaireResponse
4548
4649
Returns:
4750
Union[dict, tuple]: Response for GET /QuestionnaireResponse
4851
"""
49-
return get_questionnaire_response_response()
52+
return generate_response_from_example(METHOD_NOT_ALLOWED, 405)
53+
54+
55+
@app.route(f"/{COMMON_PATH}/QuestionnaireResponse/<identifier>", methods=["GET"])
56+
def get_questionnaire_response_id(identifier: str) -> Union[dict, tuple]:
57+
"""Sandbox API for GET /QuestionnaireResponse
58+
59+
Returns:
60+
Union[dict, tuple]: Response for GET /QuestionnaireResponse
61+
"""
62+
return get_questionnaire_response_by_path_id_response(identifier)
5063

5164

5265
@app.route(f"/{COMMON_PATH}/QuestionnaireResponse", methods=["POST"])

sandbox/api/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
INVALIDATED_RESOURCE = "./api/examples/errors/invalidated-resource.yaml"
1515
MISSING_IDENTIFIER = "./api/examples/errors/missing-identifier.yaml"
1616
INVALID_IDENTIFIER = "./api/examples/errors/invalid-identifier.yaml"
17+
METHOD_NOT_ALLOWED = "./api/examples/errors/method-not-allowed.yaml"
1718

1819
# GET Consent examples
1920
GET_CONSENT__DIRECTORY = "./api/examples/GET_Consent/"

sandbox/api/get_questionnaire_response.py renamed to sandbox/api/get_questionnaire_response_by_path_id.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
11
from logging import INFO, basicConfig, getLogger
22
from typing import Union
33

4-
from flask import request
5-
64
from .constants import (
75
GET_QUESTIONNAIRE_RESPONSE__INVALID,
8-
GET_QUESTIONNAIRE_RESPONSE__MISSING,
96
GET_QUESTIONNAIRE_RESPONSE__NOT_FOUND,
107
GET_QUESTIONNAIRE_RESPONSE__SUCCESS,
118
INTERNAL_SERVER_ERROR_EXAMPLE,
9+
METHOD_NOT_ALLOWED,
1210
)
1311
from .utils import generate_response_from_example
1412

1513
basicConfig(level=INFO, format="%(asctime)s - %(message)s")
1614
logger = getLogger(__name__)
1715

1816

19-
def get_questionnaire_response_response() -> Union[dict, tuple]:
20-
"""Sandbox API for GET /QuestionnaireResponse
17+
def get_questionnaire_response_by_path_id_response(access_request_id: str) -> Union[dict, tuple]:
18+
"""Sandbox API for GET /QuestionnaireResponse/{id}
2119
2220
Returns:
23-
Union[dict, tuple]: Response for GET /QuestionnaireResponse
21+
Union[dict, tuple]: Response for GET /QuestionnaireResponse/{id}
2422
"""
2523
try:
26-
access_request_id = request.args.get("ID")
2724
if access_request_id == "156e1560-e532-4e2a-85ad-5aeff03dc43e":
2825
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__SUCCESS, 200)
2926
elif access_request_id == "INVALID":
3027
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__INVALID, 400)
3128
elif access_request_id == "" or access_request_id is None:
32-
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__MISSING, 400)
29+
return generate_response_from_example(METHOD_NOT_ALLOWED, 405)
3330
elif access_request_id == "60d09b82-f4bb-41f9-b41e-767999b4ac9b":
3431
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__NOT_FOUND, 404)
3532
else:

sandbox/api/tests/test_get_questionnaire_response.py renamed to sandbox/api/tests/test_get_questionnaire_response_by_path_id.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,68 @@
77

88

99
@pytest.mark.parametrize(
10-
("request_args", "response_file_name", "status_code"),
10+
("path", "response_file_name", "status_code"),
1111
[
1212
(
13-
"ID=156e1560-e532-4e2a-85ad-5aeff03dc43e",
13+
"/156e1560-e532-4e2a-85ad-5aeff03dc43e",
1414
"./api/examples/GET_QuestionnaireResponse/success.yaml",
1515
200,
1616
),
1717
(
18-
"ID=INVALID",
18+
"/INVALID",
1919
"./api/examples/GET_QuestionnaireResponse/errors/invalid_access_request_id.yaml",
2020
400,
2121
),
2222
(
23-
"ID=",
24-
"./api/examples/GET_QuestionnaireResponse/errors/missing_access_request_id.yaml",
25-
400,
26-
),
27-
(
28-
"ID=60d09b82-f4bb-41f9-b41e-767999b4ac9b",
23+
"/60d09b82-f4bb-41f9-b41e-767999b4ac9b",
2924
"./api/examples/GET_QuestionnaireResponse/errors/questionnaire_response_not_found.yaml",
3025
404,
3126
),
3227
(
33-
"ID=INVALID_CODE",
28+
"/INVALID_CODE",
3429
"./api/examples/errors/internal-server-error.yaml",
3530
500,
3631
),
3732
],
3833
)
39-
@patch("sandbox.api.get_questionnaire_response.generate_response_from_example")
40-
def test_get_questionnaire_response_returns_expected_responses__mocked_utils(
34+
@patch("sandbox.api.get_questionnaire_response_by_path_id.generate_response_from_example")
35+
def test_get_questionnaire_response_id_returns_expected_responses__mocked_utils(
4136
mock_generate_response_from_example: MagicMock,
42-
request_args: str,
37+
path: str,
4338
response_file_name: str,
4439
status_code: int,
4540
client: object,
4641
) -> None:
47-
"""Test GET Consent endpoint."""
42+
"""Test GET /QuestionnaireResponse/{id} endpoint."""
4843
mock_generate_response_from_example.return_value = mocked_response = Response(
4944
dumps({"data": "mocked"}),
5045
status=status_code,
5146
content_type="application/json",
5247
)
5348
# Act
54-
response = client.get(f"{GET_QUESTIONNAIRE_RESPONSE_API_ENDPOINT}?{request_args}")
49+
response = client.get(f"{GET_QUESTIONNAIRE_RESPONSE_API_ENDPOINT}{path}")
5550
# Assert
5651
mock_generate_response_from_example.assert_called_once_with(response_file_name, status_code)
5752
assert response.status_code == status_code
5853
assert response.json == loads(mocked_response.get_data(as_text=True))
54+
55+
56+
@pytest.mark.parametrize("path", ["/", ""])
57+
@patch("sandbox.api.app.generate_response_from_example")
58+
def test_get_questionnaire_response_without_path_params_return_405_errors(
59+
mock_generate_response_from_example: MagicMock,
60+
path: str,
61+
client: object,
62+
) -> None:
63+
"""Test the GET /QuestionnaireResponse endpoint with no path params."""
64+
mock_generate_response_from_example.return_value = mocked_response = Response(
65+
dumps({"data": "mocked"}),
66+
status=405,
67+
content_type="application/json",
68+
)
69+
# Act
70+
response = client.get(f"{GET_QUESTIONNAIRE_RESPONSE_API_ENDPOINT}{path}")
71+
# Assert
72+
mock_generate_response_from_example.assert_called_once_with("./api/examples/errors/method-not-allowed.yaml", 405)
73+
assert response.status_code == 405
74+
assert response.json == loads(mocked_response.get_data(as_text=True))

sandbox/api/tests/test_patch_consent.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,27 @@
2323
("74eed847-ca25-4e76-8cf2-f2c2d7842a7a", PATCH_CONSENT__SUCCESS, 200),
2424
("6b71ac92-baa3-4b76-b0f5-a601257e2722", PATCH_CONSENT__SUCCESS, 200),
2525
("43003db8-ffcd-4bd6-ab2f-b49b9656f9e5", PATCH_CONSENT__SUCCESS, 200),
26-
("849ea584-2318-471b-a24c-cee1b5ad0137", PATCH_CONSENT__INVALID_PATCH_FORMAT, 400),
26+
(
27+
"849ea584-2318-471b-a24c-cee1b5ad0137",
28+
PATCH_CONSENT__INVALID_PATCH_FORMAT,
29+
400,
30+
),
2731
("01abb0c5-b1ac-499d-9655-9cd0b8d3588f", PATCH_CONSENT__INVALID_PATH, 400),
28-
("78c35330-fa2f-4934-a5dd-fff847f38de5", PATCH_CONSENT__INVALID_STATUS_CODE, 422),
29-
("51fb4df5-815a-45cd-8427-04d6558336b7", PATCH_CONSENT__INVALID_STATUS_REASON, 422),
30-
("7b7f47b8-96e5-43eb-b733-283bf1449f2c", PATCH_CONSENT__INVALID_STATE_TRANSITION, 422),
32+
(
33+
"78c35330-fa2f-4934-a5dd-fff847f38de5",
34+
PATCH_CONSENT__INVALID_STATUS_CODE,
35+
422,
36+
),
37+
(
38+
"51fb4df5-815a-45cd-8427-04d6558336b7",
39+
PATCH_CONSENT__INVALID_STATUS_REASON,
40+
422,
41+
),
42+
(
43+
"7b7f47b8-96e5-43eb-b733-283bf1449f2c",
44+
PATCH_CONSENT__INVALID_STATE_TRANSITION,
45+
422,
46+
),
3147
("xxxxxxxx", PATCH_CONSENT__RESOURCE_NOT_FOUND, 404),
3248
("12345678", PATCH_CONSENT__RESOURCE_NOT_FOUND, 404),
3349
],

sandbox/api/tests/test_post_questionnaire_response.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ def test_post_questionnaire_response(
5959
status=status_code,
6060
content_type="application/json",
6161
)
62-
json = {"resourceType": "QuestionnaireResponse", "source": {"identifier": {"value": nhs_num}}}
62+
json = {
63+
"resourceType": "QuestionnaireResponse",
64+
"source": {"identifier": {"value": nhs_num}},
65+
}
6366
# Act
6467
response = client.post(QUESTIONNAIRE_RESPONSE_API_ENDPOINT, json=json)
6568
# Assert
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
MethodNotAllowedError:
2+
summary: Method is not allowed
3+
description: 405 error response bundle as HTTP method is not allowed
4+
value:
5+
resourceType: "OperationOutcome"
6+
issue:
7+
- code: "not-supported"
8+
details:
9+
coding:
10+
- system: "https://fhir.nhs.uk/R4/CodeSystem/ValidatedRelationships-ErrorOrWarningCode"
11+
version: "1"
12+
code: "METHOD_NOT_ALLOWED"
13+
display: "The method is not allowed."
14+
diagnostics: "The method is not allowed for the requested resource."
15+
severity: "error"

specification/validated-relationships-service-api.yaml

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
openapi: "3.0.0"
44
info:
55
title: "Validated Relationships Service API"
6-
version: "1.13.0"
6+
version: "1.14.0"
77
description: |
88
## Overview
99
Use this API to access the Validated Relationships Service - the national electronic database of relationships
@@ -141,7 +141,7 @@ info:
141141
* only covers a limited set of scenarios
142142
* is open access, so does not allow you to test authorisation
143143
144-
[<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/44536620-c011ca09-3246-4d9c-870e-33ebd0629114?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D44536620-c011ca09-3246-4d9c-870e-33ebd0629114%26entityType%3Dcollection%26workspaceId%3D81756490-ef07-4f09-9861-3c601a39729e)
144+
[<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/18067099-5c759d0a-d924-44ab-8668-3587dcaf27e1?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D18067099-5c759d0a-d924-44ab-8668-3587dcaf27e1%26entityType%3Dcollection%26workspaceId%3D1ee72d72-3355-4213-a165-a79aa2ab1de8)
145145
146146
### Integration testing
147147
@@ -300,6 +300,8 @@ paths:
300300
examples:
301301
internalServerError:
302302
$ref: "./examples/responses/errors/internal-server-error.yaml#/InternalServerError"
303+
304+
/QuestionnaireResponse/{id}:
303305
get:
304306
summary: Get a proxy access request
305307
description: |
@@ -308,7 +310,7 @@ paths:
308310
QuestionnaireResponse document that was previously submitted.
309311
310312
## Request Requirements
311-
A valid access request ID must be provided as a query parameter. This access request ID is returned
313+
A valid access request ID must be provided as a path parameter. This access request ID is returned
312314
when a QuestionnaireResponse is initially submitted via the POST endpoint.
313315
314316
## Access modes
@@ -319,25 +321,19 @@ paths:
319321
320322
## Sandbox test scenarios
321323
322-
| Scenario | Request | Response |
323-
| -------------------------------- | -----------------------------------------| ------------------------------------------------------------- |
324-
| Valid access request ID | `ID=156e1560-e532-4e2a-85ad-5aeff03dc43e`| HTTP Status 200 with QuestionnaireResponse |
325-
| Invalid access request ID | `ID=INVALID` | HTTP Status 400 with INVALID_IDENTIFIER_VALUE message |
326-
| Missing access request ID | No ID parameter | HTTP Status 400 with BAD_REQUEST message |
327-
| Non-existent access request ID | `ID=60d09b82-f4bb-41f9-b41e-767999b4ac9b`| HTTP Status 404 with QUESTIONNAIRE_RESPONSE_NOT_FOUND message |
324+
| Scenario | Request | Response |
325+
| -------------------------------- | ------------------------------------------------ | ------------------------------------------------------------- |
326+
| Valid access request ID | ID value: `156e1560-e532-4e2a-85ad-5aeff03dc43e` | HTTP Status 200 with QuestionnaireResponse |
327+
| Invalid access request ID | ID value: `INVALID` | HTTP Status 400 with INVALID_IDENTIFIER_VALUE message |
328+
| Missing access request ID | No ID path parameter | HTTP Status 405 with METHOD_NOT_ALLOWED message |
329+
| Non-existent access request ID | ID value: `60d09b82-f4bb-41f9-b41e-767999b4ac9b` | HTTP Status 404 with QUESTIONNAIRE_RESPONSE_NOT_FOUND message |
328330
329331
operationId: get-questionnaire-response
330332
parameters:
331333
- $ref: "#/components/parameters/BearerAuthorization"
332334
- $ref: "#/components/parameters/RequestID"
333335
- $ref: "#/components/parameters/CorrelationID"
334-
- name: ID
335-
in: query
336-
description: The unique access request ID of the QuestionnaireResponse to retrieve
337-
required: true
338-
schema:
339-
type: string
340-
example: "156e1560-e532-4e2a-85ad-5aeff03dc43e"
336+
- $ref: "#/components/parameters/AccessRequestID"
341337
responses:
342338
"200":
343339
description: QuestionnaireResponse successfully retrieved.
@@ -374,7 +370,7 @@ paths:
374370
invalidAccessRequestID:
375371
$ref: "./examples/responses/GET_QuestionnaireResponse/errors/invalid_access_request_id.yaml#/InvalidAccessRequestID"
376372
missingAccessRequestID:
377-
$ref: "./examples/responses/GET_QuestionnaireResponse/errors/missing_access_request_id.yaml#/MissingAccessRequestID"
373+
$ref: "./examples/responses/errors/method-not-allowed.yaml#/MethodNotAllowedError"
378374
questionnaireResponseNotFound:
379375
$ref: "./examples/responses/GET_QuestionnaireResponse/errors/questionnaire_response_not_found.yaml#/QuestionnaireResponseNotFound"
380376
"5XX":
@@ -2788,3 +2784,14 @@ components:
27882784
format: uuid
27892785
pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
27902786
example: 74eed847-ca25-4e76-8cf2-f2c2d7842a7a
2787+
2788+
AccessRequestID:
2789+
in: path
2790+
name: id
2791+
required: true
2792+
description: The unique access request ID of the QuestionnaireResponse to retrieve
2793+
schema:
2794+
type: string
2795+
format: uuid
2796+
pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
2797+
example: "156e1560-e532-4e2a-85ad-5aeff03dc43e"

0 commit comments

Comments
 (0)