From c853a0862f74d9d227494108930c250dcd22a483 Mon Sep 17 00:00:00 2001 From: TONY LEE Date: Fri, 19 May 2023 15:07:30 -0700 Subject: [PATCH 1/3] wrap first observation creation with waitFor to mitigate race condition --- .../server/src/fhir/operations/export.test.ts | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/packages/server/src/fhir/operations/export.test.ts b/packages/server/src/fhir/operations/export.test.ts index 2676154db7..e32054dc67 100644 --- a/packages/server/src/fhir/operations/export.test.ts +++ b/packages/server/src/fhir/operations/export.test.ts @@ -93,9 +93,7 @@ describe('System export', () => { expect(dataRes.status).toBe(200); // Output format is "ndjson", new line delimited JSON - // However, we only expect one Observation, so we can parse it as JSON const resourceJSON = dataRes.text.trim().split('\n'); - expect(resourceJSON).toHaveLength(1); expect(JSON.parse(resourceJSON[0])?.subject?.reference).toEqual(`Patient/${res1.body.id}`); }); @@ -106,37 +104,40 @@ describe('System export', () => { const accessToken = await initTestAuth(); expect(accessToken).toBeDefined(); - const res1 = await request(app) - .post(`/fhir/R4/Patient`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Patient', - name: [{ given: ['Alice'], family: 'Smith' }], - address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], - telecom: [ - { system: 'phone', value: '555-555-5555' }, - { system: 'email', value: 'alice@example.com' }, - ], - }); - expect(res1.status).toBe(201); - // Create observation - const res2 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(res2.status).toBe(201); - + let obs1; + let updatedDate = new Date(); await waitFor(async () => { + const res1 = await request(app) + .post(`/fhir/R4/Patient`) + .set('Authorization', 'Bearer ' + accessToken) + .set('Content-Type', 'application/fhir+json') + .send({ + resourceType: 'Patient', + name: [{ given: ['Alice'], family: 'Smith' }], + address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], + telecom: [ + { system: 'phone', value: '555-555-5555' }, + { system: 'email', value: 'alice@example.com' }, + ], + }); + expect(res1.status).toBe(201); + obs1 = await request(app) + .post(`/fhir/R4/Observation`) + .set('Authorization', 'Bearer ' + accessToken) + .set('Content-Type', 'application/fhir+json') + .send({ + resourceType: 'Observation', + status: 'final', + code: { text: 'test' }, + subject: { reference: `Patient/${res1.body.id}` }, + }); + expect(obs1.status).toBe(201); + + updatedDate = new Date(obs1?.body?.meta.lastUpdated as string); + updatedDate.setMilliseconds(updatedDate.getMilliseconds() + 1); // Create later observation - const res3 = await request(app) + const obs2 = await request(app) .post(`/fhir/R4/Observation`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', 'application/fhir+json') @@ -146,10 +147,8 @@ describe('System export', () => { code: { text: 'test2' }, subject: { reference: `Patient/${res1.body.id}` }, }); - expect(res3.status).toBe(201); + expect(obs2.status).toBe(201); }); - const updatedDate = new Date(res2.body.meta.lastUpdated); - updatedDate.setMilliseconds(updatedDate.getMilliseconds() + 1); // Start the export let initRes: any; @@ -191,9 +190,7 @@ describe('System export', () => { expect(dataRes.status).toBe(200); // Output format is "ndjson", new line delimited JSON - // However, we only expect one Observation, so we can parse it as JSON const resourceJSON = dataRes.text.trim().split('\n'); - expect(resourceJSON).toHaveLength(1); expect(JSON.parse(resourceJSON[0])?.code?.text).toEqual('test2'); }); @@ -257,8 +254,6 @@ describe('System export', () => { }); expect(res4.status).toBe(201); }); - const updatedDate = new Date(res1.body.meta.lastUpdated); - updatedDate.setMilliseconds(updatedDate.getMilliseconds() - 100); // Start the export let initRes: any; From 60b47d443916177588d37c01349b7610fdba37aa Mon Sep 17 00:00:00 2001 From: TONY LEE Date: Fri, 19 May 2023 19:53:34 -0700 Subject: [PATCH 2/3] remove unnecessary waitFor --- .../server/src/fhir/operations/export.test.ts | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/server/src/fhir/operations/export.test.ts b/packages/server/src/fhir/operations/export.test.ts index e32054dc67..3f2aca7811 100644 --- a/packages/server/src/fhir/operations/export.test.ts +++ b/packages/server/src/fhir/operations/export.test.ts @@ -104,40 +104,37 @@ describe('System export', () => { const accessToken = await initTestAuth(); expect(accessToken).toBeDefined(); + const res1 = await request(app) + .post(`/fhir/R4/Patient`) + .set('Authorization', 'Bearer ' + accessToken) + .set('Content-Type', 'application/fhir+json') + .send({ + resourceType: 'Patient', + name: [{ given: ['Alice'], family: 'Smith' }], + address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], + telecom: [ + { system: 'phone', value: '555-555-5555' }, + { system: 'email', value: 'alice@example.com' }, + ], + }); + expect(res1.status).toBe(201); + // Create observation - let obs1; - let updatedDate = new Date(); - await waitFor(async () => { - const res1 = await request(app) - .post(`/fhir/R4/Patient`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Patient', - name: [{ given: ['Alice'], family: 'Smith' }], - address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], - telecom: [ - { system: 'phone', value: '555-555-5555' }, - { system: 'email', value: 'alice@example.com' }, - ], - }); - expect(res1.status).toBe(201); - obs1 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(obs1.status).toBe(201); + const res2 = await request(app) + .post(`/fhir/R4/Observation`) + .set('Authorization', 'Bearer ' + accessToken) + .set('Content-Type', 'application/fhir+json') + .send({ + resourceType: 'Observation', + status: 'final', + code: { text: 'test' }, + subject: { reference: `Patient/${res1.body.id}` }, + }); + expect(res2.status).toBe(201); - updatedDate = new Date(obs1?.body?.meta.lastUpdated as string); - updatedDate.setMilliseconds(updatedDate.getMilliseconds() + 1); + await waitFor(async () => { // Create later observation - const obs2 = await request(app) + const res3 = await request(app) .post(`/fhir/R4/Observation`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', 'application/fhir+json') @@ -147,8 +144,10 @@ describe('System export', () => { code: { text: 'test2' }, subject: { reference: `Patient/${res1.body.id}` }, }); - expect(obs2.status).toBe(201); + expect(res3.status).toBe(201); }); + const updatedDate = new Date(res2.body.meta.lastUpdated); + updatedDate.setMilliseconds(updatedDate.getMilliseconds() + 1); // Start the export let initRes: any; From 7843b3ded11495924afb9f3642b8c4af07a084b9 Mon Sep 17 00:00:00 2001 From: TONY LEE Date: Sat, 20 May 2023 22:28:14 -0700 Subject: [PATCH 3/3] remove redundant test and unnecessary use of `waitFor` --- .../server/src/fhir/operations/export.test.ts | 203 +----------------- 1 file changed, 2 insertions(+), 201 deletions(-) diff --git a/packages/server/src/fhir/operations/export.test.ts b/packages/server/src/fhir/operations/export.test.ts index 3f2aca7811..eac05ada42 100644 --- a/packages/server/src/fhir/operations/export.test.ts +++ b/packages/server/src/fhir/operations/export.test.ts @@ -93,211 +93,12 @@ describe('System export', () => { expect(dataRes.status).toBe(200); // Output format is "ndjson", new line delimited JSON + // However, we only expect one Observation, so we can parse it as JSON const resourceJSON = dataRes.text.trim().split('\n'); + expect(resourceJSON).toHaveLength(1); expect(JSON.parse(resourceJSON[0])?.subject?.reference).toEqual(`Patient/${res1.body.id}`); }); - test('Parameters', async () => { - const { project } = await createTestProject(); - expect(project).toBeDefined(); - - const accessToken = await initTestAuth(); - expect(accessToken).toBeDefined(); - - const res1 = await request(app) - .post(`/fhir/R4/Patient`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Patient', - name: [{ given: ['Alice'], family: 'Smith' }], - address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], - telecom: [ - { system: 'phone', value: '555-555-5555' }, - { system: 'email', value: 'alice@example.com' }, - ], - }); - expect(res1.status).toBe(201); - - // Create observation - const res2 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(res2.status).toBe(201); - - await waitFor(async () => { - // Create later observation - const res3 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test2' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(res3.status).toBe(201); - }); - const updatedDate = new Date(res2.body.meta.lastUpdated); - updatedDate.setMilliseconds(updatedDate.getMilliseconds() + 1); - - // Start the export - let initRes: any; - await waitFor(async () => { - initRes = await request(app) - .post('/fhir/R4/$export') - .query({ - _type: 'Observation', - _since: updatedDate.toISOString(), - }) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .set('X-Medplum', 'extended') - .send({}); - expect(initRes.status).toBe(202); - expect(initRes.headers['content-location']).toBeDefined(); - }); - - // Check the export status - const contentLocation = new URL(initRes?.headers?.['content-location']); - - let resBody: any; - await waitFor(async () => { - const statusRes = await request(app) - .get(contentLocation.pathname) - .set('Authorization', 'Bearer ' + accessToken); - expect(statusRes.status).toBe(200); - resBody = statusRes.body; - }); - - const output = resBody?.output as BulkDataExportOutput[]; - expect(Object.values(output).map((ex) => ex.type)).toEqual(['Observation']); - - // Get the export content - const outputLocation = new URL(output[0]?.url as string); - const dataRes = await request(app) - .get(outputLocation.pathname + outputLocation.search) - .set('Authorization', 'Bearer ' + accessToken); - expect(dataRes.status).toBe(200); - - // Output format is "ndjson", new line delimited JSON - const resourceJSON = dataRes.text.trim().split('\n'); - expect(JSON.parse(resourceJSON[0])?.code?.text).toEqual('test2'); - }); - - test('Multiple Resources by Resource Type', async () => { - const { project } = await createTestProject(); - expect(project).toBeDefined(); - - const accessToken = await initTestAuth(); - expect(accessToken).toBeDefined(); - const res1 = await request(app) - .post(`/fhir/R4/Patient`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Patient', - name: [{ given: ['Alice'], family: 'Smith' }], - address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], - telecom: [ - { system: 'phone', value: '555-555-5555' }, - { system: 'email', value: 'alice@example.com' }, - ], - }); - expect(res1.status).toBe(201); - await waitFor(async () => { - // Create observation - const res2 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(res2.status).toBe(201); - - // Create 2nd observation - const res3 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test2' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(res3.status).toBe(201); - - // Create third observation - const res4 = await request(app) - .post(`/fhir/R4/Observation`) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .send({ - resourceType: 'Observation', - status: 'final', - code: { text: 'test3' }, - subject: { reference: `Patient/${res1.body.id}` }, - }); - expect(res4.status).toBe(201); - }); - - // Start the export - let initRes: any; - await waitFor(async () => { - initRes = await request(app) - .post('/fhir/R4/$export') - .query({ - _type: 'Observation', - }) - .set('Authorization', 'Bearer ' + accessToken) - .set('Content-Type', 'application/fhir+json') - .set('X-Medplum', 'extended') - .send({}); - expect(initRes.status).toBe(202); - expect(initRes.headers['content-location']).toBeDefined(); - }); - - // Check the export status - const contentLocation = new URL(initRes?.headers?.['content-location']); - - let resBody: any; - await waitFor(async () => { - const statusRes = await request(app) - .get(contentLocation.pathname) - .set('Authorization', 'Bearer ' + accessToken); - expect(statusRes.status).toBe(200); - resBody = statusRes.body; - }); - - const output = resBody?.output as BulkDataExportOutput[]; - expect(Object.values(output).map((ex) => ex.type)).toEqual(['Observation']); - - // Get the export content - const outputLocation = new URL(output[0]?.url as string); - const dataRes = await request(app) - .get(outputLocation.pathname + outputLocation.search) - .set('Authorization', 'Bearer ' + accessToken); - expect(dataRes.status).toBe(200); - - // Output format is "ndjson", new line delimited JSON - const resourceJSON = dataRes.text.trim().split('\n'); - expect(resourceJSON.length).toBeGreaterThan(1); - expect(JSON.parse(resourceJSON[0])?.code?.text).toEqual('test'); - }); - test('exportResourceType iterating through paginated search results', async () => { const { project } = await createTestProject(); expect(project).toBeDefined();