Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NPA-2676 Add QuestionnaireResponse to Sandbox and Postman Collection #79

Merged
merged 7 commits into from
May 20, 2024
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
160 changes: 89 additions & 71 deletions poetry.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"info": {
"_postman_id": "a7ded7b6-5327-41f5-bf02-dcfe56d258c8",
"_postman_id": "85ee4695-8528-4e6e-82a6-29c4bea26a6d",
"name": "Validate Relationship Service Sandbox",
"description": "Example usage of the Validate Relationship Service (VRS) sandbox.\n\nFull specification is available at [https://digital.nhs.uk/developer/api-catalogue/validated-relationship-service](https://digital.nhs.uk/developer/api-catalogue/validated-relationship-service)",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "21394218"
"_exporter_id": "34042403"
},
"item": [
{
Expand Down Expand Up @@ -202,6 +202,43 @@
}
],
"description": "Examples of how to utilise the API to list relationships for a given NHS record."
},
{
"name": "Questionnaire Response",
"item": [
{
"name": "Questionnaire Response",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/fhir+json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"resourceType\": \"QuestionnaireResponse\",\n \"status\": \"completed\",\n \"authored\": \"2024-03-24T16:32:12.363Z\",\n \"source\": { \n \"type\": \"RelatedPerson\",\n \"identifier\": \"9000000001\" \n },\n \"item\": [\n {\n \"linkId\": \"proxy_details\",\n \"text\": \"Proxy details\",\n \"item\": [\n {\n \"linkId\": \"nhs_number\",\n \"text\": \"NHS Number\",\n \"answer\": [ \n {\n \"valueString\": \"9000000001\"\n }\n ]\n },\n {\n \"linkId\": \"relationship\",\n \"text\": \"Relationship\",\n \"answer\": [\n {\n \"valueCoding\": {\n \"system\": \"http://terminology.hl7.org/CodeSystem/v3-RoleCode\",\n \"code\": \"PRN\",\n \"display\": \"Parent\"\n }\n }\n ]\n }\n ]\n },\n {\n \"linkId\": \"patient_details\",\n \"text\": \"Patient details\",\n \"item\": [\n {\n \"linkId\": \"nhs_number\",\n \"text\": \"NHS Number\",\n \"answer\": [\n {\n \"valueString\": \"9000000002\"\n }\n ]\n },\n {\n \"linkId\": \"first_name\",\n \"text\": \"First Name\",\n \"answer\": [\n {\n \"valueString\": \"Timmy\"\n }\n ]\n },\n {\n \"linkId\": \"last_name\",\n \"text\": \"Last name\",\n \"answer\": [\n {\n \"valueString\": \"Tenenbaum\"\n }\n ]\n },\n {\n \"linkId\": \"date_of_birth\",\n \"text\": \"Date of Birth\",\n \"answer\": [\n {\n \"valueDate\": \"2020-10-22\"\n }\n ]\n },\n {\n \"linkId\": \"postcode\",\n \"text\": \"Postcode\",\n \"answer\": [\n {\n \"valueString\": \"LS1 4AP\"\n }\n ]\n }\n ]\n },\n {\n \"linkId\": \"requested_services\",\n \"text\": \"Requested services\",\n \"answer\": [\n {\n \"valueCoding\": {\n \"system\": \"http://terminology.hl7.org/CodeSystem/consentaction\",\n \"code\": \"appointments\",\n \"display\": \"manage appointments\"\n }\n },\n {\n \"valueCoding\": {\n \"system\": \"http://terminology.hl7.org/CodeSystem/consentaction\",\n \"code\": \"medicines\",\n \"display\": \"manage medicines\"\n }\n },\n {\n \"valueCoding\": {\n \"system\": \"http://terminology.hl7.org/CodeSystem/consentaction\",\n \"code\": \"records\",\n \"display\": \"access medical records\"\n }\n },\n {\n \"valueCoding\": {\n \"system\": \"http://terminology.hl7.org/CodeSystem/consentaction\",\n \"code\": \"demographics\",\n \"display\": \"manage demographics and contact details\"\n }\n }\n ]\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{api_base_url}}/QuestionnaireResponse",
"host": [
"{{api_base_url}}"
],
"path": [
"QuestionnaireResponse"
]
},
"description": "Example of a response where the given NHS numbers do not have a relationship."
},
"response": []
}
]
}
],
"event": [
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ pytest-nhsd-apim = "^3.3.2"
[tool.poetry.dev-dependencies]
flake8 = "^3.7.9"
black = "^24.3.0"
pip-licenses = "^2.0.1"
pip-licenses = "^4.4.0"
jinja2 = "^3.1.3"
pyyaml = "^6.0.1"
docopt = "^0.6.2"
jsonpath-rw = "^1.4.0"
semver = "^2.9.0"
semver = "^3.0.2"
gitpython = "^3.0.5"
pytest = "^8.2.0"
coverage = "^5.5"
coverage = "^7.5"
aiohttp = "^3.7.3"
pytest-asyncio = "^0.14.0"
31 changes: 24 additions & 7 deletions sandbox/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@
from flask import Flask, request

from .utils import (
ERROR_RESPONSE,
LIST_RELATIONSHIP,
LIST_RELATIONSHIP_INCLUDE,
QUESTIONNAIRE_RESPONSE_SUCCESS,
VALIDATE_RELATIONSHIP_009,
VALIDATE_RELATIONSHIP_INCLUDE_009,
VALIDATE_RELATIONSHIP_025,
VALIDATE_RELATIONSHIP_INCLUDE_009,
VALIDATE_RELATIONSHIP_INCLUDE_025,
LIST_RELATIONSHIP,
LIST_RELATIONSHIP_INCLUDE,
ERROR_RESPONSE,
check_for_errors,
check_for_empty,
check_for_validate,
check_for_errors,
check_for_list,
check_for_validate,
generate_response,
load_json_file,
)

app = Flask(__name__)
basicConfig(level=INFO, format="%(asctime)s - %(message)s")
logger = getLogger(__name__)
COMMON_PATH = "FHIR/R4"


@app.route("/_status", methods=["GET"])
Expand All @@ -35,7 +37,7 @@ def health() -> dict:
}


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

Expand Down Expand Up @@ -90,3 +92,18 @@ def get_related_persons() -> Union[dict, tuple]:
except Exception as e:
logger.error(e)
return generate_response(load_json_file(ERROR_RESPONSE), 500)


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

Returns:
Union[dict, tuple]: Response for POST /QuestionnaireResponse
"""

try:
return generate_response(load_json_file(QUESTIONNAIRE_RESPONSE_SUCCESS), 200)
except Exception as e:
logger.error(e)
return generate_response(load_json_file(ERROR_RESPONSE), 500)
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
]
},
"diagnostics": "The 'identifier' parameter is required",
"severity": "error",
"expression": ["RelatedPerson.identifier"]
"severity": "error"
}
],
"resourceType": "OperationOutcome"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
]
},
"diagnostics": "The identifier system is not valid.",
"severity": "error",
"expression": ["identifier"]
"severity": "error"
}
],
"resourceType": "OperationOutcome"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "informational",
"details": {
"coding": [
{
"code": "HDJ2123F",
"display": "HDJ2123F"
}
]
}
}
]
}
4 changes: 3 additions & 1 deletion sandbox/api/tests/conftest.py
JackPlowman marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from ..app import app

RELATED_PERSON_API_ENDPOINT = "/FHIR/R4/RelatedPerson"
FHIR_PATH = "/FHIR/R4"
RELATED_PERSON_API_ENDPOINT = f"{FHIR_PATH}/RelatedPerson"
QUESTIONNAIRE_RESPONSE_API_ENDPOINT = f"{FHIR_PATH}/QuestionnaireResponse"


@pytest.fixture()
Expand Down
46 changes: 38 additions & 8 deletions sandbox/api/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import pytest

from .conftest import RELATED_PERSON_API_ENDPOINT
from .conftest import RELATED_PERSON_API_ENDPOINT, QUESTIONNAIRE_RESPONSE_API_ENDPOINT

FILE_PATH = "sandbox.api.utils"
UTILS_FILE_PATH = "sandbox.api.utils"
APP_FILE_PATH = "sandbox.api.app"


@pytest.mark.parametrize("endpoint", ["/_status", "/_ping", "/health"])
Expand All @@ -25,12 +26,12 @@ def test_health_check(client: object, endpoint: str) -> None:
[
(
"identifier=9000000041",
"./api/responses/GET_RelatedPerson/not_found.json",
"./api/responses/not_found.json",
404,
),
(
"identifier=9000000017&patient:identifier=9000000041",
"./api/responses/GET_RelatedPerson/not_found.json",
"./api/responses/not_found.json",
404,
),
(
Expand Down Expand Up @@ -85,20 +86,49 @@ def test_health_check(client: object, endpoint: str) -> None:
),
],
)
@patch(f"{FILE_PATH}.load_json_file")
@patch(f"{UTILS_FILE_PATH}.load_json_file")
def test_related_person(
mock_get_response: MagicMock,
mock_load_json_file: MagicMock,
request_args: str,
response_file_name: str,
client: object,
status_code: int,
) -> None:
"""Test related_persons endpoint with identifier only."""
# Arrange
mock_get_response.return_value = expected_body = {"data": "mocked"}
mock_load_json_file.return_value = expected_body = {"data": "mocked"}
# Act
response = client.get(f"{RELATED_PERSON_API_ENDPOINT}?{request_args}")
# Assert
mock_get_response.assert_called_once_with(response_file_name)
mock_load_json_file.assert_called_once_with(response_file_name)
assert response.status_code == status_code
assert response.json == expected_body


@pytest.mark.parametrize(
"url_path,response_file_name,status_code",
[
(
QUESTIONNAIRE_RESPONSE_API_ENDPOINT,
"./api/responses/POST_QuestionnaireResponse/questionnaire_response_success.json",
200,
),
],
)
@patch(f"{APP_FILE_PATH}.load_json_file")
def test_questionnaire_response(
mock_load_json_file: MagicMock,
url_path: str,
response_file_name: str,
client: object,
status_code: int,
) -> None:
"""Test related_persons endpoint with identifier only."""
# Arrange
mock_load_json_file.return_value = expected_body = {"data": "mocked"}
# Act
response = client.post(url_path, json={"data": "mocked"})
# Assert
mock_load_json_file.assert_called_once_with(response_file_name)
assert response.status_code == status_code
assert response.json == expected_body
6 changes: 5 additions & 1 deletion sandbox/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from flask import request, Response

NOT_FOUND = "./api/responses/GET_RelatedPerson/not_found.json"
NOT_FOUND = "./api/responses/not_found.json"
EMPTY_RESPONSE = "./api/responses/GET_RelatedPerson/empty_response_9000000033.json"
LIST_RELATIONSHIP = (
"./api/responses/GET_RelatedPerson/list_relationship_9000000017.json"
Expand All @@ -26,6 +26,10 @@
ERROR_RESPONSE = "./api/responses/internal_server_error.json"
INCLUDE_FLAG = "RelatedPerson:patient"

QUESTIONNAIRE_RESPONSE_SUCCESS = (
"./api/responses/POST_QuestionnaireResponse/questionnaire_response_success.json"
)

PATIENT_IDENTIFIERS = ["9000000017", "9000000033"]
RELATED_IDENTIFIERS = ["9000000009", "9000000025"]

Expand Down
5 changes: 5 additions & 0 deletions specification/validated-relationships-service-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ paths:
Proxy Access Service and submitted as a QuestionaireResponse.

For the most part demographics information doesn't need to be provided in the access request since it can be pulled from PDS.

## Sandbox test scenarios

For details of sandbox test scenarios, or to try out the sandbox using our 'Try it out' feature, see the documentation for each endpoint.

operationId: new-access-request
parameters:
- $ref: "#/components/parameters/BearerAuthorization"
Expand Down