Skip to main content
Glama
voice-webhook.test.ts6.47 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { getReferenceString, indexSearchParameterBundle, indexStructureDefinitionBundle, unauthorized, } from '@medplum/core'; import { readJson, SEARCH_PARAMETER_BUNDLE_FILES } from '@medplum/definitions'; import type { Binary, Bundle, SearchParameter } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { validateRequest } from 'twilio/lib/webhooks/webhooks'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { handler } from './voice-webhook'; // Mock the Twilio validateRequest function vi.mock('twilio/lib/webhooks/webhooks', () => ({ validateRequest: vi.fn(), })); describe('Twilio Voice Webhook', () => { let medplum: MockClient; let bot: any; let projectMembership: any; const secrets = { TWILIO_AUTH_TOKEN: { name: 'TWILIO_AUTH_TOKEN', valueString: 'test-auth-token' }, TWILIO_NUMBER: { name: 'TWILIO_NUMBER', valueString: '+15551234567' }, }; beforeAll(() => { indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-medplum.json') as Bundle); for (const filename of SEARCH_PARAMETER_BUNDLE_FILES) { indexSearchParameterBundle(readJson(filename) as Bundle<SearchParameter>); } }); beforeEach(async () => { vi.clearAllMocks(); medplum = new MockClient(); // Create a bot bot = await medplum.createResource({ resourceType: 'Bot', name: 'Test Voice Bot', code: 'test-code', }); // Create a project membership for the bot projectMembership = await medplum.createResource({ resourceType: 'ProjectMembership', profile: getReferenceString(bot), project: { reference: 'Project/test-project' }, user: { reference: 'User/test-user' }, } as any); }); test('should return unauthorized when Twilio validation fails', async () => { // Mock validateRequest to return false (invalid signature) vi.mocked(validateRequest).mockReturnValue(false); const input = { From: '+15559876543', To: '+15551234567', CallSid: 'CA1234567890', }; const event = { bot: { reference: getReferenceString(bot) }, input, headers: { 'x-twilio-signature': 'invalid-signature', }, secrets, contentType: 'application/x-www-form-urlencoded', }; const result = await handler(medplum, event); expect(result).toEqual(unauthorized); expect(validateRequest).toHaveBeenCalledWith( 'test-auth-token', 'invalid-signature', `${medplum.getBaseUrl()}/webhook/${projectMembership.id}`, input ); }); test('should throw error when bot membership is not found', async () => { // Create a bot without membership const orphanBot = await medplum.createResource({ resourceType: 'Bot', name: 'Orphan Bot', code: 'orphan-code', }); const input = { From: '+15559876543', To: '+15551234567', CallSid: 'CA1234567890', }; const event = { bot: { reference: getReferenceString(orphanBot) }, input, headers: { 'x-twilio-signature': 'test-signature', }, secrets, contentType: 'application/x-www-form-urlencoded', }; await expect(handler(medplum, event)).rejects.toThrow('Could not find the bot membership'); }); test('should handle inbound call successfully', async () => { // Mock validateRequest to return true (valid signature) vi.mocked(validateRequest).mockReturnValue(true); const input = { From: '+15559876543', To: '+15551234567', // Same as TWILIO_NUMBER (inbound) CallSid: 'CA1234567890', }; const event = { bot: { reference: getReferenceString(bot) }, input, headers: { 'x-twilio-signature': 'valid-signature', }, secrets, contentType: 'application/x-www-form-urlencoded', }; const result = await handler(medplum, event); expect(result).toBeDefined(); expect(result.resourceType).toBe('Binary'); // Type assertion for Binary result const binaryResult = result as Binary; expect(binaryResult.contentType).toBe('application/xml'); expect(binaryResult.data).toBeDefined(); // Decode the base64 data to check TwiML content const twimlContent = Buffer.from(binaryResult.data as string, 'base64').toString('utf-8'); expect(twimlContent).toContain('<Response>'); expect(twimlContent).toContain('<Say>Thanks for calling Medplum. Please hold while we connect you.</Say>'); expect(twimlContent).toContain('<Dial>'); expect(twimlContent).toContain(`<Client>${secrets.TWILIO_NUMBER.valueString}</Client>`); expect(twimlContent).toContain('</Dial>'); expect(twimlContent).toContain('</Response>'); }); test('should handle outbound call successfully', async () => { // Mock validateRequest to return true (valid signature) vi.mocked(validateRequest).mockReturnValue(true); const input = { From: '+15551234567', // Same as TWILIO_NUMBER To: '+15559876543', // Different from TWILIO_NUMBER (outbound) CallSid: 'CA1234567890', }; const event = { bot: { reference: getReferenceString(bot) }, input, headers: { 'x-twilio-signature': 'valid-signature', }, secrets, contentType: 'application/x-www-form-urlencoded', }; const result = await handler(medplum, event); expect(result).toBeDefined(); expect(result.resourceType).toBe('Binary'); // Type assertion for Binary result const binaryResult = result as Binary; expect(binaryResult.contentType).toBe('application/xml'); expect(binaryResult.data).toBeDefined(); // Decode the base64 data to check TwiML content const twimlContent = Buffer.from(binaryResult.data as string, 'base64').toString('utf-8'); expect(twimlContent).toContain('<Response>'); expect(twimlContent).toContain('<Dial'); expect(twimlContent).toContain(`callerId="${secrets.TWILIO_NUMBER.valueString}"`); expect(twimlContent).toContain('<Number>+15559876543</Number>'); expect(twimlContent).toContain('</Dial>'); expect(twimlContent).toContain('</Response>'); }); });

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