Skip to main content
Glama
routes.test.ts27.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { ContentType, getReferenceString } from '@medplum/core'; import type { Bundle, Meta, Organization, Patient, Reference } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../app'; import { registerNew } from '../auth/register'; import { loadTestConfig } from '../config/loader'; import { DatabaseMode, getDatabasePool } from '../database'; import { addTestUser, bundleContains, createTestProject, initTestAuth, withTestContext } from '../test.setup'; const app = express(); let accessToken: string; let legacyJsonResponseAccessToken: string; let searchOnReaderAccessToken: string; let testPatient: WithId<Patient>; let patientId: string; let patientVersionId: string; describe('FHIR Routes', () => { beforeAll(async () => { const config = await loadTestConfig(); await initApp(app, config); accessToken = await initTestAuth(); legacyJsonResponseAccessToken = await initTestAuth({ project: { systemSetting: [{ name: 'legacyFhirJsonResponseFormat', valueBoolean: true }] }, }); searchOnReaderAccessToken = await initTestAuth({ project: { systemSetting: [{ name: 'searchOnReader', valueBoolean: true }] }, }); for (const token of [accessToken, searchOnReaderAccessToken]) { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + token) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient', name: [ { given: ['Alice'], family: 'Smith', }, ], }); expect(res.status).toBe(201); if (token === accessToken) { testPatient = res.body as WithId<Patient>; patientId = testPatient.id; patientVersionId = (testPatient.meta as Meta).versionId as string; } } }); afterEach(() => { jest.restoreAllMocks(); }); afterAll(async () => { await shutdownApp(); }); test('Get CapabilityStatement anonymously', async () => { const res = await request(app).get(`/fhir/R4/metadata`); expect(res.status).toBe(200); expect(res.body.resourceType).toStrictEqual('CapabilityStatement'); }); test('Get CapabilityStatement authenticated', async () => { const res = await request(app) .get(`/fhir/R4/metadata`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); expect(res.body.resourceType).toStrictEqual('CapabilityStatement'); }); test('Get versions anonymously', async () => { const res = await request(app).get(`/fhir/R4/$versions`); expect(res.status).toBe(200); expect(res.body).toMatchObject({ versions: ['4.0'], default: '4.0' }); }); test('Get SMART-on-FHIR configuration', async () => { const res = await request(app).get(`/fhir/R4/.well-known/smart-configuration`); expect(res.status).toBe(200); expect(res.headers['content-type']).toStrictEqual('application/json; charset=utf-8'); // Required fields: https://build.fhir.org/ig/HL7/smart-app-launch/conformance.html#response expect(res.body.authorization_endpoint).toMatch(/\/oauth2\/authorize$/); expect(res.body.grant_types_supported).toEqual( expect.arrayContaining(['authorization_code', 'refresh_token', 'client_credentials']) ); expect(res.body.token_endpoint).toMatch(/\/oauth2\/token$/); expect(res.body.capabilities).toBeDefined(); expect(res.body.code_challenge_methods_supported).toEqual(expect.arrayContaining(['S256'])); // Additional fields expect(res.body.introspection_endpoint).toMatch(/\/oauth2\/introspect$/); const res2 = await request(app).get(`/fhir/R4/.well-known/smart-styles.json`); expect(res2.status).toBe(200); expect(res2.headers['content-type']).toStrictEqual('application/json; charset=utf-8'); }); test('Invalid JSON', async () => { const res = await request(app) .post(`/fhir/R4/`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send('not-json'); expect(res.status).toBe(400); }); test.each<['standard' | 'legacy']>([['standard'], ['legacy']])( 'Create resource success with %s FHIR JSON response format', async (jsonFormat) => { const patientToCreate: Patient = { resourceType: 'Patient', identifier: [] }; const token = jsonFormat === 'standard' ? accessToken : legacyJsonResponseAccessToken; const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + token) .set('Content-Type', ContentType.FHIR_JSON) .send(patientToCreate); expect(res.status).toBe(201); expect(res.body.resourceType).toStrictEqual('Patient'); expect(res.headers.location).toContain('Patient'); expect(res.headers.location).toContain(res.body.id); const patient = res.body; const res2 = await request(app) .get(`/fhir/R4/Patient/` + patient.id) .set('Authorization', 'Bearer ' + token); expect(res2.status).toBe(200); if (jsonFormat === 'standard') { expect(patient.identifier).toBeUndefined(); } else { expect(patient.identifier).toStrictEqual([]); } } ); test('Create resource invalid resource type', async () => { const res = await request(app) .post(`/fhir/R4/Patientx`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patientx' }); expect(res.status).toBe(400); }); test('Create resource incorrect resource type', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patientx' }); expect(res.status).toBe(400); }); test('Create resource invalid content type', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.TEXT) .send('hello'); expect(res.status).toBe(400); }); test('Read resource', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); }); test('Read resource _pretty', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}?_pretty=true`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); expect(res.text).toStrictEqual(JSON.stringify(res.body, undefined, 2)); }); test('Read resource invalid UUID', async () => { const res = await request(app) .get(`/fhir/R4/Patient/123`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Read resource invalid resource type', async () => { const res = await request(app) .get(`/fhir/R4/Patientx/${patientId}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(400); }); test('Read resource not found', async () => { const res = await request(app) .get(`/fhir/R4/Patient/8a54c7db-654b-4c3d-ba85-e0909f51c12c`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Read resource minimal', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .set('Prefer', 'return=minimal'); expect(res.status).toBe(200); expect(res.text).toStrictEqual(''); }); test('Read resource history', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}/_history`) .query({ _count: 2, _offset: '0' }) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); const bundle = res.body as Bundle; expect(bundle.entry).toBeDefined(); expect(bundle.entry).toHaveLength(1); }); test('Read resource history invalid UUID', async () => { const res = await request(app) .get(`/fhir/R4/Patient/123/_history`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Read resource history invalid resource type', async () => { const res = await request(app) .get(`/fhir/R4/xyz/${patientId}/_history`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(400); }); test('Read resource version', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}/_history/${patientVersionId}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); // Expect "ETag" header to start with "W/" (weak validator) expect(res.headers.etag).toBeDefined(); expect(res.headers.etag).toContain('W/'); }); test('Read resource version invalid UUID', async () => { const res = await request(app) .get(`/fhir/R4/Patient/123/_history/${patientVersionId}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Read resource version invalid version UUID', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}/_history/123`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Read resource version invalid resource type', async () => { const res = await request(app) .get(`/fhir/R4/xyz/${patientId}/_history/${patientVersionId}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(400); }); test('Read resource version not found', async () => { const res = await request(app) .get(`/fhir/R4/Patient/${patientId}/_history/${randomUUID()}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Update resource', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); const patient = res.body; const res2 = await request(app) .put(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken) .send({ ...patient, active: true }); expect(res2.status).toBe(200); }); test('Update resource not modified', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); const patient = res.body; const res2 = await request(app) .put(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken) .send(patient); expect(res2.status).toBe(200); expect(res2.body.meta.versionId).toStrictEqual(patient.meta.versionId); }); test('Update resource not modified with empty strings', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient', managingOrganization: { reference: 'Organization/123', }, }); expect(res.status).toBe(201); const patient = res.body; const res2 = await request(app) .put(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken) .send({ ...patient, managingOrganization: { reference: 'Organization/123', display: '', }, }); expect(res2.status).toBe(200); expect(res2.body.meta.versionId).toStrictEqual(patient.meta.versionId); }); test('Update resource invalid', async () => { const res = await request(app) .put(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .send({}); expect(res.status).toBe(400); }); test('Update resource wrong content-type', async () => { const res = await request(app) .put(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.TEXT) .send('hello'); expect(res.status).toBe(400); }); test('Update resource missing ID', async () => { const res = await request(app) .put(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient' }); expect(res.status).toBe(400); }); test('Update resource not found', async () => { const res = await request(app) .put(`/fhir/R4/Patient/${randomUUID()}`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient' }); expect(res.status).toBe(400); }); test('Update resource with precondition', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); const patient = res.body; const res2 = await request(app) .put(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken) .set('If-Match', 'W/"' + patient.meta?.versionId + '"') .send({ ...patient, active: true }); expect(res2.status).toBe(200); }); test('Update resource with failed precondition', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); const patient = res.body; const res2 = await request(app) .put(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken) .set('If-Match', 'W/"bad-id"') .send({ ...patient, active: true }); expect(res2.status).toBe(412); }); test('Delete resource', async () => { const res = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res.status).toBe(201); const patient = res.body; const res2 = await request(app) .delete(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res2.status).toBe(200); const res3 = await request(app) .get(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res3.status).toBe(410); }); test('Delete resource invalid UUID', async () => { const res = await request(app) .delete(`/fhir/R4/Patient/123`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(404); }); test('Delete resource invalid resource type', async () => { const res = await request(app) .delete(`/fhir/R4/xyz/${patientId}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(400); }); test('Patch resource not found', async () => { const res = await request(app) .patch(`/fhir/R4/Patient/${randomUUID()}`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.JSON_PATCH) .send([ { op: 'add', path: '/generalPractitioner', value: [{ reference: 'Practitioner/123' }], }, ]); expect(res.status).toBe(404); }); test('Patch resource wrong content type', async () => { const res = await request(app) .patch(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.TEXT) .send('hello'); expect(res.status).toBe(400); }); test('Patch resource invalid result', async () => { const res = await request(app) .patch(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.JSON_PATCH) .send([{ op: 'remove', path: '/resourceType' }]); expect(res.status).toBe(400); }); test('Patch resource success', async () => { const res = await request(app) .patch(`/fhir/R4/Patient/${patientId}`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.JSON_PATCH) .send([ { op: 'add', path: '/generalPractitioner', value: [{ reference: 'Practitioner/123' }], }, ]); expect(res.status).toBe(200); }); describe.each<['writer' | 'reader']>([['writer'], ['reader']])('On %s', (repoMode) => { test('Search', async () => { const readerSpy = jest.spyOn(getDatabasePool(DatabaseMode.READER), 'query'); const writerSpy = jest.spyOn(getDatabasePool(DatabaseMode.WRITER), 'query'); const token = repoMode === 'writer' ? accessToken : searchOnReaderAccessToken; const res = await request(app) .get(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + token); expect(res.status).toBe(200); if (repoMode === 'writer') { expect(writerSpy).toHaveBeenCalledTimes(1); expect(readerSpy).toHaveBeenCalledTimes(0); } else { expect(writerSpy).toHaveBeenCalledTimes(0); expect(readerSpy).toHaveBeenCalledTimes(1); } }); test('Search by POST', async () => { const readerSpy = jest.spyOn(getDatabasePool(DatabaseMode.READER), 'query'); const writerSpy = jest.spyOn(getDatabasePool(DatabaseMode.WRITER), 'query'); const token = repoMode === 'writer' ? accessToken : searchOnReaderAccessToken; const res = await request(app) .post(`/fhir/R4/Patient/_search`) .set('Authorization', 'Bearer ' + token) .type('form'); expect(res.status).toBe(200); const result = res.body as Bundle; expect(result.type).toStrictEqual('searchset'); expect(result.entry?.length).toBeGreaterThan(0); if (repoMode === 'writer') { expect(writerSpy).toHaveBeenCalledTimes(1); expect(readerSpy).toHaveBeenCalledTimes(0); } else { expect(writerSpy).toHaveBeenCalledTimes(0); expect(readerSpy).toHaveBeenCalledTimes(1); } }); test('Search multiple resource types with _type', async () => withTestContext(async () => { const { accessToken } = await createTestProject({ withAccessToken: true, project: repoMode === 'reader' ? { systemSetting: [{ name: 'searchOnReader', valueBoolean: true }] } : undefined, }); const res1 = await request(app) .post('/fhir/R4/Patient') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient' }); expect(res1.status).toBe(201); const res2 = await request(app) .post('/fhir/R4/Observation') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Observation', status: 'final', code: { text: 'test' }, subject: { reference: `Patient/${res1.body.id}` }, }); expect(res2.status).toBe(201); const readerSpy = jest.spyOn(getDatabasePool(DatabaseMode.READER), 'query'); const writerSpy = jest.spyOn(getDatabasePool(DatabaseMode.WRITER), 'query'); const res3 = await request(app) .get('/fhir/R4?_type=Patient,Observation') .set('Authorization', 'Bearer ' + accessToken); expect(res3.status).toBe(200); const patient = res1.body; const obs = res2.body; const bundle = res3.body; expect(bundle.entry?.length).toBe(2); expect(bundleContains(bundle, patient)).toBeTruthy(); expect(bundleContains(bundle, obs)).toBeTruthy(); if (repoMode === 'writer') { expect(writerSpy).toHaveBeenCalledTimes(1); expect(readerSpy).toHaveBeenCalledTimes(0); } else { expect(writerSpy).toHaveBeenCalledTimes(0); expect(readerSpy).toHaveBeenCalledTimes(1); } // Also verify that trailing slash works const res4 = await request(app) .get('/fhir/R4/?_type=Patient,Observation') .set('Authorization', 'Bearer ' + accessToken); expect(res4.status).toBe(200); const bundle2 = res4.body; expect(bundle2.entry?.length).toBe(2); expect(bundleContains(bundle2, patient)).toBeTruthy(); expect(bundleContains(bundle2, obs)).toBeTruthy(); })); }); test('Search invalid resource', async () => { const res = await request(app) .get(`/fhir/R4/Patientx`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(400); }); test('Search invalid search parameter', async () => { const res = await request(app) .get(`/fhir/R4/ServiceRequest?basedOn=ServiceRequest/123`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Unknown search parameter: basedOn'); }); test('Validate create success', async () => { const res = await request(app) .post(`/fhir/R4/Patient/$validate`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient' }); expect(res.status).toBe(200); }); test('Validate create failure', async () => { const res = await request(app) .post(`/fhir/R4/Patient/$validate`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient', badProperty: 'bad' }); expect(res.status).toBe(400); }); test('Validate wrong content type', async () => { const res = await request(app) .post(`/fhir/R4/Patient/$validate`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.TEXT) .send('hello'); expect(res.status).toBe(400); }); test('Reindex resource access denied', async () => { const res = await request(app) .post(`/fhir/R4/Patient/${patientId}/$reindex`) .set('Authorization', 'Bearer ' + accessToken) .send({}); expect(res.status).toBe(403); }); test('Resend subscriptions access denied', async () => { const res = await request(app) .post(`/fhir/R4/Patient/${patientId}/$resend`) .set('Authorization', 'Bearer ' + accessToken) .send({}); expect(res.status).toBe(403); }); test('Resend as project admin', async () => { const { profile, accessToken } = await withTestContext(() => registerNew({ firstName: 'Alice', lastName: 'Smith', projectName: 'Alice Project', email: `alice${randomUUID()}@example.com`, password: 'password!@#', }) ); const res = await request(app) .post(`/fhir/R4/${getReferenceString(profile)}/$resend`) .set('Authorization', 'Bearer ' + accessToken) .send({}); expect(res.status).toBe(200); // Resend with verbose=true const res2 = await request(app) .post(`/fhir/R4/${getReferenceString(profile)}/$resend`) .set('Authorization', 'Bearer ' + accessToken) .send({ verbose: true }); expect(res2.status).toBe(200); // Resend with subscription option const res3 = await request(app) .post(`/fhir/R4/${getReferenceString(profile)}/$resend`) .set('Authorization', 'Bearer ' + accessToken) .send({ subscription: 'Subscription/123' }); expect(res3.status).toBe(200); }); test('ProjectMembership with null access policy', async () => withTestContext(async () => { const adminRegistration = await createTestProject({ membership: { admin: true }, withRepo: true, withAccessToken: true, }); expect(adminRegistration).toBeDefined(); const normalRegistration = await addTestUser(adminRegistration.project); expect(normalRegistration).toBeDefined(); const membership = normalRegistration.membership; const res1 = await request(app) .get(`/fhir/R4/ProjectMembership/${membership.id}`) .set('Authorization', 'Bearer ' + adminRegistration.accessToken); expect(res1.status).toBe(200); const res2 = await request(app) .get(`/fhir/R4/ProjectMembership/${membership.id}`) .set('Authorization', 'Bearer ' + normalRegistration.accessToken); expect(res2.status).toBe(403); const res3 = await request(app) .put(`/fhir/R4/ProjectMembership/${membership.id}`) .set('Authorization', 'Bearer ' + normalRegistration.accessToken) .send({ ...membership, accessPolicy: undefined }); expect(res3.status).toBe(403); })); test('Set accounts on create', async () => { const { project, accessToken } = await createTestProject({ withAccessToken: true }); const res1 = await request(app) .post('/fhir/R4/Organization') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Organization' }); expect(res1.status).toBe(201); const account = res1.body as Organization; const res2 = await request(app) .post('/fhir/R4/Questionnaire') .set('Authorization', 'Bearer ' + accessToken) .set('X-Medplum', 'extended') .send({ resourceType: 'Questionnaire', meta: { accounts: [{ reference: getReferenceString(account) }] }, title: 'Questionnaire A.1', status: 'active', item: [ { linkId: '1', text: 'How would you rate your overall experience?', type: 'choice', answerOption: [ { valueCoding: { system: 'http://example.org/rating', code: '5', display: 'Excellent', }, }, ], }, ], }); expect(res2.status).toBe(201); expect(res2.body.meta?.accounts?.length).toBe(1); expect(res2.body.meta?.accounts?.[0].reference).toBe(getReferenceString(account)); // There should be 2 compartments: // 1: the project // 2: the account organization const compartments = res2.body.meta?.compartment as Reference[]; expect(compartments.length).toBe(2); expect(compartments.find((c) => c.reference === getReferenceString(project))).toBeDefined(); expect(compartments.find((c) => c.reference === getReferenceString(account))).toBeDefined(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server