Skip to main content
Glama
index.test.ts25.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { WS } from 'jest-websocket-mock'; import type { FhircastConnectEvent, FhircastDiagnosticReportOpenContext, FhircastDiagnosticReportUpdateContext, FhircastDisconnectEvent, FhircastImagingStudyOpenContext, FhircastMessageEvent, FhircastMessagePayload, FhircastPatientOpenContext, SubscriptionRequest, } from '.'; import { assertContextVersionOptional, createFhircastMessagePayload, FHIRCAST_EVENT_VERSION_REQUIRED, FhircastConnection, isContextVersionRequired, serializeFhircastSubscriptionRequest, validateFhircastSubscriptionRequest, } from '.'; import { generateId } from '../crypto'; import { OperationOutcomeError } from '../outcomes'; describe('validateFhircastSubscriptionRequest', () => { test('Valid subscription requests', () => { expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'subscribe', channelType: 'websocket', events: ['Patient-open'], }) ).toBe(true); expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'unsubscribe', channelType: 'websocket', events: ['Patient-open'], }) ).toBe(true); expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'unsubscribe', channelType: 'websocket', events: ['Patient-open'], }) ).toBe(true); expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'unsubscribe', channelType: 'websocket', events: ['ImagingStudy-open', 'Patient-open'], }) ).toBe(true); expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'subscribe', channelType: 'websocket', events: ['Patient-open'], endpoint: 'wss://abc.com/hub', }) ).toBe(true); }); test('Invalid subscription requests', () => { // Must have at least one event expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'unsubscribe', channelType: 'websocket', events: [], }) ).toBe(false); expect( validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'unsubscribe', channelType: 'websocket', // @ts-expect-error events must be a EventName[] events: 'Patient-open', }) ).toBe(false); expect( // @ts-expect-error must include events prop validateFhircastSubscriptionRequest({ topic: 'abc123', mode: 'unsubscribe', channelType: 'websocket', }) ).toBe(false); expect( // @ts-expect-error must include topic prop validateFhircastSubscriptionRequest({ mode: 'unsubscribe', channelType: 'websocket', events: ['Patient-open'], }) ).toBe(false); expect( validateFhircastSubscriptionRequest({ // @ts-expect-error mode must be `subscribe` | `unsubscribe` mode: 'subscreebe', topic: 'abc123', channelType: 'websocket', events: ['Patient-open'], }) ).toBe(false); expect( validateFhircastSubscriptionRequest({ mode: 'subscribe', topic: 'abc123', // @ts-expect-error channelType must be `websocket` channelType: 'webhooks', events: ['Patient-open'], }) ).toBe(false); // Endpoint must be either ws:// or wss:// expect( validateFhircastSubscriptionRequest({ mode: 'subscribe', topic: 'abc123', channelType: 'websocket', events: ['Patient-open'], endpoint: 'http://abc.com/hub', }) ).toBe(false); expect( validateFhircastSubscriptionRequest({ mode: 'subscribe', // @ts-expect-error Topic needs to be a string topic: 12, channelType: 'websocket', events: ['Patient-open'], }) ).toBe(false); expect( // @ts-expect-error subscriptionRequest must be an object validateFhircastSubscriptionRequest(undefined) ).toBe(false); }); }); describe('serializeFhircastSubscriptionRequest', () => { test('Valid subscription request', () => { expect( serializeFhircastSubscriptionRequest({ mode: 'subscribe', channelType: 'websocket', topic: 'abc123', events: ['Patient-open'], }) ).toStrictEqual('hub.channel.type=websocket&hub.mode=subscribe&hub.topic=abc123&hub.events=Patient-open'); }); test('Valid subscription request with multiple events', () => { expect( serializeFhircastSubscriptionRequest({ mode: 'subscribe', channelType: 'websocket', topic: 'abc123', events: ['Patient-open', 'Patient-close'], }) ).toStrictEqual( 'hub.channel.type=websocket&hub.mode=subscribe&hub.topic=abc123&hub.events=Patient-open%2CPatient-close' ); }); test('Valid subscription request with endpoint', () => { expect( serializeFhircastSubscriptionRequest({ mode: 'subscribe', channelType: 'websocket', topic: 'abc123', events: ['Patient-open'], endpoint: 'wss://abc.com/hub', }) ).toStrictEqual( 'hub.channel.type=websocket&hub.mode=subscribe&hub.topic=abc123&hub.events=Patient-open&endpoint=wss%3A%2F%2Fabc.com%2Fhub' ); }); test('Invalid subscription request', () => { expect(() => serializeFhircastSubscriptionRequest({ mode: 'unsubscribe' } as unknown as SubscriptionRequest) ).toThrow(OperationOutcomeError); }); }); describe('createFhircastMessagePayload', () => { test('Valid message creation with single context', () => { const topic = 'abc123'; const event = 'Patient-open'; const resourceId = 'patient-123'; const context = { key: 'patient', resource: { resourceType: 'Patient', id: resourceId }, } satisfies FhircastPatientOpenContext; const messagePayload = createFhircastMessagePayload(topic, event, context); expect(messagePayload).toBeDefined(); expect(messagePayload).toEqual<FhircastMessagePayload>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': topic, 'hub.event': event, context: expect.any(Object) }, }); expect(new Date(messagePayload.timestamp).toISOString()).toStrictEqual(messagePayload.timestamp); expect(messagePayload.event.context[0]).toStrictEqual(context); }); test('Valid message with array of contexts', () => { const topic = 'abc123'; const event = 'ImagingStudy-open'; const resourceId1 = '123'; const context1 = { key: 'patient', resource: { resourceType: 'Patient', id: resourceId1 }, } satisfies FhircastImagingStudyOpenContext; const resourceId2 = '456'; const context2 = { key: 'study', resource: { resourceType: 'ImagingStudy', id: resourceId2, status: 'available', subject: { reference: 'Patient/123' }, }, } satisfies FhircastImagingStudyOpenContext; const messagePayload = createFhircastMessagePayload(topic, event, [context1, context2]); expect(messagePayload).toBeDefined(); expect(messagePayload).toEqual<FhircastMessagePayload>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': topic, 'hub.event': event, context: expect.any(Object) }, }); expect(new Date(messagePayload.timestamp).toISOString()).toStrictEqual(messagePayload.timestamp); expect(messagePayload.event.context[0]).toStrictEqual(context1); expect(messagePayload.event.context[1]).toStrictEqual(context2); }); test('Valid message with optional context included', () => { const topic = 'abc123'; const event = 'Patient-open'; const resourceId1 = '123'; const context1 = { key: 'patient', resource: { resourceType: 'Patient', id: resourceId1 }, } satisfies FhircastPatientOpenContext; const resourceId2 = '456'; const context2 = { key: 'encounter', resource: { resourceType: 'Encounter', id: resourceId2, status: 'in-progress', class: { code: 'Test Encounter' }, }, } satisfies FhircastPatientOpenContext; const messagePayload = createFhircastMessagePayload(topic, event, [context1, context2]); expect(messagePayload).toBeDefined(); expect(messagePayload).toEqual<FhircastMessagePayload>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': topic, 'hub.event': event, context: expect.any(Object) }, }); expect(new Date(messagePayload.timestamp).toISOString()).toStrictEqual(messagePayload.timestamp); expect(messagePayload.event.context[0]).toStrictEqual(context1); expect(messagePayload.event.context[1]).toStrictEqual(context2); }); test('Syncerror', () => { expect( createFhircastMessagePayload('abc-123', 'syncerror', { key: 'operationoutcome', resource: { resourceType: 'OperationOutcome', id: 'patient-123', issue: [{ severity: 'error', code: 'processing' }], }, }) ).toEqual<FhircastMessagePayload<'syncerror'>>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': 'abc-123', 'hub.event': 'syncerror', context: expect.any(Object) }, }); }); test('Invalid topic', () => { expect(() => createFhircastMessagePayload( // @ts-expect-error Invalid topic, must be a string 123, 'ImagingStudy-open', [ { key: 'patient', resource: { id: '123', resourceType: 'Patient' } }, { key: 'study', resource: { id: '123', resourceType: 'ImagingStudy', status: 'available', subject: { reference: 'Patient/123' }, }, }, ] ) ).toThrow(OperationOutcomeError); }); test('Invalid event name', () => { expect(() => createFhircastMessagePayload( 'abc-123', // @ts-expect-error Invalid event, must be one of the enumerated FHIRcast events 'imagingstudy-create', [ { key: 'patient', resource: { id: '123', resourceType: 'Patient' }, } satisfies FhircastDiagnosticReportOpenContext, { key: 'study', resource: { resourceType: 'ImagingStudy', id: '123', status: 'available', subject: { reference: 'Patient/123' }, }, } satisfies FhircastDiagnosticReportOpenContext, ] ) ).toThrow(OperationOutcomeError); }); test('Invalid context', () => { expect(() => createFhircastMessagePayload( // @ts-expect-error Topic must be a string 12, 'ImagingStudy-open', { key: 'study', resource: { id: 'imagingstudy-123', resourceType: 'ImagingStudy' } } ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'ImagingStudy-open', // @ts-expect-error Invalid context, must be an object 42 ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'ImagingStudy-open', // @ts-expect-error Invalid context, must be of type FhircastEventContext | FhircastEventContext[] { id: 'imagingstudy-123' } ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload('abc-123', 'ImagingStudy-open', { key: 'patient', resource: { resourceType: 'Patient' }, }) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'ImagingStudy-open', // @ts-expect-error Invalid resource, resourceType required { key: 'patient', resource: { id: 'patient-123' } } ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'ImagingStudy-open', // @ts-expect-error Invalid resourceType, must be a FHIRcast-related resource { key: 'patient', resource: { resourceType: 'Observation', id: 'observation-123' } } ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'ImagingStudy-open', // @ts-expect-error Invalid context, must have a valid resource AND a key { resource: { resourceType: 'Patient', id: 'patient-123' } } ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'Patient-open', // @ts-expect-error Invalid context, must have a valid resource AND a key { key: 'subject', resource: { resourceType: 'Patient', id: 'patient-123' } } ) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload( 'abc-123', 'Patient-open', // @ts-expect-error Invalid context, must have a valid resource AND a key { key: 'imagingstudy', resource: { resourceType: 'ImagingStudy', id: 'patient-123' } } ) ).toThrow(OperationOutcomeError); expect(() => // Should throw because keys must be unique createFhircastMessagePayload('abc-123', 'ImagingStudy-open', [ { key: 'patient', resource: { resourceType: 'Patient', id: 'patient-123' } }, { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-456', status: 'available', subject: { reference: 'Patient/patient-123' }, }, }, { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-789', status: 'available', subject: { reference: 'Patient/patient-123' }, }, }, ]) ).toThrow(OperationOutcomeError); expect(() => // Should throw because Patient-open has an optional 2nd context of `Encounter` createFhircastMessagePayload('abc-123', 'Patient-open', [ { key: 'patient', resource: { resourceType: 'Patient', id: 'patient-123' } }, // @ts-expect-error 'study' is not a valid key on 'Patient-open' event { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-456' } }, ]) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload('abc-123', 'Patient-open', [ // @ts-expect-error Key 'patient' expects a 'Patient' resource { key: 'patient', resource: { resourceType: 'Bundle', id: 'patient-123' } }, ]) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload('abc-123', 'Patient-open', [ { key: 'patient', resource: { resourceType: 'Patient', id: 'patient-123' } }, // @ts-expect-error Need a key { resource: { resourceType: 'Encounter', id: 'encounter-456' } }, ]) ).toThrow(OperationOutcomeError); expect(() => createFhircastMessagePayload('abc-123', 'Patient-open', [ { key: 'patient', resource: { resourceType: 'Patient', id: 'patient-123' } }, // @ts-expect-error Resource should be an object { key: 'encounter', resource: 42 }, ]) ).toThrow(OperationOutcomeError); }); test('Valid `DiagnosticReport-open` event w/ multiple studies', () => { const payload = createFhircastMessagePayload('abc-123', 'DiagnosticReport-open', [ { key: 'report', resource: { resourceType: 'DiagnosticReport', id: 'report-789', status: 'final', code: { text: 'test' } }, }, { key: 'patient', resource: { resourceType: 'Patient', id: 'patient-123' } }, { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-123', status: 'available', subject: {} }, }, { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-456', status: 'available', subject: {} }, }, { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-789', status: 'available', subject: {} }, }, ]); expect(payload).toEqual<FhircastMessagePayload<'DiagnosticReport-open'>>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': 'abc-123', 'hub.event': 'DiagnosticReport-open', context: expect.any(Object) }, }); expect(payload.event.context.length).toStrictEqual(5); }); test('Invalid `DiagnosticReport-open` event w/ multiple reports', () => { expect(() => createFhircastMessagePayload('abc-123', 'DiagnosticReport-open', [ { key: 'report', resource: { resourceType: 'DiagnosticReport', id: 'report-789', status: 'final', code: { text: 'test' } }, }, { key: 'report', resource: { resourceType: 'DiagnosticReport', id: 'report-789', status: 'final', code: { text: 'test' } }, }, { key: 'patient', resource: { resourceType: 'Patient', id: 'patient-123' } }, { key: 'study', resource: { resourceType: 'ImagingStudy', id: 'imagingstudy-123', status: 'available', subject: {} }, }, ]) ).toThrow(OperationOutcomeError); }); test('Valid `DiagnosticReport-select` event', () => { const messagePayload = createFhircastMessagePayload('abc-123', 'DiagnosticReport-select', [ { key: 'report', reference: { reference: 'DiagnosticReport/123' }, }, { key: 'select', reference: { reference: 'Observation/123' }, }, { key: 'select', reference: { reference: 'Observation/456' }, }, ]); expect(messagePayload).toBeDefined(); expect(messagePayload).toEqual<FhircastMessagePayload>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': 'abc-123', 'hub.event': 'DiagnosticReport-select', context: expect.any(Object) }, }); expect(new Date(messagePayload.timestamp).toISOString()).toStrictEqual(messagePayload.timestamp); expect(messagePayload.event.context[0]).toBeDefined(); }); test('Valid `DiagnosticReport-update` event', () => { const messagePayload = createFhircastMessagePayload( 'abc-123', 'DiagnosticReport-update', [ { key: 'report', reference: { reference: 'DiagnosticReport/123' }, }, { key: 'updates', resource: { resourceType: 'Bundle', id: 'bundle-123', type: 'transaction' } }, ], generateId() ); expect(messagePayload).toBeDefined(); expect(messagePayload).toEqual<FhircastMessagePayload>({ id: expect.any(String), timestamp: expect.any(String), event: { 'hub.topic': 'abc-123', 'hub.event': 'DiagnosticReport-update', context: expect.any(Object), 'context.versionId': expect.any(String), }, }); expect(new Date(messagePayload.timestamp).toISOString()).toStrictEqual(messagePayload.timestamp); expect(messagePayload.event.context[0]).toBeDefined(); }); test('Resource context instead of reference for report in `*-update` event', () => { expect(() => createFhircastMessagePayload( 'abc-123', 'DiagnosticReport-update', [ // This report should be a reference { key: 'report', resource: { resourceType: 'DiagnosticReport', id: 'report-123' } }, { key: 'updates', resource: { resourceType: 'Bundle', id: 'bundle-123' } }, ] as FhircastDiagnosticReportUpdateContext[], generateId() ) ).toThrow(OperationOutcomeError); }); test('Missing `context.versionId` in `*-update` event', () => { expect(() => createFhircastMessagePayload( 'abc-123', // @ts-expect-error Missing `context.versionId` for test 'DiagnosticReport-update', [ { key: 'report', reference: { reference: 'DiagnosticReport/123' } }, { key: 'updates', resource: { resourceType: 'Bundle', id: 'bundle-123' } }, ] ) ).toThrow(OperationOutcomeError); }); }); describe('FhircastConnection', () => { let wsServer: WS; let connection: FhircastConnection; beforeAll(() => { wsServer = new WS('ws://localhost:1234', { jsonProtocol: true }); }); afterAll(() => { WS.clean(); }); test('Constructor / .addEventListener("connect")', (done) => { const subRequest = { topic: 'abc123', mode: 'subscribe', channelType: 'websocket', events: ['Patient-open'], endpoint: 'ws://localhost:1234', } satisfies SubscriptionRequest; connection = new FhircastConnection(subRequest); expect(connection).toBeDefined(); const handler = (event: FhircastConnectEvent): void => { expect(event).toBeDefined(); expect(event.type).toBe('connect'); connection.removeEventListener('connect', handler); done(); }; connection.addEventListener('connect', handler); }); test('.addEventListener("message") - FhircastMessage', (done) => { const message = createFhircastMessagePayload('abc123', 'Patient-open', { key: 'patient', resource: { id: '123', resourceType: 'Patient' }, }); const handler = (event: FhircastMessageEvent): void => { expect(event).toBeDefined(); expect(event.type).toBe('message'); expect(event.payload).toStrictEqual(message); connection.removeEventListener('message', handler); done(); }; connection.addEventListener('message', handler); wsServer.send(message); }); test('.addEventListener("message") - Subscription Confirmation', (done) => { const message = createFhircastMessagePayload('abc123', 'Patient-open', { key: 'patient', resource: { id: '123', resourceType: 'Patient' }, }); const handler = (event: FhircastMessageEvent): void => { expect(event).toBeDefined(); expect(event.type).toBe('message'); expect(event.payload).toStrictEqual(message); connection.removeEventListener('message', handler); done(); }; connection.addEventListener('message', handler); wsServer.send({ 'hub.topic': generateId() }); wsServer.send(message); }); test('.addEventListener("message") - Heartbeat message', (done) => { const heartbeatMessage = { id: generateId(), timestamp: new Date().toISOString(), event: { 'hub.topic': 'abc123', 'hub.event': 'heartbeat', context: [{ key: 'period', decimal: '10' }], }, }; const message = createFhircastMessagePayload('abc123', 'Patient-open', { key: 'patient', resource: { id: '123', resourceType: 'Patient' }, }); const handler = (event: FhircastMessageEvent): void => { expect(event).toBeDefined(); expect(event.type).toBe('message'); expect(event.payload).toStrictEqual(message); connection.removeEventListener('message', handler); done(); }; connection.addEventListener('message', handler); wsServer.send(heartbeatMessage); wsServer.send(message); }); test('.disconnect() / .addEventListener("disconnect")', (done) => { const handler = (event: FhircastDisconnectEvent): void => { expect(event).toBeDefined(); expect(event.type).toBe('disconnect'); connection.removeEventListener('disconnect', handler); done(); }; connection.addEventListener('disconnect', handler); connection.disconnect(); }); test('Invalid SubscriptionRequest in constructor', () => { expect( () => new FhircastConnection({ topic: 'abc123', mode: 'subscribe', // @ts-expect-error Invalid channelType channelType: 'webhooks', events: ['Patient-open'], endpoint: 'ws://localhost:1234', }) ).toThrow(OperationOutcomeError); }); }); describe('isContextVersionRequired', () => { test('Version required: true', () => { expect(FHIRCAST_EVENT_VERSION_REQUIRED.includes('DiagnosticReport-update')).toStrictEqual(true); expect(isContextVersionRequired('DiagnosticReport-update')).toStrictEqual(true); }); test('Version required: false', () => { expect((FHIRCAST_EVENT_VERSION_REQUIRED as readonly string[]).includes('Patient-open')).toStrictEqual(false); expect(isContextVersionRequired('Patient-open')).toStrictEqual(false); }); }); describe('assertContextVersionOptional', () => { test('Version optional: true', () => { expect((FHIRCAST_EVENT_VERSION_REQUIRED as readonly string[]).includes('Patient-open')).toStrictEqual(false); expect(() => assertContextVersionOptional('Patient-open')).not.toThrow(); }); test('Version optional: false', () => { expect(FHIRCAST_EVENT_VERSION_REQUIRED.includes('DiagnosticReport-update')).toStrictEqual(true); expect(() => assertContextVersionOptional('DiagnosticReport-update')).toThrow(OperationOutcomeError); }); });

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