diff --git a/azure/templates/pds-tests.yml b/azure/templates/pds-tests.yml index 0615dba56..78592aa80 100644 --- a/azure/templates/pds-tests.yml +++ b/azure/templates/pds-tests.yml @@ -120,3 +120,22 @@ steps: poetry run pytest -v tests/functional/test_patient_access.py --api-name=personal-demographics-service --proxy-name "$(FULLY_QUALIFIED_SERVICE_NAME)" --junitxml=tests/patient-access-test-report.xml --reruns 20 --reruns-delay 1 displayName: Run patient access tests workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)" + + + - bash: | + export APIGEE_API_TOKEN="$(secret.AccessToken)" + export APIGEE_ACCESS_TOKEN="$(secret.AccessToken)" + export OAUTH_BASE_URI="https://$(ENVIRONMENT).api.service.nhs.uk" + export OAUTH_PROXY="oauth2-mock" + export TEST_PATIENT_ID="$(TEST_PATIENT_ID)" + export INTERNAL_DEV_ASID="$(INTERNAL_DEV_ASID)" + + poetry run pytest -v tests/functional/test_patient_create.py --api-name=personal-demographics-service --proxy-name "$(FULLY_QUALIFIED_SERVICE_NAME)" --junitxml=tests/patient-create-test-report.xml --reruns 20 --reruns-delay 1 + displayName: Run post patient tests + workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)" + + - task: PublishTestResults@2 + displayName: "Publish post patient test results" + inputs: + testResultsFiles: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/tests/patient-create-test-report.xml + failTaskOnFailedTests: true diff --git a/tests/functional/data/responses/valid_patient_post_response.json b/tests/functional/data/responses/valid_patient_post_response.json new file mode 100644 index 000000000..b308632b4 --- /dev/null +++ b/tests/functional/data/responses/valid_patient_post_response.json @@ -0,0 +1,44 @@ +{ + "id": "#nhsNumber", + "identifier": [ + { + "extension": [ + { + "url": "https://fhir.hl7.org.uk/StructureDefinition/Extension-UKCore-NHSNumberVerificationStatus", + "valueCodeableConcept": { + "coding": [ + { + "code": "01", + "display": "Number present and verified", + "system": "https://fhir.hl7.org.uk/CodeSystem/UKCore-NHSNumberVerificationStatus", + "version": "1.0.0" + } + ] + } + } + ], + "system": "https://fhir.nhs.uk/Id/nhs-number", + "value": "#nhsNumber" + } + ], + "meta": { + "security": [ + { + "code": "U", + "display": "unrestricted", + "system": "http://terminology.hl7.org/CodeSystem/v3-Confidentiality" + } + ], + "versionId": "1" + }, + "name": [ + { + "family": "#family", + "period": { + "start": "#todayDate" + }, + "use": "usual" + } + ], + "resourceType": "Patient" +} \ No newline at end of file diff --git a/tests/functional/features/post_patient.feature b/tests/functional/features/post_patient.feature new file mode 100644 index 000000000..a07fe6f64 --- /dev/null +++ b/tests/functional/features/post_patient.feature @@ -0,0 +1,67 @@ +Feature: Post Patient + +Background: + Given I am a healthcare worker user + +Scenario: Negative test - invalid request payload + Given path "Patient" + And request body: + {"blahblahblah":"blah"} + When method POST + Then status 400 + And response body: + { + "issue": [ + { + "code": "required", + "details": { + "coding": [ + { + "code": "MISSING_VALUE", + "display": "Required value is missing", + "system": "https://fhir.nhs.uk/R4/CodeSystem/Spine-ErrorOrWarningCode", + "version": "1" + } + ] + }, + "diagnostics": "Missing value - 'nhsNumberAllocation'", + "severity": "error" + } + ], + "resourceType": "OperationOutcome" + } + +Scenario: Valid request, basic payload + Given path "Patient" + And request body: + { + "nhsNumberAllocation": "Done", + "name": { + "use": "L", + "name.familyName": "Smith" + }, + "registeringAuthority": { + "regAuthorityType.code": "x", + "regAuthorityType.codeSystem": "2.16.840.1.113883.2.1.3.2.4.16.20", + "regOrganisation.root": "2.16.840.1.113883.2.1.4.3", + "regOrganisation.extension": "RWF", + "authorPersonID": "", + "authorSystemID": "230811201324", + "deathStatus": "", + "deceasedTime": "", + "overallUpdateMode": "create" + } + } + When method POST + Then status 201 + And expected_response template == read(valid_patient_post_response) + And set expected_response['#nhsNumber'] = response['id'] + And set expected_response['#family'] = 'Smith' + And ignore in response comparison the family name Id + And response body == expected_response + + +Scenario: The rate limit is tripped when POSTing new Patients (>5tps) + When I post to the Patient endpoint more than 5 times per second + Then I get a mix of 400 and 429 HTTP response codes + And the 429 response bodies alert me that there have been too many Create Patient requests diff --git a/tests/functional/features/post_patient_spike_arrest.feature b/tests/functional/features/post_patient_spike_arrest.feature deleted file mode 100644 index b3f3f768b..000000000 --- a/tests/functional/features/post_patient_spike_arrest.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Post Patient Spike Arrest Policy - - Scenario: The rate limit is tripped when POSTing new Patients (>5tps) - Given I am a healthcare worker user - When I post to the Patient endpoint more than 5 times per second - Then I get a mix of 400 and 429 HTTP response codes - And the 429 response bodies alert me that there have been too many Create Patient requests diff --git a/tests/functional/test_patient_create.py b/tests/functional/test_patient_create.py index 35c0614b5..07c9c00b0 100644 --- a/tests/functional/test_patient_create.py +++ b/tests/functional/test_patient_create.py @@ -1,5 +1,6 @@ import datetime import json +import os import re import uuid @@ -12,16 +13,26 @@ from dateutil import parser from functools import partial from lxml import html -from pytest_bdd import when, then +from pytest_bdd import given, when, then, parsers -from .configuration.config import (PDS_BASE_PATH, CLIENT_ID, CLIENT_SECRET) -from .utils.helpers import get_role_id_from_user_info_endpoint +from tests.functional.conftest import RESPONSES_DIR +from tests.functional.configuration.config import (CLIENT_ID, CLIENT_SECRET) +from tests.functional.utils.helpers import get_role_id_from_user_info_endpoint -scenario = partial(pytest_bdd.scenario, './features/post_patient_spike_arrest.feature') +scenario = partial(pytest_bdd.scenario, './features/post_patient.feature') + + +@scenario('Negative test - invalid request payload') +def test_post_patient_invalid(): + pass + + +@scenario('Valid request, basic payload') +def test_post_patient_basic(): + pass -@pytest.mark.skipif("asid-required" in PDS_BASE_PATH, reason="Don't run in asid-required environment") @scenario('The rate limit is tripped when POSTing new Patients (>5tps)') def test_post_patient_rate_limit(): pass @@ -86,7 +97,7 @@ def healthcare_worker_auth_headers(identity_service_base_url: str) -> dict: async def _create_patient(session, headers, url, body): details = {'request_time': datetime.datetime.now(datetime.timezone.utc)} - async with session.post(url=url, headers=headers, data=body) as resp: + async with session.post(url=url, headers=headers, json=body) as resp: status = resp.status headers = resp.headers text = await resp.text() @@ -112,6 +123,41 @@ async def _create_all_patients(headers, url, body, loop, num_patients): # STEPS---------------------------------------------------------------------------------------------------------- +# --------------------------------------------------------------------------------------------------------------- +# GIVEN---------------------------------------------------------------------------------------------------------- +@given(parsers.parse('path "{path}"'), target_fixture="full_url") +def set_url_path(path: str, pds_url: str) -> str: + if path[0] != "/": + path = f"/{path}" + return f"{pds_url}{path}" + + +@given(parsers.parse("request body:\n{request_body:json}", extra_types=dict(json=json.loads)), + target_fixture="request_body") +def set_request_body(request_body: dict) -> dict: + return request_body + + +@given(parsers.parse("headers:\n{headers:json}", extra_types=dict(json=json.loads)), target_fixture="headers") +def set_additional_headers(headers: dict, healthcare_worker_auth_headers: dict) -> dict: + for key, value in headers.items(): + if value == "#(uuid)": + headers[key] = str(uuid.uuid4()) + healthcare_worker_auth_headers.update(headers) + return healthcare_worker_auth_headers + + +# WHEN------------------------------------------------------------------------------------------------------------ +@when("method POST", target_fixture="response") +def make_post_request(full_url: str, healthcare_worker_auth_headers: dict, request_body: dict) -> requests.Response: + response = requests.post( + full_url, + headers=healthcare_worker_auth_headers, + json=request_body, + ) + return response + + @pytest.mark.asyncio @when("I post to the Patient endpoint more than 5 times per second", target_fixture='post_results') def post_patient_multiple_times(healthcare_worker_auth_headers: dict, pds_url: str) -> list: @@ -142,6 +188,24 @@ def post_patient_multiple_times(healthcare_worker_auth_headers: dict, pds_url: s return results +# THEN------------------------------------------------------------------------------------------------------------ +@then(parsers.parse("status {expected_status:d}")) +def assert_expected_status(expected_status: int, response: requests.Response): + assert response.status_code == expected_status + + +@then(parsers.parse("response body:\n{expected_response:json}", extra_types=dict(json=json.loads))) +def assert_expected_response_body(expected_response: dict, response: requests.Response): + assert expected_response == response.json() + + +@then(parsers.parse("response body == read({response_file})")) +def assert_against_example_response_file(response_file, response: requests.Response): + with open(os.path.join(RESPONSES_DIR, f'{response_file}.json'), 'r') as f: + expected_response_body = json.load(f) + assert response.json() == expected_response_body + + @then("I get a mix of 400 and 429 HTTP response codes") def assert_expected_spike_arrest_response_codes(post_results): successful_requests = [x for x in post_results if x['status'] == 400] @@ -157,3 +221,41 @@ def assert_expected_429_diagnostics(post_results): expected_diagnostics = 'There have been too many Create Patient requests. Please try again later.' correct_diagnostics = [x for x in diagnostics if x == expected_diagnostics] assert len(correct_diagnostics) == len(spike_arrests), "Some of the diagnostics messages were not as expected" + + +@then(parsers.parse("expected_response template == read({response_file})"), target_fixture='expected_response_json') +def set_expected_response_json_string(response_file): + today_date = datetime.datetime.now().strftime("%Y-%m-%d") + + with open(os.path.join(RESPONSES_DIR, f'{response_file}.json'), 'r') as f: + json_string = f.read() + + # swap out any pre-defined dynamic values + new_string = json_string.replace("#todayDate", today_date) + return new_string + + +@then(parsers.parse("set expected_response['{placeholder}'] = '{value}'"), target_fixture='expected_response_json') +def set_expected_response_json_value(placeholder, value, expected_response_json): + new_string = expected_response_json.replace(placeholder, value) + return new_string + + +@then(parsers.parse("set expected_response['{placeholder}'] = response['{value}']"), + target_fixture='expected_response_json') +def set_expected_response_json_with_response_value(placeholder, value, response, expected_response_json): + new_string = expected_response_json.replace(placeholder, response.json()[value]) + return new_string + + +@then(parsers.parse("ignore in response comparison the family name Id"), target_fixture='response_json') +def set_ignore_path(response): + response_json = response.json() + del response_json['name'][0]['id'] + return response_json + + +@then("response body == expected_response") +def assert_response_is_expected_response(response_json, expected_response_json): + expected_response = json.loads(expected_response_json) + assert response_json == expected_response