Skip to main content
Glama
custom.test.ts10.8 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { badRequest, ContentType, createReference } from '@medplum/core'; import type { Bot } from '@medplum/fhirtypes'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { loadTestConfig } from '../../config/loader'; import { initTestAuth } from '../../test.setup'; describe('Custom operation', () => { const app = express(); let accessToken: string; beforeAll(async () => { const config = await loadTestConfig(); config.vmContextBotsEnabled = true; await initApp(app, config); accessToken = await initTestAuth(); }); afterAll(async () => { await shutdownApp(); }); test('Missing extension', async () => { // Test the case where there is an OperationDefinition, // but it does not have the required extension for a custom operation const res1 = await request(app) .post('/fhir/R4/OperationDefinition') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'OperationDefinition', name: 'missing-extension', status: 'active', kind: 'operation', code: 'missing-extension', system: true, type: false, instance: false, parameter: [ { use: 'in', name: 'foo', type: 'string', min: 1, max: '1', }, { use: 'out', name: 'bar', type: 'string', min: 1, max: '1', }, ], }); expect(res1.status).toBe(201); const res2 = await request(app) .post('/fhir/R4/Patient/$missing-extension') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res2.status).toBe(404); }); test('Extension is not a bot', async () => { // Test the case where there is an OperationDefinition, // and the OperationDefinition does have the required extension, // but it does not reference a Bot resource const res3 = await request(app) .post('/fhir/R4/OperationDefinition') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'OperationDefinition', extension: [ { url: 'https://medplum.com/fhir/StructureDefinition/operationDefinition-implementation', valueReference: { reference: 'foo' }, }, ], name: 'not-a-bot', status: 'active', kind: 'operation', code: 'not-a-bot', system: true, type: false, instance: false, parameter: [ { use: 'in', name: 'foo', type: 'string', min: 1, max: '1', }, { use: 'out', name: 'bar', type: 'string', min: 1, max: '1', }, ], }); expect(res3.status).toBe(201); const res4 = await request(app) .post('/fhir/R4/Patient/$not-a-bot') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res4.status).toBe(404); }); test('Success', async () => { const res1 = await request(app) .post('/fhir/R4/Bot') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Bot', name: 'Custom Operation Bot', runtimeVersion: 'vmcontext', }); expect(res1.status).toBe(201); const bot = res1.body as WithId<Bot>; const res2 = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$deploy`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ code: ` exports.handler = async function (medplum, event) { const patient = event.input; patient.identifier = patient.identifier || []; patient.identifier.push({ system: 'https://example.com/patient-id', value: '12345', }); return patient; }; `, }); expect(res2.status).toBe(200); const res3 = await request(app) .post('/fhir/R4/OperationDefinition') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'OperationDefinition', extension: [ { url: 'https://medplum.com/fhir/StructureDefinition/operationDefinition-implementation', valueReference: createReference(bot), }, ], name: 'my-custom-operation', status: 'active', kind: 'operation', code: 'my-custom-operation', system: true, type: false, instance: false, parameter: [ { use: 'in', name: 'foo', type: 'string', min: 1, max: '1', }, { use: 'out', name: 'return', type: 'Patient', min: 1, max: '1', }, ], }); expect(res3.status).toBe(201); const res4 = await request(app) .post('/fhir/R4/Patient/$my-custom-operation') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res4.status).toBe(200); expect(res4.body.resourceType).toBe('Patient'); expect(res4.body.identifier).toBeDefined(); expect(res4.body.identifier.length).toBe(1); expect(res4.body.identifier[0]).toMatchObject({ system: 'https://example.com/patient-id', value: '12345', }); }); test('Error value returned all the way to the client', async () => { const res1 = await request(app) .post('/fhir/R4/Bot') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Bot', name: 'Custom Operation Bot', runtimeVersion: 'vmcontext', }); expect(res1.status).toBe(201); const bot = res1.body as WithId<Bot>; const res2 = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$deploy`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ code: ` const { badRequest, OperationOutcomeError } = require('@medplum/core'); exports.handler = async function (medplum, event) { throw new OperationOutcomeError(badRequest('test')); }; `, }); expect(res2.status).toBe(200); const res3 = await request(app) .post('/fhir/R4/OperationDefinition') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'OperationDefinition', extension: [ { url: 'https://medplum.com/fhir/StructureDefinition/operationDefinition-implementation', valueReference: createReference(bot), }, ], name: 'my-error-operation', status: 'active', kind: 'operation', code: 'my-error-operation', system: true, type: false, instance: false, parameter: [ { use: 'in', name: 'foo', type: 'string', min: 1, max: '1', }, { use: 'out', name: 'return', type: 'Patient', min: 1, max: '1', }, ], }); expect(res3.status).toBe(201); const res4 = await request(app) .post('/fhir/R4/Patient/$my-error-operation') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res4.status).toBe(400); expect(res4.body).toMatchObject(badRequest('test')); }); test('Helpful error if return type does not match', async () => { const res1 = await request(app) .post('/fhir/R4/Bot') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Bot', name: 'Custom Operation Bot', runtimeVersion: 'vmcontext', }); expect(res1.status).toBe(201); const bot = res1.body as WithId<Bot>; const res2 = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$deploy`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ code: ` const { badRequest, OperationOutcomeError } = require('@medplum/core'); exports.handler = async function (medplum, event) { return 'test string'; }; `, }); expect(res2.status).toBe(200); const res3 = await request(app) .post('/fhir/R4/OperationDefinition') .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'OperationDefinition', extension: [ { url: 'https://medplum.com/fhir/StructureDefinition/operationDefinition-implementation', valueReference: createReference(bot), }, ], name: 'my-return-type-operation', status: 'active', kind: 'operation', code: 'my-return-type-operation', system: true, type: false, instance: false, parameter: [ { use: 'in', name: 'foo', type: 'string', min: 1, max: '1', }, { use: 'out', name: 'return', type: 'Patient', min: 1, max: '1', }, ], }); expect(res3.status).toBe(201); const res4 = await request(app) .post('/fhir/R4/Patient/$my-return-type-operation') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient' }); expect(res4.status).toBe(400); expect(res4.body).toMatchObject(badRequest('Expected Patient output, but got unexpected string')); }); });

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