Skip to main content
Glama
routes.test.ts30.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { CurrentContext, FhircastEventContext, FhircastEventPayload, WithId } from '@medplum/core'; import { ContentType, createFhircastMessagePayload, generateId, isOperationOutcome } from '@medplum/core'; import type { DiagnosticReport, Project } from '@medplum/fhirtypes'; import express from 'express'; import type { ChainableCommander } from 'ioredis'; import { randomUUID } from 'node:crypto'; import type { Server } from 'node:http'; import request from 'superwstest'; import { initApp, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import type { MedplumServerConfig } from '../config/types'; import { getRedis } from '../redis'; import { createTestProject, withTestContext } from '../test.setup'; import { setTopicCurrentContext } from './utils'; const STU2_BASE_ROUTE = '/fhircast/STU2'; const STU3_BASE_ROUTE = '/fhircast/STU3'; type ExecResult = Awaited<ReturnType<ChainableCommander['exec']>>; class MockChainableCommander { result: ExecResult = null; setnx(): this { return this; } get(): this { return this; } async exec(): Promise<[Error | null, unknown][] | null> { return this.result; } setNextExecResult(result: ExecResult): void { this.result = result; } } describe('FHIRcast routes', () => { let app: express.Express; let config: MedplumServerConfig; let server: Server; let project: WithId<Project>; let accessToken: string; let tokenForAnotherProject: string; beforeAll(async () => { app = express(); config = await loadTestConfig(); config.heartbeatEnabled = false; server = await initApp(app, config); const { accessToken: _accessToken1, project: _project1 } = await withTestContext(() => createTestProject({ membership: { admin: true }, withAccessToken: true }) ); const { accessToken: _accessToken2 } = await withTestContext(() => createTestProject({ membership: { admin: true }, withAccessToken: true }) ); accessToken = _accessToken1; project = _project1; tokenForAnotherProject = _accessToken2; await new Promise<void>((resolve) => { server.listen(0, 'localhost', 8517, resolve); }); }); afterAll(async () => { await shutdownApp(); }); test('Get well known', async () => { let res: any; res = await request(server).get(`${STU2_BASE_ROUTE}/.well-known/fhircast-configuration`); expect(res.status).toBe(200); expect(res.body.eventsSupported).toBeDefined(); expect(res.body.getCurrentSupport).toBeUndefined(); expect(res.body.websocketSupport).toBe(true); expect(res.body.webhookSupport).toBe(false); expect(res.body.fhircastVersion).toBe('STU2'); res = await request(server).get(`${STU3_BASE_ROUTE}/.well-known/fhircast-configuration`); expect(res.status).toBe(200); expect(res.body.eventsSupported).toBeDefined(); expect(res.body.getCurrentSupport).toBe(true); expect(res.body.websocketSupport).toBe(true); expect(res.body.webhookSupport).toBe(false); expect(res.body.fhircastVersion).toBe('STU3'); }); test('New subscription success', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(202); expect(res.body['hub.channel.endpoint']).toBeDefined(); } }); test('New subscription no auth', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server).post(route).set('Content-Type', ContentType.JSON).send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(401); expect(res.body.issue[0].details.text).toStrictEqual('Unauthorized'); } }); test('New subscription missing channel type', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Missing hub.channel.type'); } }); test('New subscription invalid channel type', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'xyz', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Invalid hub.channel.type'); } }); test('New subscription invalid mode', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'xyz', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Invalid hub.mode'); } }); test('Subscribing twice to the same topic yields the same url', async () => { const res1 = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res1.status).toBe(202); expect(res1.body['hub.channel.endpoint']).toMatch(/ws:\/\/localhost:8103\/ws\/fhircast\/*/); expect(res1.body['hub.channel.endpoint']).not.toContain('topic'); const res2 = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res2.status).toBe(202); expect(res2.body['hub.channel.endpoint']).toStrictEqual(res1.body['hub.channel.endpoint']); }); test('Subscribing to the same topic from a different project yields a different endpoint', async () => { const res1 = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res1.status).toBe(202); expect(res1.body['hub.channel.endpoint']).toMatch(/ws:\/\/localhost:8103\/ws\/fhircast\/*/); expect(res1.body['hub.channel.endpoint']).not.toContain('topic'); const res2 = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + tokenForAnotherProject) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res2.status).toBe(202); expect(res2.body['hub.channel.endpoint']).not.toStrictEqual(res1.body['hub.channel.endpoint']); }); test('Redis returns `null`', async () => { const redis = getRedis(); const mockCommander = new MockChainableCommander(); const mockFn = (() => { return mockCommander; }) as unknown as (commands?: unknown[][]) => ChainableCommander; const redisMulti = jest.spyOn(redis, 'multi').mockImplementation(mockFn); mockCommander.setNextExecResult(null); const res = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(500); expect(isOperationOutcome(res.body)).toStrictEqual(true); expect(res.body).toMatchObject({ resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'exception', details: { text: 'Internal server error' }, diagnostics: 'Error: Failed to get endpoint for topic', }, ], }); redisMulti.mockRestore(); }); test('Redis result contains error', async () => { const redis = getRedis(); const mockCommander = new MockChainableCommander(); const mockFn = (() => { return mockCommander; }) as unknown as (commands?: unknown[][]) => ChainableCommander; const redisMulti = jest.spyOn(redis, 'multi').mockImplementation(mockFn); mockCommander.setNextExecResult([ [null, 'OK'], [new Error('Something happened when querying Redis'), null], ]); const res = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(res.status).toBe(500); expect(isOperationOutcome(res.body)).toStrictEqual(true); expect(res.body).toMatchObject({ resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'exception', details: { text: 'Internal server error' }, diagnostics: 'Error: Failed to get endpoint for topic', }, ], }); redisMulti.mockRestore(); }); test('Unsubscribe', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const subRes = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'subscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(subRes.status).toBe(202); expect(subRes.body['hub.channel.endpoint']).toBeDefined(); const pathname = new URL(subRes.body['hub.channel.endpoint']).pathname; await request(server) .ws(pathname) .expectJson((obj) => { // Connection verification message expect(obj['hub.topic']).toBe('topic'); }) .exec(async () => { const unsubRes = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ 'hub.channel.type': 'websocket', 'hub.mode': 'unsubscribe', 'hub.topic': 'topic', 'hub.events': 'Patient-open', }); expect(unsubRes.status).toBe(202); expect(unsubRes.body['hub.channel.endpoint']).toBeDefined(); }) .expectJson({ 'hub.topic': 'topic', 'hub.mode': 'denied', 'hub.reason': 'Subscriber unsubscribed from topic', 'hub.events': 'Patient-open', }) .close() .expectClosed(); } }); test('Publish event missing timestamp', async () => { const topic = randomUUID(); for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(`${route}/${topic}`) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ id: randomUUID(), event: {}, }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Missing event timestamp'); } }); test('Context change request on hub.url', async () => { const topic = randomUUID(); for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ id: randomUUID(), timestamp: new Date().toISOString(), event: { 'hub.topic': topic, 'hub.event': 'Patient-close', context: [ { key: 'patient', resource: { resourceType: 'Patient', id: '798E4MyMcpCWHab9', identifier: [ { type: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0203', value: 'MR', display: 'Medical Record Number', }, ], text: 'MRN', }, }, ], }, }, ], }, }); expect(res.status).toBe(202); } }); test('Context change request on /:topic', async () => { const topic = randomUUID(); for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(`${route}/${topic}`) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ id: randomUUID(), timestamp: new Date().toISOString(), event: { 'hub.topic': topic, 'hub.event': 'Patient-close', context: [ { key: 'patient', resource: { resourceType: 'Patient', id: '798E4MyMcpCWHab9', identifier: [ { type: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0203', value: 'MR', display: 'Medical Record Number', }, ], text: 'MRN', }, }, ], }, }, ], }, }); expect(res.status).toBe(202); } }); test('Context change -- missing "hub.topic"', async () => { for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ id: randomUUID(), timestamp: new Date().toISOString(), event: { 'hub.event': 'Patient-close', context: [ { key: 'patient', resource: { resourceType: 'Patient', id: '798E4MyMcpCWHab9', identifier: [ { type: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0203', value: 'MR', display: 'Medical Record Number', }, ], text: 'MRN', }, }, ], }, }, ], }, }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Missing event["hub.topic"]'); } }); test('Context change -- missing "hub.event"', async () => { const topic = randomUUID(); for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ id: randomUUID(), timestamp: new Date().toISOString(), event: { 'hub.topic': topic, context: [ { key: 'patient', resource: { resourceType: 'Patient', id: '798E4MyMcpCWHab9', identifier: [ { type: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0203', value: 'MR', display: 'Medical Record Number', }, ], text: 'MRN', }, }, ], }, }, ], }, }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Missing event["hub.event"]'); } }); test('Context change -- missing context', async () => { const topic = randomUUID(); for (const route of [STU2_BASE_ROUTE, STU3_BASE_ROUTE]) { const res = await request(server) .post(route) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ id: randomUUID(), timestamp: new Date().toISOString(), event: { 'hub.topic': topic, 'hub.event': 'Patient-close', }, }); expect(res.status).toBe(400); expect(res.body.issue[0].details.text).toStrictEqual('Missing event.context'); } }); test('Get context', async () => { const topic = randomUUID(); let res: any; // Non-standard FHIRcast extension to support Nuance PowerCast Hub res = await request(server) .get(`${STU2_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); expect(res.body).toStrictEqual([]); res = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(res.status).toBe(200); expect(res.body).toStrictEqual({ 'context.type': '', context: [] }); }); test('Get context after *-open event', async () => { let contextRes: any; const topic = randomUUID(); const payload = createFhircastMessagePayload(topic, 'DiagnosticReport-open', [ { key: 'report', resource: { id: 'def-456', resourceType: 'DiagnosticReport', status: 'final', code: { text: 'test' } }, }, { key: 'patient', resource: { id: 'xyz-789', resourceType: 'Patient' } }, ]); const publishRes = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send(payload); expect(publishRes.status).toBe(202); contextRes = await request(server) .get(`${STU2_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(contextRes.status).toBe(200); expect(contextRes.body).toStrictEqual([ ...payload.event.context, { key: 'content', resource: { id: expect.any(String), resourceType: 'Bundle', type: 'collection' } }, ]); contextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(contextRes.status).toBe(200); expect(contextRes.body).toMatchObject({ 'context.type': 'DiagnosticReport', 'context.versionId': expect.any(String), context: [ ...payload.event.context, { key: 'content', resource: { id: expect.any(String), resourceType: 'Bundle', type: 'collection' } }, ], }); }); test('Get context cannot read from cross-project topic', async () => { const topic = randomUUID(); const payload1 = createFhircastMessagePayload(topic, 'DiagnosticReport-open', [ { key: 'report', resource: { id: 'def-456', resourceType: 'DiagnosticReport', status: 'final', code: { text: 'test' } }, }, { key: 'patient', resource: { id: 'xyz-789', resourceType: 'Patient' } }, ]); const payload2 = createFhircastMessagePayload(topic, 'DiagnosticReport-open', [ { key: 'report', resource: { id: 'abc-123', resourceType: 'DiagnosticReport', status: 'final', code: { text: 'test' } }, }, { key: 'patient', resource: { id: 'def-456', resourceType: 'Patient' } }, ]); let publishRes = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send(payload1); expect(publishRes.status).toBe(202); let contextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(contextRes.status).toBe(200); expect(contextRes.body).toMatchObject({ 'context.type': 'DiagnosticReport', 'context.versionId': expect.any(String), context: [ ...payload1.event.context, { key: 'content', resource: { id: expect.any(String), resourceType: 'Bundle', type: 'collection' } }, ], }); // Users from other projects should not be able to see the context from the original project contextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + tokenForAnotherProject); expect(contextRes.status).toBe(200); expect(contextRes.body).toMatchObject({ 'context.type': '', context: [] }); // Now set publish another event for the same topic in another project publishRes = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + tokenForAnotherProject) .send(payload2); expect(publishRes.status).toBe(202); // Context for project 1 should still be the same as before contextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(contextRes.status).toBe(200); expect(contextRes.body).toMatchObject({ 'context.type': 'DiagnosticReport', 'context.versionId': expect.any(String), context: [ ...payload1.event.context, { key: 'content', resource: { id: expect.any(String), resourceType: 'Bundle', type: 'collection' } }, ], }); // Context for project 2 should not be the same as the last published event contextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + tokenForAnotherProject); expect(contextRes.status).toBe(200); expect(contextRes.body).toMatchObject({ 'context.type': 'DiagnosticReport', 'context.versionId': expect.any(String), context: [ ...payload2.event.context, { key: 'content', resource: { id: expect.any(String), resourceType: 'Bundle', type: 'collection' } }, ], }); }); test('Get context after *-close event', async () => { let beforeContextRes: any; let afterContextRes: any; const topic = randomUUID(); const context = [ { key: 'report', resource: { id: 'def-456', resourceType: 'DiagnosticReport', status: 'final', code: { text: 'test' } }, }, { key: 'patient', resource: { id: 'xyz-789', resourceType: 'Patient' } }, ] satisfies FhircastEventContext<'DiagnosticReport-open'>[]; const payload = createFhircastMessagePayload(topic, 'DiagnosticReport-open', context); payload.event['context.versionId'] = generateId(); const contentBundleId = generateId(); // Setup the key as if we have already opened this resource await setTopicCurrentContext(project.id, topic, { 'context.type': 'DiagnosticReport', 'context.versionId': generateId(), context: [ ...context, { key: 'content', resource: { id: contentBundleId, resourceType: 'Bundle', type: 'collection' } }, ], }); beforeContextRes = await request(server) .get(`${STU2_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(beforeContextRes.status).toBe(200); expect(beforeContextRes.body).toStrictEqual([ ...context, { key: 'content', resource: { id: contentBundleId, resourceType: 'Bundle', type: 'collection' } }, ]); beforeContextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(beforeContextRes.status).toBe(200); expect(beforeContextRes.body).toStrictEqual({ 'context.type': 'DiagnosticReport', 'context.versionId': expect.any(String), context: [ ...context, { key: 'content', resource: { id: contentBundleId, resourceType: 'Bundle', type: 'collection' } }, ], }); const publishRes = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send(createFhircastMessagePayload(topic, 'DiagnosticReport-close', context)); expect(publishRes.status).toBe(202); afterContextRes = await request(server) .get(`${STU2_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(afterContextRes.status).toBe(200); expect(afterContextRes.body).toStrictEqual([]); afterContextRes = await request(server) .get(`${STU3_BASE_ROUTE}/${topic}`) .set('Authorization', 'Bearer ' + accessToken); expect(afterContextRes.status).toBe(200); expect(afterContextRes.body).toStrictEqual({ 'context.type': '', context: [] }); }); test('Check for `context.versionId` on `DiagnosticReport-open`', async () => { const topic = randomUUID(); const context = [ { key: 'report', resource: { id: 'abc-123', resourceType: 'DiagnosticReport', status: 'final', code: { text: 'test' } }, }, { key: 'study', resource: { id: 'def-456', resourceType: 'ImagingStudy', status: 'available', subject: {} } }, { key: 'patient', resource: { id: 'xyz-789', resourceType: 'Patient' } }, ] satisfies FhircastEventContext<'DiagnosticReport-open'>[]; const payload = createFhircastMessagePayload(topic, 'DiagnosticReport-open', context); const publishRes = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send(payload); expect(publishRes.status).toBe(202); expect(publishRes.body.event?.event?.['context.versionId']).toBeDefined(); const latestContextStr = (await getRedis().get( `medplum:fhircast:project:${project.id}:topic:${topic}:latest` )) as string; expect(latestContextStr).toBeTruthy(); const latestContext = JSON.parse(latestContextStr) as CurrentContext<'DiagnosticReport'>; expect(publishRes.body.event?.event?.['context.versionId']).toStrictEqual(latestContext['context.versionId']); }); test('`DiagnosticReport-update`: `context.priorVersionId` matches prior `context.versionId`', async () => { const topic = randomUUID(); const contentBundleId = generateId(); const versionId = generateId(); // Setup the key as if we have already opened this resource await setTopicCurrentContext(project.id as string, topic, { 'context.type': 'DiagnosticReport', 'context.versionId': versionId, context: [ { key: 'report', resource: { id: '123', resourceType: 'DiagnosticReport', status: 'preliminary', code: { coding: [ { system: 'http://loinc.org', code: '19005-8', display: 'Radiology Imaging study [Impression] (narrative)', }, ], }, } satisfies DiagnosticReport, }, { key: 'content', resource: { id: contentBundleId, resourceType: 'Bundle', type: 'collection' } }, ], }); const context = [ { key: 'report', reference: { reference: 'DiagnosticReport/123' }, }, { key: 'patient', reference: { reference: 'Patient/123' }, }, { key: 'updates', resource: { id: 'bundle-123', resourceType: 'Bundle', type: 'transaction' } }, ] satisfies FhircastEventContext<'DiagnosticReport-update'>[]; const payload = createFhircastMessagePayload(topic, 'DiagnosticReport-update', context, versionId); const publishRes = await request(server) .post(STU3_BASE_ROUTE) .set('Content-Type', ContentType.JSON) .set('Authorization', 'Bearer ' + accessToken) .send(payload); expect(publishRes.status).toBe(202); expect(publishRes.body.event).toMatchObject({ ...payload, event: { ...payload.event, 'context.priorVersionId': payload.event['context.versionId'], 'context.versionId': expect.any(String), }, }); expect( (publishRes.body.event.event as FhircastEventPayload<'DiagnosticReport-update'>)['context.priorVersionId'] ).toStrictEqual(versionId); }); });

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