Skip to content
Merged
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
632 changes: 459 additions & 173 deletions postman/Validated Relationship Service Sandbox.postman_collection.json

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions sandbox/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

from .get_consent import get_consent_response
from .get_consent_by_id import get_consent_by_id_response
from .get_questionnaire_response import get_questionnaire_response_response
from .get_questionnaire_response_by_path_id import get_questionnaire_response_by_path_id_response
from .get_related_person import get_related_person_response
from .patch_consent import patch_consent_response
from .post_consent import post_consent_response
from .post_questionnaire_response import post_questionnaire_response_response
from .utils import generate_response_from_example
from .constants import METHOD_NOT_ALLOWED

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


@app.route(f"/{COMMON_PATH}/QuestionnaireResponse", methods=["GET"])
@app.route(f"/{COMMON_PATH}/QuestionnaireResponse/", methods=["GET"])
def get_questionnaire_response() -> Union[dict, tuple]:
"""Sandbox API for GET /QuestionnaireResponse

Returns:
Union[dict, tuple]: Response for GET /QuestionnaireResponse
"""
return get_questionnaire_response_response()
return generate_response_from_example(METHOD_NOT_ALLOWED, 405)


@app.route(f"/{COMMON_PATH}/QuestionnaireResponse/<identifier>", methods=["GET"])
def get_questionnaire_response_id(identifier: str) -> Union[dict, tuple]:
"""Sandbox API for GET /QuestionnaireResponse

Returns:
Union[dict, tuple]: Response for GET /QuestionnaireResponse
"""
return get_questionnaire_response_by_path_id_response(identifier)


@app.route(f"/{COMMON_PATH}/QuestionnaireResponse", methods=["POST"])
Expand Down
1 change: 1 addition & 0 deletions sandbox/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
INVALIDATED_RESOURCE = "./api/examples/errors/invalidated-resource.yaml"
MISSING_IDENTIFIER = "./api/examples/errors/missing-identifier.yaml"
INVALID_IDENTIFIER = "./api/examples/errors/invalid-identifier.yaml"
METHOD_NOT_ALLOWED = "./api/examples/errors/method-not-allowed.yaml"

# GET Consent examples
GET_CONSENT__DIRECTORY = "./api/examples/GET_Consent/"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
from logging import INFO, basicConfig, getLogger
from typing import Union

from flask import request

from .constants import (
GET_QUESTIONNAIRE_RESPONSE__INVALID,
GET_QUESTIONNAIRE_RESPONSE__MISSING,
GET_QUESTIONNAIRE_RESPONSE__NOT_FOUND,
GET_QUESTIONNAIRE_RESPONSE__SUCCESS,
INTERNAL_SERVER_ERROR_EXAMPLE,
METHOD_NOT_ALLOWED,
)
from .utils import generate_response_from_example

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


def get_questionnaire_response_response() -> Union[dict, tuple]:
"""Sandbox API for GET /QuestionnaireResponse
def get_questionnaire_response_by_path_id_response(access_request_id: str) -> Union[dict, tuple]:
"""Sandbox API for GET /QuestionnaireResponse/{id}

Returns:
Union[dict, tuple]: Response for GET /QuestionnaireResponse
Union[dict, tuple]: Response for GET /QuestionnaireResponse/{id}
"""
try:
access_request_id = request.args.get("ID")
if access_request_id == "156e1560-e532-4e2a-85ad-5aeff03dc43e":
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__SUCCESS, 200)
elif access_request_id == "INVALID":
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__INVALID, 400)
elif access_request_id == "" or access_request_id is None:
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__MISSING, 400)
return generate_response_from_example(METHOD_NOT_ALLOWED, 405)
elif access_request_id == "60d09b82-f4bb-41f9-b41e-767999b4ac9b":
return generate_response_from_example(GET_QUESTIONNAIRE_RESPONSE__NOT_FOUND, 404)
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,68 @@


@pytest.mark.parametrize(
("request_args", "response_file_name", "status_code"),
("path", "response_file_name", "status_code"),
[
(
"ID=156e1560-e532-4e2a-85ad-5aeff03dc43e",
"/156e1560-e532-4e2a-85ad-5aeff03dc43e",
"./api/examples/GET_QuestionnaireResponse/success.yaml",
200,
),
(
"ID=INVALID",
"/INVALID",
"./api/examples/GET_QuestionnaireResponse/errors/invalid_access_request_id.yaml",
400,
),
(
"ID=",
"./api/examples/GET_QuestionnaireResponse/errors/missing_access_request_id.yaml",
400,
),
(
"ID=60d09b82-f4bb-41f9-b41e-767999b4ac9b",
"/60d09b82-f4bb-41f9-b41e-767999b4ac9b",
"./api/examples/GET_QuestionnaireResponse/errors/questionnaire_response_not_found.yaml",
404,
),
(
"ID=INVALID_CODE",
"/INVALID_CODE",
"./api/examples/errors/internal-server-error.yaml",
500,
),
],
)
@patch("sandbox.api.get_questionnaire_response.generate_response_from_example")
def test_get_questionnaire_response_returns_expected_responses__mocked_utils(
@patch("sandbox.api.get_questionnaire_response_by_path_id.generate_response_from_example")
def test_get_questionnaire_response_id_returns_expected_responses__mocked_utils(
mock_generate_response_from_example: MagicMock,
request_args: str,
path: str,
response_file_name: str,
status_code: int,
client: object,
) -> None:
"""Test GET Consent endpoint."""
"""Test GET /QuestionnaireResponse/{id} endpoint."""
mock_generate_response_from_example.return_value = mocked_response = Response(
dumps({"data": "mocked"}),
status=status_code,
content_type="application/json",
)
# Act
response = client.get(f"{GET_QUESTIONNAIRE_RESPONSE_API_ENDPOINT}?{request_args}")
response = client.get(f"{GET_QUESTIONNAIRE_RESPONSE_API_ENDPOINT}{path}")
# Assert
mock_generate_response_from_example.assert_called_once_with(response_file_name, status_code)
assert response.status_code == status_code
assert response.json == loads(mocked_response.get_data(as_text=True))


@pytest.mark.parametrize("path", ["/", ""])
@patch("sandbox.api.app.generate_response_from_example")
def test_get_questionnaire_response_without_path_params_return_405_errors(
mock_generate_response_from_example: MagicMock,
path: str,
client: object,
) -> None:
"""Test the GET /QuestionnaireResponse endpoint with no path params."""
mock_generate_response_from_example.return_value = mocked_response = Response(
dumps({"data": "mocked"}),
status=405,
content_type="application/json",
)
# Act
response = client.get(f"{GET_QUESTIONNAIRE_RESPONSE_API_ENDPOINT}{path}")
# Assert
mock_generate_response_from_example.assert_called_once_with("./api/examples/errors/method-not-allowed.yaml", 405)
assert response.status_code == 405
assert response.json == loads(mocked_response.get_data(as_text=True))
24 changes: 20 additions & 4 deletions sandbox/api/tests/test_patch_consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,27 @@
("74eed847-ca25-4e76-8cf2-f2c2d7842a7a", PATCH_CONSENT__SUCCESS, 200),
("6b71ac92-baa3-4b76-b0f5-a601257e2722", PATCH_CONSENT__SUCCESS, 200),
("43003db8-ffcd-4bd6-ab2f-b49b9656f9e5", PATCH_CONSENT__SUCCESS, 200),
("849ea584-2318-471b-a24c-cee1b5ad0137", PATCH_CONSENT__INVALID_PATCH_FORMAT, 400),
(
"849ea584-2318-471b-a24c-cee1b5ad0137",
PATCH_CONSENT__INVALID_PATCH_FORMAT,
400,
),
("01abb0c5-b1ac-499d-9655-9cd0b8d3588f", PATCH_CONSENT__INVALID_PATH, 400),
("78c35330-fa2f-4934-a5dd-fff847f38de5", PATCH_CONSENT__INVALID_STATUS_CODE, 422),
("51fb4df5-815a-45cd-8427-04d6558336b7", PATCH_CONSENT__INVALID_STATUS_REASON, 422),
("7b7f47b8-96e5-43eb-b733-283bf1449f2c", PATCH_CONSENT__INVALID_STATE_TRANSITION, 422),
(
"78c35330-fa2f-4934-a5dd-fff847f38de5",
PATCH_CONSENT__INVALID_STATUS_CODE,
422,
),
(
"51fb4df5-815a-45cd-8427-04d6558336b7",
PATCH_CONSENT__INVALID_STATUS_REASON,
422,
),
(
"7b7f47b8-96e5-43eb-b733-283bf1449f2c",
PATCH_CONSENT__INVALID_STATE_TRANSITION,
422,
),
("xxxxxxxx", PATCH_CONSENT__RESOURCE_NOT_FOUND, 404),
("12345678", PATCH_CONSENT__RESOURCE_NOT_FOUND, 404),
],
Expand Down
5 changes: 4 additions & 1 deletion sandbox/api/tests/test_post_questionnaire_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ def test_post_questionnaire_response(
status=status_code,
content_type="application/json",
)
json = {"resourceType": "QuestionnaireResponse", "source": {"identifier": {"value": nhs_num}}}
json = {
"resourceType": "QuestionnaireResponse",
"source": {"identifier": {"value": nhs_num}},
}
# Act
response = client.post(QUESTIONNAIRE_RESPONSE_API_ENDPOINT, json=json)
# Assert
Expand Down
15 changes: 15 additions & 0 deletions specification/examples/responses/errors/method-not-allowed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
MethodNotAllowedError:
summary: Method is not allowed
description: 405 error response bundle as HTTP method is not allowed
value:
resourceType: "OperationOutcome"
issue:
- code: "not-supported"
details:
coding:
- system: "https://fhir.nhs.uk/R4/CodeSystem/ValidatedRelationships-ErrorOrWarningCode"
version: "1"
code: "METHOD_NOT_ALLOWED"
display: "The method is not allowed."
diagnostics: "The method is not allowed for the requested resource."
severity: "error"
41 changes: 24 additions & 17 deletions specification/validated-relationships-service-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
openapi: "3.0.0"
info:
title: "Validated Relationships Service API"
version: "1.13.0"
version: "1.14.0"
description: |
## Overview
Use this API to access the Validated Relationships Service - the national electronic database of relationships
Expand Down Expand Up @@ -141,7 +141,7 @@ info:
* only covers a limited set of scenarios
* is open access, so does not allow you to test authorisation

[<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)
[<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)

### Integration testing

Expand Down Expand Up @@ -300,6 +300,8 @@ paths:
examples:
internalServerError:
$ref: "./examples/responses/errors/internal-server-error.yaml#/InternalServerError"

/QuestionnaireResponse/{id}:
get:
summary: Get a proxy access request
description: |
Expand All @@ -308,7 +310,7 @@ paths:
QuestionnaireResponse document that was previously submitted.

## Request Requirements
A valid access request ID must be provided as a query parameter. This access request ID is returned
A valid access request ID must be provided as a path parameter. This access request ID is returned
when a QuestionnaireResponse is initially submitted via the POST endpoint.

## Access modes
Expand All @@ -319,25 +321,19 @@ paths:

## Sandbox test scenarios

| Scenario | Request | Response |
| -------------------------------- | -----------------------------------------| ------------------------------------------------------------- |
| Valid access request ID | `ID=156e1560-e532-4e2a-85ad-5aeff03dc43e`| HTTP Status 200 with QuestionnaireResponse |
| Invalid access request ID | `ID=INVALID` | HTTP Status 400 with INVALID_IDENTIFIER_VALUE message |
| Missing access request ID | No ID parameter | HTTP Status 400 with BAD_REQUEST message |
| Non-existent access request ID | `ID=60d09b82-f4bb-41f9-b41e-767999b4ac9b`| HTTP Status 404 with QUESTIONNAIRE_RESPONSE_NOT_FOUND message |
| Scenario | Request | Response |
| -------------------------------- | ------------------------------------------------ | ------------------------------------------------------------- |
| Valid access request ID | ID value: `156e1560-e532-4e2a-85ad-5aeff03dc43e` | HTTP Status 200 with QuestionnaireResponse |
| Invalid access request ID | ID value: `INVALID` | HTTP Status 400 with INVALID_IDENTIFIER_VALUE message |
| Missing access request ID | No ID path parameter | HTTP Status 405 with METHOD_NOT_ALLOWED message |
| Non-existent access request ID | ID value: `60d09b82-f4bb-41f9-b41e-767999b4ac9b` | HTTP Status 404 with QUESTIONNAIRE_RESPONSE_NOT_FOUND message |

operationId: get-questionnaire-response
parameters:
- $ref: "#/components/parameters/BearerAuthorization"
- $ref: "#/components/parameters/RequestID"
- $ref: "#/components/parameters/CorrelationID"
- name: ID
in: query
description: The unique access request ID of the QuestionnaireResponse to retrieve
required: true
schema:
type: string
example: "156e1560-e532-4e2a-85ad-5aeff03dc43e"
- $ref: "#/components/parameters/AccessRequestID"
responses:
"200":
description: QuestionnaireResponse successfully retrieved.
Expand Down Expand Up @@ -374,7 +370,7 @@ paths:
invalidAccessRequestID:
$ref: "./examples/responses/GET_QuestionnaireResponse/errors/invalid_access_request_id.yaml#/InvalidAccessRequestID"
missingAccessRequestID:
$ref: "./examples/responses/GET_QuestionnaireResponse/errors/missing_access_request_id.yaml#/MissingAccessRequestID"
$ref: "./examples/responses/errors/method-not-allowed.yaml#/MethodNotAllowedError"
questionnaireResponseNotFound:
$ref: "./examples/responses/GET_QuestionnaireResponse/errors/questionnaire_response_not_found.yaml#/QuestionnaireResponseNotFound"
"5XX":
Expand Down Expand Up @@ -2788,3 +2784,14 @@ components:
format: uuid
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}$"
example: 74eed847-ca25-4e76-8cf2-f2c2d7842a7a

AccessRequestID:
in: path
name: id
required: true
description: The unique access request ID of the QuestionnaireResponse to retrieve
schema:
type: string
format: uuid
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}$"
example: "156e1560-e532-4e2a-85ad-5aeff03dc43e"