-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New Karate mock for retrieve patient and patch patient functionality (#…
…994) * patch patient tests - successful patching scenarios * starts adding tests for patch error scenarios * more update scenarios implemented * invalid address ID test * adds more scnearios, moves some stubs around * adds final two patch error scenarios to feature * tidies up mock patch code * makes all tests like the original tests, gets everything working * fixes logic so that successful patch scenarios work * ignore sandbox tests in parallel runs for the moment * removes the get-patient-search.js file from the mock - not needed for this * removes comments, adds lines, cleans up * adds parallel runner for mock env; updates test for content headers - application/json should be allowed * adds karate sandbox tests to pipeline
- Loading branch information
Pete Loggie
authored
Mar 27, 2024
1 parent
70308b4
commit 0234490
Showing
33 changed files
with
2,674 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,3 +32,4 @@ keys/ | |
|
||
|
||
karate-tests/target/ | ||
karate-tests/src/test/java/target/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
steps: | ||
|
||
- template: "azure/components/aws-assume-role.yml@common" | ||
parameters: | ||
role: "auto-ops" | ||
profile: "apm_ptl" | ||
|
||
- template: "azure/components/get-aws-secrets-and-ssm-params.yml@common" | ||
parameters: | ||
config_ids: | ||
- /ptl/azure-devops/env-internal-dev/test-app/internal-testing-internal-dev/CLIENT_ID | ||
secret_ids: | ||
- ptl/app-credentials/jwt_testing/non-prod/JWT_TESTING_API_KEY | ||
- ptl/app-credentials/jwt_testing/non-prod/JWT_TESTING_WITH_ASID_API_KEY | ||
- ptl/backends/ig3/INTERNAL_DEV_ASID | ||
|
||
- bash: | | ||
echo '##vso[task.setvariable variable=CLIENT_ID]$(CLIENT_ID)' | ||
displayName: Expose common variables | ||
- bash: | | ||
export CLIENT_ID="$(CLIENT_ID)" | ||
mvn clean test -Dtest=TestMockParallel | ||
displayName: 'Run Karate Sandbox Tests' | ||
workingDirectory: "$(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/karate-tests" | ||
- task: PublishTestResults@2 | ||
displayName: 'Publish Karate sandbox test results' | ||
condition: in(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues', 'Failed') | ||
inputs: | ||
testResultsFiles: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/karate-tests/target/karate-reports/*.xml | ||
failTaskOnFailedTests: true | ||
|
||
- task: PublishBuildArtifacts@1 | ||
displayName: 'Publish Karate Sandbox HTML test report as an artifact' | ||
condition: in(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues', 'Failed') | ||
inputs: | ||
pathToPublish: $(Pipeline.Workspace)/s/$(SERVICE_NAME)/$(SERVICE_ARTIFACT_NAME)/karate-tests/target/karate-reports | ||
artifactName: KarateSandboxHTMLReports |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package mocks; | ||
|
||
|
||
import com.intuit.karate.http.HttpServer; | ||
import com.intuit.karate.http.ServerConfig; | ||
import com.intuit.karate.http.ServerContext; | ||
|
||
|
||
public class MockRunner { | ||
|
||
public static HttpServer start(String root, int port) { | ||
ServerConfig config = new ServerConfig(root) | ||
.useGlobalSession(true); | ||
config.contextFactory(request -> { | ||
ServerContext context = new ServerContext(config, request); | ||
context.setApi(true); | ||
request.setResourcePath("sandbox.js"); | ||
return context; | ||
}); | ||
return HttpServer.config(config) | ||
.http(port) | ||
.corsEnabled(true) | ||
.build(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
Our patients "database" for the get by NHS Number requests :-) | ||
*/ | ||
session.patients = session.patients || { | ||
'9000000009': context.read('classpath:mocks/stubs/patientResponses/patient_9000000009.json'), | ||
'9000000025': context.read('classpath:mocks/stubs/patientResponses/patient_9000000025.json'), | ||
'9000000033': context.read('classpath:mocks/stubs/patientResponses/patient_9000000033.json'), | ||
'9693632109': context.read('classpath:mocks/stubs/patientResponses/patient_9693632109.json') | ||
} | ||
|
||
/* | ||
Handler for get patient by NHS Number | ||
*/ | ||
if (request.pathMatches('/Patient/{nhsNumber}') && request.get) { | ||
let valid = validateHeaders(request) && validateNHSNumber(request) ; | ||
|
||
if (valid) { | ||
const nhsNumber = request.pathParams.nhsNumber; | ||
if (typeof session.patients[nhsNumber] == 'undefined') { | ||
response.body = context.read('classpath:mocks/stubs/errorResponses/RESOURCE_NOT_FOUND.json'); | ||
response.headers = basicResponseHeaders(request) | ||
response.status = 404 | ||
} else { | ||
patient = session.patients[nhsNumber] | ||
let responseHeaders = basicResponseHeaders(request) | ||
responseHeaders['etag'] = `W/"${patient.meta.versionId}"` | ||
response.body = patient; | ||
response.headers = responseHeaders; | ||
response.status = 200; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
function buildResponseHeaders(request, patient) { | ||
return { | ||
'content-type': 'application/fhir+json', | ||
'etag': `W/"${patient.meta.versionId}"`, | ||
'x-request-id': request.header('x-request-id'), | ||
'x-correlation-id': request.header('x-correlation-id') | ||
}; | ||
} | ||
|
||
/* | ||
Diagnostics strings for error messages | ||
*/ | ||
const NO_IF_MATCH_HEADER = "Invalid update with error - If-Match header must be supplied to update this resource"; | ||
const NO_PATCHES_PROVIDED = "Invalid update with error - No patches found"; | ||
const INVALID_RESOURCE_ID = "Invalid update with error - This resource has changed since you last read. Please re-read and try again with the new version number."; | ||
const INVALID_PATCH = "Invalid patch: Operation `op` property is not one of operations defined in RFC-6902" | ||
|
||
/* | ||
Functions to handle error responses | ||
*/ | ||
function invalidUpdateError(request, diagnostics) { | ||
let body = context.read('classpath:mocks/stubs/errorResponses/INVALID_UPDATE.json'); | ||
body.issue[0].diagnostics = diagnostics; | ||
response.headers = basicResponseHeaders(request); | ||
response.body = body; | ||
response.status = 400; | ||
return false | ||
} | ||
|
||
function preconditionFailedError(request, diagnostics) { | ||
let body = context.read('classpath:mocks/stubs/errorResponses/PRECONDITION_FAILED.json'); | ||
body.issue[0].diagnostics = diagnostics; | ||
response.headers = basicResponseHeaders(request); | ||
response.body = body; | ||
response.status = 412; | ||
return false | ||
} | ||
|
||
function unsupportedServiceError(request) { | ||
let body = context.read('classpath:mocks/stubs/errorResponses/UNSUPPORTED_SERVICE.json'); | ||
response.headers = basicResponseHeaders(request); | ||
response.body = body; | ||
response.status = 400; | ||
return false | ||
} | ||
|
||
/* | ||
Validate the headers specific to patching a patient | ||
*/ | ||
function validatePatchHeaders(request) { | ||
var valid = true; | ||
if (!request.header('if-match')) { | ||
valid = preconditionFailedError(request, NO_IF_MATCH_HEADER) | ||
} | ||
if (valid && !request.header('content-type').startsWith('application/json')) { | ||
valid = unsupportedServiceError(request) | ||
} | ||
return valid | ||
} | ||
|
||
/* | ||
The main logic for patching a patient | ||
*/ | ||
function patchPatient(originalPatient, request) { | ||
|
||
if (!request.body.patches) { | ||
return invalidUpdateError(request, NO_PATCHES_PROVIDED); | ||
} | ||
if (request.header('if-match') != `W/"${originalPatient.meta.versionId}"`) { | ||
return preconditionFailedError(request, INVALID_RESOURCE_ID); | ||
} | ||
|
||
let updatedPatient = JSON.parse(JSON.stringify(originalPatient)); | ||
let updateErrors = []; | ||
|
||
const validOperations = ['add', 'replace', 'remove', 'test'] | ||
for(let i = 0; i < request.body.patches.length; i++) { | ||
let patch = request.body.patches[i]; | ||
if (!validOperations.includes(patch.op)) { | ||
return invalidUpdateError(request, INVALID_PATCH) | ||
} | ||
if (patch.op == 'add' && patch.path === '/name/-') { | ||
updatedPatient.name.push(patch.value); | ||
} | ||
if (patch.op == 'replace' && patch.path === '/name/0/given/0') { | ||
updatedPatient.name[0].given[0] = patch.value; | ||
} | ||
if (patch.op == 'replace' && patch.path === '/gender') { | ||
updatedPatient.gender = patch.value; | ||
} | ||
if (patch.op == 'remove' && patch.path === '/name/0/suffix/0') { | ||
updatedPatient.name[0].suffix.splice(0, 1); | ||
} | ||
|
||
// these specific error scenarios for update errors should be reviewed | ||
if (patch.op == 'replace' && patch.path === "/address/0/line/0" && patch.value === "2 Whitehall Quay") { | ||
updateErrors.push("Invalid update with error - no id or url found for path with root /address/0"); | ||
} else if (patch.op == 'replace' && patch.path.startsWith("/address/0/") && !originalPatient.hasOwnProperty('address')) { | ||
updateErrors.push("Invalid update with error - Invalid patch - index '0' is out of bounds"); | ||
} else if (patch.op == 'replace' && patch.path === "/address/0/id" && patch.value === "456") { | ||
updateErrors.push("Invalid update with error - no 'address' resources with object id 456"); | ||
} else if (patch.op == 'replace' && patch.path === "/address/0/line") { | ||
updateErrors.push("Invalid update with error - Invalid patch - can't replace non-existent object 'line'"); | ||
} else if (patch.op == 'replace' && patch.path === "/address/0/id" && patch.value === "123456") { | ||
updateErrors.push("Invalid update with error - no 'address' resources with object id 123456"); | ||
} | ||
} | ||
|
||
// why is it that for this specific scenario (Invalid patch - attempt to replace non-existent object), | ||
// we have to pick the last error message, when for all the others we pick the first error message? | ||
const rogueErrors = [ | ||
"Invalid update with error - no 'address' resources with object id 456", | ||
"Invalid update with error - Invalid patch - can't replace non-existent object 'line'" | ||
] | ||
|
||
if (updateErrors.length > 0) { | ||
if (updateErrors.every(item => rogueErrors.includes(item)) && rogueErrors.every(item => updateErrors.includes(item))) { | ||
return invalidUpdateError(request, updateErrors[1]) | ||
} else { | ||
return invalidUpdateError(request, updateErrors[0]) | ||
} | ||
} else { | ||
updatedPatient.meta.versionId = (parseInt(updatedPatient.meta.versionId) + 1); | ||
return updatedPatient; | ||
} | ||
} | ||
|
||
/* | ||
Handler for patch patient | ||
*/ | ||
if (request.pathMatches('/Patient/{nhsNumber}') && request.patch) { | ||
const valid = validateNHSNumber(request) && validatePatchHeaders(request) && validateHeaders(request) | ||
if (valid) { | ||
const nhsNumber = request.pathParams.nhsNumber; | ||
const originalPatient = session.patients[nhsNumber]; | ||
let updatedPatient = patchPatient(originalPatient, request); | ||
if (updatedPatient) { | ||
// this line is commented out because the existing tests assume a stateless mock | ||
// session.patients[nhsNumber] = updatedPatient; | ||
response.headers = buildResponseHeaders(request, updatedPatient); | ||
response.body = updatedPatient; | ||
response.status = 200; | ||
} | ||
} | ||
} |
Oops, something went wrong.