Skip to main content
Glama
set-accounts.test.ts22.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { ContentType, createReference } from '@medplum/core'; import type { AsyncJob, Bundle, Communication, DiagnosticReport, Login, Observation, Organization, Patient, Project, ProjectMembership, UserConfiguration, } from '@medplum/fhirtypes'; import type { Job } from 'bullmq'; import express from 'express'; import type { RateLimiterRes } from 'rate-limiter-flexible'; import { RateLimiterRedis } from 'rate-limiter-flexible'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { loadTestConfig } from '../../config/loader'; import { runInAsyncContext } from '../../context'; import { createTestProject, initTestAuth, waitForAsyncJob } from '../../test.setup'; import type { SetAccountsJobData } from '../../workers/set-accounts'; import { execSetAccountsJob, getSetAccountsQueue } from '../../workers/set-accounts'; import { setAccountsHandler } from './set-accounts'; const app = express(); let accessToken: string; let login: WithId<Login>; let membership: WithId<ProjectMembership>; let project: WithId<Project>; let observation: Observation; let diagnosticReport: DiagnosticReport; let patient: Patient; let organization1: Organization; let organization2: Organization; describe('Patient Set Accounts Operation', () => { beforeEach(async () => { const config = await loadTestConfig(); await initApp(app, config); ({ accessToken, login, membership, project } = await createTestProject({ withAccessToken: true, withClient: true, membership: { admin: true }, })); // Create organization const orgRes = await request(app) .post('/fhir/R4/Organization') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Organization' }); expect(orgRes.status).toBe(201); organization1 = orgRes.body as Organization; const orgRes2 = await request(app) .post('/fhir/R4/Organization') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Organization' }); expect(orgRes2.status).toBe(201); organization2 = orgRes2.body as Organization; // Create patient const res1 = await request(app) .post('/fhir/R4/Patient') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], } satisfies Patient); expect(res1.status).toBe(201); patient = res1.body as Patient; // Create observation const res2 = await request(app) .post('/fhir/R4/Observation') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Observation', status: 'final', code: { coding: [ { system: 'http://loinc.org', code: 'test-code', }, ], }, subject: createReference(patient), } satisfies Observation); expect(res2.status).toBe(201); observation = res2.body as Observation; //Create a diagnostic report const res3 = await request(app) .post('/fhir/R4/DiagnosticReport') .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'DiagnosticReport', subject: createReference(patient), status: 'final', code: { coding: [ { system: 'http://loinc.org', code: 'test-code', }, ], }, } satisfies DiagnosticReport); expect(res3.status).toBe(201); diagnosticReport = res3.body as DiagnosticReport; }); afterEach(async () => { await shutdownApp(); }); test('Updates target patient and compartment resources', async () => { // Execute the operation adding the organization to the patient's compartment const res3 = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, { name: 'accounts', valueReference: createReference(organization2), }, { name: 'propagate', valueBoolean: true, }, ], }); expect(res3.status).toBe(200); const result = res3.body; expect(result.parameter?.[0].name).toBe('resourcesUpdated'); expect(result.parameter?.[0].valueInteger).toBe(3); // Observation and DiagnosticReport //check if the accounts are updated on the patient const res4 = await request(app) .get(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res4.status).toBe(200); const updatedPatient = res4.body as Patient; expect(updatedPatient.meta?.accounts).toBeDefined(); expect(updatedPatient.meta?.accounts?.[0].reference).toBe(`Organization/${organization1.id}`); expect(updatedPatient.meta?.accounts?.[1].reference).toBe(`Organization/${organization2.id}`); // Check if accounts are updated on the observation const res5 = await request(app) .get(`/fhir/R4/Observation/${observation.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res5.status).toBe(200); const updatedObservation = res5.body as Observation; expect(updatedObservation.meta?.accounts).toBeDefined(); expect(updatedObservation.meta?.accounts?.[0].reference).toBe(`Organization/${organization1.id}`); expect(updatedObservation.meta?.accounts?.[1].reference).toBe(`Organization/${organization2.id}`); // Check if accounts are updated on the diagnostic report const res6 = await request(app) .get(`/fhir/R4/DiagnosticReport/${diagnosticReport.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res6.status).toBe(200); const updatedDiagnosticReport = res6.body as DiagnosticReport; expect(updatedDiagnosticReport.meta?.accounts).toBeDefined(); expect(updatedDiagnosticReport.meta?.accounts?.[0].reference).toBe(`Organization/${organization1.id}`); expect(updatedDiagnosticReport.meta?.accounts?.[1].reference).toBe(`Organization/${organization2.id}`); }); test('Resources returned in $patient-everything but NOT in the patient compartment are not updated', async () => { const res7 = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, { name: 'accounts', valueReference: createReference(organization2), }, { name: 'propagate', valueBoolean: true, }, ], }); expect(res7.status).toBe(200); const numberResourcesUpdated = res7.body.parameter?.[0].valueInteger; const res = await request(app) .get(`/fhir/R4/Patient/${patient.id}/$everything`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); const everything = res.body as Bundle; const allResources = everything.entry?.length ?? 0; const resourcesNotInCompartment = everything.entry?.filter((entry) => entry?.search?.mode !== 'match').length ?? 0; //Number of resources updated only includes the ones in the compartment, not other resources returned in $patient-everything expect(numberResourcesUpdated).toBe(allResources - resourcesNotInCompartment); }); test('Patient not found', async () => { const res = await request(app) .post(`/fhir/R4/Patient/not-found/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, ], }); expect(res.status).toBe(404); }); test('setAccountsHandler() called without an id', async () => { const res = await setAccountsHandler({ params: { id: '' }, method: 'POST', url: '/fhir/R4/Patient/$set-accounts', pathname: '/fhir/R4/Patient/$set-accounts', body: {}, query: {}, }); expect(res[0].issue?.[0]?.details?.text).toBe('Must specify resource type and ID'); }); test('Preserves other meta fields on compartment resources', async () => { //Create a Communication in Patient's compartment with a security tag const res1 = await request(app) .post(`/fhir/R4/Communication`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Communication', subject: createReference(patient), status: 'completed', meta: { security: [ { system: 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality', code: 'N', }, ], }, } satisfies Communication); expect(res1.status).toBe(201); const communication = res1.body as Communication; expect(communication.meta?.security).toBeDefined(); const res2 = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .set('x-medplum', 'extended') .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, { name: 'propagate', valueBoolean: true, }, ], }); expect(res2.status).toBe(200); const res4 = await request(app) .get(`/fhir/R4/Communication/${communication.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res4.status).toBe(200); const updatedCommunication = res4.body as Communication; expect(updatedCommunication.meta?.accounts).toHaveLength(1); expect(updatedCommunication.meta?.security).toBeDefined(); }); test('Preserves changes to accounts of compartment resources', async () => { const res1 = await request(app) .post(`/fhir/R4/Observation/${observation.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization2), }, ], }); expect(res1.status).toBe(200); const result = res1.body; expect(result.parameter?.[0].name).toBe('resourcesUpdated'); expect(result.parameter?.[0].valueInteger).toBe(1); // Observation only // Check if accounts are updated on the observation const res2 = await request(app) .get(`/fhir/R4/Observation/${observation.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res2.status).toBe(200); const updatedObservation = res2.body as Observation; expect(updatedObservation.meta?.accounts).toBeDefined(); expect(updatedObservation.meta?.accounts?.[0].reference).toBe(`Organization/${organization2.id}`); // Execute the operation adding the organization to the patient's compartment const res3 = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, { name: 'propagate', valueBoolean: true, }, ], }); expect(res3.status).toBe(200); const result2 = res3.body; expect(result2.parameter?.[0].name).toBe('resourcesUpdated'); expect(result2.parameter?.[0].valueInteger).toBe(3); // Observation and DiagnosticReport included //check if the accounts are updated on the patient const res4 = await request(app) .get(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res4.status).toBe(200); const updatedPatient = res4.body as Patient; expect(updatedPatient.meta?.accounts).toBeDefined(); expect(updatedPatient.meta?.accounts?.[0].reference).toBe(`Organization/${organization1.id}`); // Check if accounts are updated on the observation const res5 = await request(app) .get(`/fhir/R4/Observation/${observation.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res5.status).toBe(200); const finalObservation = res5.body as Observation; expect(finalObservation.meta?.accounts).toHaveLength(2); expect(finalObservation.meta?.accounts?.[0].reference).toBe(`Organization/${organization2.id}`); expect(finalObservation.meta?.accounts?.[1].reference).toBe(`Organization/${organization1.id}`); // Check if accounts are updated on the diagnostic report const res6 = await request(app) .get(`/fhir/R4/DiagnosticReport/${diagnosticReport.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(res6.status).toBe(200); const updatedDiagnosticReport = res6.body as DiagnosticReport; expect(updatedDiagnosticReport.meta?.accounts).toBeDefined(); expect(updatedDiagnosticReport.meta?.accounts?.[0].reference).toBe(`Organization/${organization1.id}`); }); test('Non-admin user cannot set accounts', async () => { accessToken = await initTestAuth(); const res = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, ], }); expect(res.status).toBe(403); }); test('Supports async response', async () => { const queue = getSetAccountsQueue() as any; queue.add.mockClear(); // Start the operation const initRes = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('Prefer', 'respond-async') .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1), }, { name: 'propagate', valueBoolean: true, }, ], }); expect(initRes.status).toBe(202); expect(initRes.headers['content-location']).toBeDefined(); // Manually push through BullMQ job expect(queue.add).toHaveBeenCalledWith( 'SetAccountsJobData', expect.objectContaining<Partial<SetAccountsJobData>>({ resourceType: 'Patient', id: patient.id }) ); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; queue.add.mockClear(); await runInAsyncContext( { login, membership, project, userConfig: {} as unknown as UserConfiguration }, undefined, undefined, () => execSetAccountsJob(job) ); // Check the export status const contentLocation = new URL(initRes.headers['content-location']); await waitForAsyncJob(initRes.headers['content-location'], app, accessToken); const statusRes = await request(app) .get(contentLocation.pathname) .set('Authorization', 'Bearer ' + accessToken); expect(statusRes.status).toBe(200); const resBody = statusRes.body as AsyncJob; expect(resBody.output?.parameter).toStrictEqual( expect.arrayContaining([{ name: 'resourcesUpdated', valueInteger: 3 }]) ); }); test('Enforces rate limits in async mode', async () => { const queue = getSetAccountsQueue() as any; queue.add.mockClear(); const { accessToken, repo } = await createTestProject({ withAccessToken: true, withRepo: true, membership: { admin: true }, project: { systemSetting: [{ name: 'userFhirQuota', valueInteger: 400 }] }, }); const patient = await repo.createResource({ resourceType: 'Patient' }); await repo.createResource({ resourceType: 'Observation', status: 'final', subject: createReference(patient), code: { text: 'Eye color' }, }); await repo.createResource({ resourceType: 'Observation', status: 'final', subject: createReference(patient), code: { text: 'Hair color' }, }); // Start the operation const initRes = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('Prefer', 'respond-async') .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1) }, { name: 'propagate', valueBoolean: true }, ], }); expect(initRes.status).toBe(202); expect(initRes.headers['content-location']).toBeDefined(); // Manually push through BullMQ job expect(queue.add).toHaveBeenCalledWith( 'SetAccountsJobData', expect.objectContaining<Partial<SetAccountsJobData>>({ resourceType: 'Patient', id: patient.id }) ); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; queue.add.mockClear(); // Mock out rate limiter to periodically block the user let count = 0; const consumeMock = jest.spyOn(RateLimiterRedis.prototype, 'consume').mockImplementation(async (key, _points) => { count = (count + 1) % 3; if (!key.toString().includes(membership.id)) { // allowed return { remainingPoints: 100, msBeforeNext: 100, consumedPoints: 100, isFirstInDuration: false, } as RateLimiterRes; } return { remainingPoints: 200 - count * 100, // Allow every third call msBeforeNext: 20, // Wait for one fake timers tick before next retry consumedPoints: 100, isFirstInDuration: false, } as RateLimiterRes; }); // Manually execute worker await runInAsyncContext( { login, membership, project, userConfig: {} as unknown as UserConfiguration }, undefined, undefined, () => execSetAccountsJob(job) ); expect(consumeMock).toHaveBeenCalledTimes(10); // Rate limits applied // Check the export status const contentLocation = new URL(initRes.headers['content-location']); await waitForAsyncJob(initRes.headers['content-location'], app, accessToken); const statusRes = await request(app) .get(contentLocation.pathname) .set('Authorization', 'Bearer ' + accessToken); expect(statusRes.status).toBe(200); // Job waits for rate limits and ultimately succeeds }); test('Removes account without extended header', async () => { const setTwo = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization1) }, { name: 'accounts', valueReference: createReference(organization2) }, { name: 'propagate', valueBoolean: false }, ], }); expect(setTwo.status).toBe(200); const get1 = await request(app) .get(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(get1.status).toBe(200); expect(get1.body.meta?.accounts?.map((r: any) => r.reference)).toEqual( expect.arrayContaining([`Organization/${organization1.id}`, `Organization/${organization2.id}`]) ); const setOne = await request(app) .post(`/fhir/R4/Patient/${patient.id}/$set-accounts`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Parameters', parameter: [ { name: 'accounts', valueReference: createReference(organization2) }, { name: 'propagate', valueBoolean: false }, ], }); expect(setOne.status).toBe(200); expect(setOne.body.parameter?.[0]).toMatchObject({ name: 'resourcesUpdated', valueInteger: 1 }); const get2 = await request(app) .get(`/fhir/R4/Patient/${patient.id}`) .set('Authorization', 'Bearer ' + accessToken); expect(get2.status).toBe(200); const acctRefs = (get2.body.meta?.accounts ?? []).map((r: any) => r.reference); expect(acctRefs).toEqual([`Organization/${organization2.id}`]); }); test('Accounts applied to resource with no default profile', async () => { const { accessToken, repo } = await createTestProject({ withAccessToken: true, withRepo: true, project: { defaultProfile: [ { resourceType: 'Patient', profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'] }, ], }, membership: { admin: true }, }); const organization = await repo.createResource<Organization>({ resourceType: 'Organization' }); const orgRef = createReference(organization); const patientRes = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('X-Medplum', 'extended') .send({ resourceType: 'Patient', meta: { accounts: [orgRef] }, } satisfies Patient); expect(patientRes.status).toBe(201); const patient = patientRes.body as Patient; const patientRef = createReference(patient); expect(patient.meta?.accounts).toStrictEqual([orgRef]); expect(patient.meta?.compartment).toContainEqual(orgRef); const reportRes = await request(app) .post(`/fhir/R4/DiagnosticReport`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('X-Medplum', 'extended') .send({ resourceType: 'DiagnosticReport', status: 'final', code: { text: 'Lab report' }, subject: patientRef, } satisfies DiagnosticReport); expect(reportRes.status).toBe(201); const diagnosticReport = reportRes.body as DiagnosticReport; expect(diagnosticReport.subject).toStrictEqual(patientRef); expect(diagnosticReport.meta?.compartment).toStrictEqual(expect.arrayContaining([orgRef, patientRef])); }); });

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