Skip to content

Commit 619c26e

Browse files
Ja/dpc -4984 - Adding smoke test for patient everything (#2906)
## 🎫 Ticket https://jira.cms.gov/browse/DPC-4984 ## 🛠 Changes <!-- What was added, updated, or removed in this PR? --> This change adds new smoke test for both synchronous and asynchronous patient everything endpoints. ## ℹ️ Context <!-- Why were these changes made? Add background context suitable for a non-technical audience. --> <!-- If any of the following security implications apply, this PR must not be merged without Stephen Walter's approval. Explain in this section and add @SJWalter11 as a reviewer. - Adds a new software dependency or dependencies. - Modifies or invalidates one or more of our security controls. - Stores or transmits data that was not stored or transmitted before. - Requires additional review of security implications for other reasons. --> This adds smoke test coverage to this high use endpoint. ## 🧪 Validation <!-- How were the changes verified? Did you fully test the acceptance criteria in the ticket? Provide reproducible testing instructions and screenshots if applicable. --> Confirmed the smoke test succeed locally and in higher environments. Dev https://github.com/CMSgov/dpc-app/actions/runs/21990516533 Test https://github.com/CMSgov/dpc-app/actions/runs/21958462870 Sandbox https://github.com/CMSgov/dpc-app/actions/runs/21959205829 Prod https://github.com/CMSgov/dpc-app/actions/runs/21960235280
1 parent 823bd91 commit 619c26e

File tree

5 files changed

+148
-44
lines changed

5 files changed

+148
-44
lines changed

dpc-load-testing/dpc-api-client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,10 @@ export function findPatientsByMbi(token, mbis) {
165165
return res;
166166
}
167167

168-
export function patientEverything(token, orgId, practitionerId, patientId) {
168+
export function patientEverything(token, orgId, practitionerId, patientId, preference = 'respond-sync') {
169169
const provenanceBody = generateProvenanceResourceBody(orgId, practitionerId);
170170
return http.get(`${urlRoot}/Patient/${patientId}/$everything`,
171-
createHeaderParam(token, {'X-Provenance': JSON.stringify(provenanceBody)})
171+
createHeaderParam(token, {'X-Provenance': JSON.stringify(provenanceBody), 'Prefer': preference})
172172
);
173173
}
174174

dpc-load-testing/smoke-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { checkBulkExportWorkflow } from './smoke_test_workflows/bulk_export.js';
1414
import { checkAuthWorkflow } from './smoke_test_workflows/auth.js';
1515
import { checkPortalsWorkflow } from './smoke_test_workflows/portals.js';
16+
import { checkPatientEverythingExportWorkflow } from './smoke_test_workflows/patient_export.js';
1617

1718
import NPIGeneratorCache from './utils/npi-generator.js';
1819

@@ -49,6 +50,7 @@ export function runSmokeTests(data) {
4950
checkAuthWorkflow(iterationData);
5051
checkBulkExportWorkflow(iterationData);
5152
checkPortalsWorkflow(iterationData);
53+
checkPatientEverythingExportWorkflow(iterationData);
5254
}
5355

5456
// Sets up three test organizations

dpc-load-testing/smoke_test_workflows/bulk_export.js

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
/*global console*/
22
/* eslint no-console: "off" */
33

4-
import { check, sleep } from 'k6';
4+
import { check } from 'k6';
55
import exec from 'k6/execution'
66
import { generateDPCToken } from '../generate-dpc-token.js';
77
import {
8-
authorizedGet,
98
createGroupWithPatients,
109
createPatientsRawData,
1110
createPractitionersRawData,
1211
exportGroup,
1312
findGroupByPractitionerNpi,
1413
} from '../dpc-api-client.js';
14+
import {
15+
monitorJob,
16+
} from './smoke_test_utils.js';
1517

16-
17-
// We allow errors in local because we don't need to test our own connection to BFD
18-
const JOB_OUPUT_ERROR_LENGTH = __ENV.ENVIRONMENT == 'local' ? 1 : 0;
19-
// Our WAF rate limits us to 300 requests every 5 minutes, so don't poll too often
20-
const EXPORT_POLL_INTERVAL_SEC = __ENV.ENVIRONMENT == 'local' ? 1 : 20;
2118
const practitionerBundle = __ENV.ENVIRONMENT == 'prod' ? open('./resources/prod_provider_bundle.json') : open('./resources/provider_bundle.json');
2219
const patientBundle = __ENV.ENVIRONMENT == 'prod' ? open('./resources/prod_patient_bundle-dpr.json') : open('./resources/patient_bundle-dpr.json');
2320
const associationsFile = __ENV.ENVIRONMENT == 'prod' ? open('./resources/prod_test_associations.csv') : open('./resources/test_associations-dpr.csv');
@@ -117,41 +114,6 @@ function exportGroups(token, orgId, practitioners) {
117114
}
118115
}
119116

120-
function monitorJob(token, jobUrl){
121-
// Loop until it isn't 202
122-
// NB: because we are not refreshing the token, it will eventually get a 401
123-
let jobResponse = authorizedGet(token, jobUrl);
124-
while(jobResponse.status === 202){
125-
sleep(EXPORT_POLL_INTERVAL_SEC);
126-
jobResponse = authorizedGet(token, jobUrl);
127-
}
128-
129-
// We got a rare exception when testing, so try is to make sure we capture the problem
130-
try {
131-
const checkJobResponse = check(
132-
jobResponse,
133-
{
134-
'job response code was 200': res => res.status === 200,
135-
'no job output errors': res => res.json().error.length <= JOB_OUPUT_ERROR_LENGTH,
136-
}
137-
);
138-
139-
if (!checkJobResponse) {
140-
if (jobResponse.status == 401) {
141-
console.error(`JOB TIMED OUT FOR TEST - MAYBE NOT FAIL: ${jobUrl}`);
142-
} else if (jobResponse.json().error) {
143-
console.error(`Too many errors in job output ${jobResponse.json().error.length}: ${jobUrl}`);
144-
} else {
145-
console.error(`Bad response code when checking job output ${jobResponse.status} ${jobUrl}`);
146-
}
147-
}
148-
} catch (error) {
149-
console.error(`Error thrown parsing ${jobResponse.body}: ${jobUrl}`)
150-
console.error(error);
151-
exec.test.fail();
152-
}
153-
}
154-
155117
function createGroups(token, orgId, patients, practitioners) {
156118
// Map of practitioners to their patients
157119
const practitionerPatientMap = practitionerNpiPatientMbiMap();
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*global console*/
2+
/* eslint no-console: "off" */
3+
4+
import { check } from 'k6';
5+
import { generateDPCToken } from '../generate-dpc-token.js';
6+
import {
7+
createPatientsBatch,
8+
patientEverything,
9+
createPractitioners,
10+
} from '../dpc-api-client.js';
11+
import {
12+
monitorJob,
13+
} from './smoke_test_utils.js';
14+
15+
const practitionerNpi = __ENV.ENVIRONMENT == 'prod' ? "1234329724" : "3247281157";
16+
17+
export async function checkPatientEverythingExportWorkflow(data) {
18+
const orgId = data.orgId;
19+
const token = generateDPCToken(orgId, data.goldenMacaroon);
20+
21+
// Uploading practitioner
22+
const createPractitionerResponse = createPractitioners(token, practitionerNpi);
23+
const checkUploadPractitioners = check(
24+
createPractitionerResponse,
25+
{
26+
'upload single practitioner returns 200': res => res.status == 200,
27+
'uploaded one practitioner': res => (res.json().entry || '').length == 1,
28+
}
29+
)
30+
if (!checkUploadPractitioners) {
31+
console.error(`Failed to upload single practitioner ${createPractitionerResponse.body}`);
32+
}
33+
34+
// Uploading Patient
35+
const patientMbi = "1SQ3F00AA00"; // valid BFD patient
36+
const uploadPatientResponse = createPatientsBatch(token, [patientMbi]);
37+
const checkUploadPatient = check(
38+
uploadPatientResponse,
39+
{
40+
'upload single patient returns 200': res => res.status == 200,
41+
'uploaded one patient': res => (res.json().entry || '').length == 1,
42+
}
43+
)
44+
if (!checkUploadPatient) {
45+
console.error(`Failed to upload patients ${uploadPatientResponse.body}`);
46+
}
47+
48+
// Abort if either upload failed
49+
if (!checkUploadPractitioners || !checkUploadPatient) {
50+
return;
51+
}
52+
53+
// Create Group for practitioner and patient
54+
const practitionerId = createPractitionerResponse.json().entry[0].resource.id;
55+
const patientId = uploadPatientResponse.json().entry[0].resource.id;
56+
57+
// Patient everything export
58+
const patientEverythingResponse = patientEverything(token, orgId, practitionerId, patientId);
59+
const checkPatientEverything = check(
60+
patientEverythingResponse,
61+
{
62+
'get patient everything returns 200': res => res.status == 200,
63+
'response returns bundle with data': (res) => {
64+
const resJson = res.json();
65+
return resJson.resourceType == "Bundle" && (resJson.total > 0 || (resJson.entry && resJson.entry.length > 0));
66+
},
67+
}
68+
)
69+
70+
if (!checkPatientEverything){
71+
console.error(`Failed to call patient everything for ${patientId}: ${patientEverythingResponse.body}`);
72+
}
73+
74+
// Patient everything export async
75+
const patientEverythingAsyncResponse = patientEverything(token, orgId, practitionerId, patientId, "respond-async");
76+
const checkPatientEverythingAsync = check(
77+
patientEverythingAsyncResponse,
78+
{
79+
'get patient everything async returns 202': res => res.status == 202,
80+
'response location header present': (res) => !!(res?.headers['Content-Location']),
81+
}
82+
)
83+
84+
if (!checkPatientEverythingAsync) {
85+
console.error(`Failed to call patient everything async for ${patientId}: ${patientEverythingAsyncResponse.body}`);
86+
}
87+
88+
// Verify patient everything job succeeds
89+
const jobUrl = patientEverythingAsyncResponse.headers['Content-Location'];
90+
monitorJob(token, jobUrl);
91+
92+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*global console*/
2+
/* eslint no-console: "off" */
3+
import { check, sleep } from 'k6';
4+
import exec from 'k6/execution'
5+
import {
6+
authorizedGet,
7+
} from '../dpc-api-client.js';
8+
9+
// We allow errors in local because we don't need to test our own connection to BFD
10+
const JOB_OUPUT_ERROR_LENGTH = __ENV.ENVIRONMENT == 'local' ? 1 : 0;
11+
12+
// Our WAF rate limits us to 300 requests every 5 minutes, so don't poll too often
13+
const EXPORT_POLL_INTERVAL_SEC = __ENV.ENVIRONMENT == 'local' ? 1 : 20;
14+
15+
export async function monitorJob(token, jobUrl){
16+
// Loop until it isn't 202
17+
// NB: because we are not refreshing the token, it will eventually get a 401
18+
let jobResponse = authorizedGet(token, jobUrl);
19+
while(jobResponse.status === 202){
20+
sleep(EXPORT_POLL_INTERVAL_SEC);
21+
jobResponse = authorizedGet(token, jobUrl);
22+
}
23+
24+
// We got a rare exception when testing, so try is to make sure we capture the problem
25+
try {
26+
const checkJobResponse = check(
27+
jobResponse,
28+
{
29+
'job response code was 200': res => res.status === 200,
30+
'no job output errors': res => res.json().error.length <= JOB_OUPUT_ERROR_LENGTH,
31+
}
32+
);
33+
34+
if (!checkJobResponse) {
35+
if (jobResponse.status == 401) {
36+
console.error(`JOB TIMED OUT FOR TEST - MAYBE NOT FAIL: ${jobUrl}`);
37+
} else if (jobResponse.json().error) {
38+
console.error(`Too many errors in job output ${jobResponse.json().error.length}: ${jobUrl}`);
39+
} else {
40+
console.error(`Bad response code when checking job output ${jobResponse.status} ${jobUrl}`);
41+
}
42+
}
43+
} catch (error) {
44+
console.error(`Error thrown parsing ${jobResponse.body}: ${jobUrl}`)
45+
console.error(error);
46+
exec.test.fail();
47+
}
48+
}

0 commit comments

Comments
 (0)