Skip to main content
Glama
execute.test.ts8.12 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { InvokeCommand, LambdaClient, ListLayerVersionsCommand } from '@aws-sdk/client-lambda'; import { ContentType } from '@medplum/core'; import type { Bot } from '@medplum/fhirtypes'; import type { AwsClientStub } from 'aws-sdk-client-mock'; import { mockClient } from 'aws-sdk-client-mock'; import { randomUUID } from 'crypto'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { getConfig, loadTestConfig } from '../../config/loader'; import { getBinaryStorage } from '../../storage/loader'; import { initTestAuth } from '../../test.setup'; import { getLambdaFunctionName } from './execute'; const app = express(); let accessToken: string; let bot: Bot; describe('Execute', () => { let mockLambdaClient: AwsClientStub<LambdaClient>; beforeEach(() => { mockLambdaClient = mockClient(LambdaClient); mockLambdaClient.on(ListLayerVersionsCommand).resolves({ LayerVersions: [ { LayerVersionArn: 'xyz', }, ], }); mockLambdaClient.on(InvokeCommand).callsFake(({ Payload }) => { const decoder = new TextDecoder(); const event = JSON.parse(decoder.decode(Payload)); const output = JSON.stringify(event.input); const encoder = new TextEncoder(); return { LogResult: `U1RBUlQgUmVxdWVzdElkOiAxNDZmY2ZjZi1jMzJiLTQzZjUtODJhNi1lZTBmMzEzMmQ4NzMgVmVyc2lvbjogJExBVEVTVAoyMDIyLTA1LTMwVDE2OjEyOjIyLjY4NVoJMTQ2ZmNmY2YtYzMyYi00M2Y1LTgyYTYtZWUwZjMxMzJkODczCUlORk8gdGVzdApFTkQgUmVxdWVzdElkOiAxNDZmY2ZjZi1jMzJiLTQzZjUtODJhNi1lZTBmMzEzMmQ4NzMKUkVQT1JUIFJlcXVlc3RJZDogMTQ2ZmNmY2YtYzMyYi00M2Y1LTgyYTYtZWUwZjMxMzJkODcz`, Payload: encoder.encode(output), }; }); }); afterEach(() => { mockLambdaClient.restore(); }); beforeAll(async () => { const config = await loadTestConfig(); await initApp(app, config); accessToken = await initTestAuth(); const res = await request(app) .post(`/fhir/R4/Bot`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Bot', identifier: [{ system: 'https://example.com/bot', value: randomUUID() }], name: 'Test Bot', runtimeVersion: 'awslambda', code: ` export async function handler(medplum, event) { console.log('input', event.input); return event.input; } `, }); expect(res.status).toBe(201); bot = res.body as Bot; }); afterAll(async () => { await shutdownApp(); }); test('Submit plain text', async () => { const res = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$execute`) .set('Content-Type', ContentType.TEXT) .set('Authorization', 'Bearer ' + accessToken) .send('input'); expect(res.status).toBe(200); expect(res.headers['content-type']).toBe('text/plain; charset=utf-8'); expect(res.text).toStrictEqual('input'); }); test('Submit FHIR with content type', async () => { const res = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$execute`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient', name: [{ given: ['John'], family: ['Doe'] }], }); expect(res.status).toBe(200); expect(res.headers['content-type']).toBe('application/json; charset=utf-8'); }); test('Submit FHIR without content type', async () => { const res = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$execute`) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Patient', name: [{ given: ['John'], family: ['Doe'] }], }); expect(res.status).toBe(200); expect(res.headers['content-type']).toBe('application/json; charset=utf-8'); }); test('Submit HL7', async () => { const binaryStorage = getBinaryStorage(); const writeFileSpy = jest.spyOn(binaryStorage, 'writeFile'); const text = 'MSH|^~\\&|Main_HIS|XYZ_HOSPITAL|iFW|ABC_Lab|20160915003015||ACK|9B38584D|P|2.6.1|\r' + 'MSA|AA|9B38584D|Everything was okay dokay!|'; const res = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$execute`) .set('Content-Type', ContentType.HL7_V2) .set('Authorization', 'Bearer ' + accessToken) .send(text); expect(res.status).toBe(200); expect(res.headers['content-type']).toBe('x-application/hl7-v2+er7; charset=utf-8'); expect(writeFileSpy).toHaveBeenCalledTimes(1); const args = writeFileSpy.mock.calls[0]; expect(args.length).toBe(3); expect(args[0]).toMatch(/^bot\//); expect(args[1]).toBe(ContentType.JSON); const row = JSON.parse(args[2] as string); expect(row.botId).toStrictEqual(bot.id); expect(row.hl7MessageType).toStrictEqual('ACK'); expect(row.hl7Version).toStrictEqual('2.6.1'); }); test('Execute without code', async () => { // Create a bot with empty code const res1 = await request(app) .post(`/fhir/R4/Bot`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Bot', name: 'Test Bot', code: '', }); expect(res1.status).toBe(201); const bot = res1.body as Bot; // Execute the bot const res2 = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$execute`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({}); expect(res2.status).toBe(400); }); test('Unsupported runtime version', async () => { const res1 = await request(app) .post(`/fhir/R4/Bot`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({ resourceType: 'Bot', name: 'Test Bot', runtimeVersion: 'unsupported', }); expect(res1.status).toBe(201); const bot = res1.body as Bot; // Step 2: Publish the 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: ` export async function handler() { console.log('input', input); return input; } `, }); expect(res2.status).toBe(200); // Step 3: Execute the bot const res3 = await request(app) .post(`/fhir/R4/Bot/${bot.id}/$execute`) .set('Content-Type', ContentType.FHIR_JSON) .set('Authorization', 'Bearer ' + accessToken) .send({}); expect(res3.status).toBe(400); }); test('Get function name', async () => { const config = getConfig(); const normalBot: Bot = { resourceType: 'Bot', id: '123' }; const customBot: Bot = { resourceType: 'Bot', id: '456', identifier: [{ system: 'https://medplum.com/bot-external-function-id', value: 'custom' }], }; expect(getLambdaFunctionName(normalBot)).toStrictEqual('medplum-bot-lambda-123'); expect(getLambdaFunctionName(customBot)).toStrictEqual('medplum-bot-lambda-456'); // Temporarily enable custom bot support config.botCustomFunctionsEnabled = true; expect(getLambdaFunctionName(normalBot)).toStrictEqual('medplum-bot-lambda-123'); expect(getLambdaFunctionName(customBot)).toStrictEqual('custom'); config.botCustomFunctionsEnabled = false; }); test('Execute by identifier', async () => { const res = await request(app) .post(`/fhir/R4/Bot/$execute?identifier=${bot.identifier?.[0]?.system}|${bot.identifier?.[0]?.value}`) .set('Content-Type', ContentType.TEXT) .set('Authorization', 'Bearer ' + accessToken) .send('input'); expect(res.status).toBe(200); expect(res.headers['content-type']).toBe('text/plain; charset=utf-8'); expect(res.text).toStrictEqual('input'); }); });

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