Skip to main content
Glama
epic-query-patient.test.ts11.7 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { getIdentifier, getReferenceString, indexSearchParameterBundle, indexStructureDefinitionBundle, resolveId, } from '@medplum/core'; import { readJson, SEARCH_PARAMETER_BUNDLE_FILES } from '@medplum/definitions'; import type { AllergyIntolerance, Bundle, MedicationRequest, Patient, Resource, SearchParameter, } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { generateKeyPairSync } from 'crypto'; import fetch from 'node-fetch'; import type { Mock } from 'vitest'; import { handler } from './epic-query-patient'; import { epicAllergyIntolerance, epicMedication, epicMedicationRequest, epicOrganization, epicPatient, epicPractitioner, medplumPatientWithoutEpicIdentifier, } from './epic-query-patient-test-data'; vi.mock('node-fetch'); describe('Epic Query Patient Bot', () => { let medplum: MockClient; let input: Patient; const bot = { reference: 'Bot/123' }; const contentType = 'application/fhir+json'; const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); const secrets = { EPIC_PRIVATE_KEY: { name: 'EPIC_PRIVATE_KEY', valueString: privateKey }, EPIC_CLIENT_ID: { name: 'EPIC_CLIENT_ID', valueString: 'test-client-id' }, }; 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.resetAllMocks(); (fetch as unknown as Mock).mockClear(); medplum = new MockClient(); input = await medplum.createResource({ ...medplumPatientWithoutEpicIdentifier, }); }); function createSearchSetBundle<T extends Resource>(...resources: T[]): Bundle<T> { return { resourceType: 'Bundle', type: 'searchset', total: resources.length, entry: resources.map((resource) => ({ resource })), }; } test('throws error when missing EPIC_CLIENT_ID', async () => { await expect( handler(medplum, { bot, input, secrets: { EPIC_PRIVATE_KEY: secrets.EPIC_PRIVATE_KEY }, contentType, }) ).rejects.toThrow('Missing EPIC_CLIENT_ID'); }); test('throws error when missing EPIC_PRIVATE_KEY', async () => { await expect( handler(medplum, { bot, input, secrets: { EPIC_CLIENT_ID: secrets.EPIC_CLIENT_ID }, contentType, }) ).rejects.toThrow('Missing EPIC_PRIVATE_KEY'); }); test('successfully syncs an existing Epic patient and related resources to Medplum', async () => { input = await medplum.createResource({ ...medplumPatientWithoutEpicIdentifier, identifier: [ { system: 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id', value: 'epic-patient-123', }, ], }); (fetch as unknown as Mock).mockImplementation(async (url: string, options?: any): Promise<any> => { const { Response } = await vi.importActual<{ Response: any }>('node-fetch'); const urlString = url.toString(); if (urlString.includes('/oauth2/token')) { return new Response(JSON.stringify({ access_token: 'mock-epic-access-token', expires_in: 300 }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } if (!options?.headers?.Authorization?.startsWith('Bearer ')) { return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }); } if (urlString.endsWith('/api/FHIR/R4/Patient/epic-patient-123')) { return new Response(JSON.stringify(epicPatient), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } if (urlString.includes('/api/FHIR/R4/Organization/') && urlString.endsWith(epicOrganization.id ?? '')) { return new Response(JSON.stringify(epicOrganization), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } if (urlString.includes('/api/FHIR/R4/Practitioner/') && urlString.endsWith(epicPractitioner.id ?? '')) { return new Response(JSON.stringify(epicPractitioner), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } if (urlString.includes('/api/FHIR/R4/AllergyIntolerance?patient=epic-patient-123')) { const bundle = createSearchSetBundle<AllergyIntolerance>(epicAllergyIntolerance); return new Response(JSON.stringify(bundle), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } if (urlString.includes('/api/FHIR/R4/MedicationRequest?patient=epic-patient-123')) { const bundle = createSearchSetBundle<MedicationRequest>(epicMedicationRequest); return new Response(JSON.stringify(bundle), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } if (urlString.includes('/api/FHIR/R4/Medication/') && urlString.endsWith(epicMedication.id ?? '')) { return new Response(JSON.stringify(epicMedication), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } console.error(`Mock Fetch: Unhandled URL: ${urlString}`); return new Response(JSON.stringify({ message: 'Not Found in Mock' }), { status: 404 }); }); const patient = await handler(medplum, { bot, input, secrets, contentType, }); expect(patient).toBeDefined(); expect(patient?.resourceType).toStrictEqual('Patient'); expect( getIdentifier(patient as Patient, 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id') ).toStrictEqual('epic-patient-123'); // managingOrganization expect(patient?.managingOrganization).toBeDefined(); const managingOrganization = await medplum.readResource( 'Organization', resolveId(patient?.managingOrganization) as string ); expect(managingOrganization).toBeDefined(); expect(getIdentifier(managingOrganization, 'http://hl7.org/fhir/sid/us-npi')).toStrictEqual( getIdentifier(epicOrganization, 'http://hl7.org/fhir/sid/us-npi') ); // generalPractitioner expect(patient?.generalPractitioner).toHaveLength(1); const generalPractitioner = await medplum.readResource( 'Practitioner', resolveId(patient?.generalPractitioner?.[0]) as string ); expect(generalPractitioner).toBeDefined(); expect(getIdentifier(generalPractitioner, 'http://hl7.org/fhir/sid/us-npi')).toStrictEqual( getIdentifier(epicPractitioner, 'http://hl7.org/fhir/sid/us-npi') ); // allergies const allergies = await medplum.searchResources('AllergyIntolerance', { patient: getReferenceString(patient), }); expect(allergies).toHaveLength(1); expect(allergies[0].code?.coding?.[0].code).toStrictEqual(epicAllergyIntolerance.code?.coding?.[0].code); // medicationRequests const medRequests = await medplum.searchResources('MedicationRequest', { patient: getReferenceString(patient), }); expect(medRequests).toHaveLength(1); expect(medRequests[0].medicationReference).toBeDefined(); const medication = await medplum.readResource( 'Medication', resolveId(medRequests[0].medicationReference) as string ); expect(medication.code?.coding?.[0].code).toStrictEqual(epicMedication.code?.coding?.[0].code); }); test('successfully creates a new Epic patient when one does not exist', async () => { const ssn = getIdentifier(medplumPatientWithoutEpicIdentifier, 'http://hl7.org/fhir/sid/us-ssn'); const newEpicPatientId = 'new-epic-patient-456'; (fetch as unknown as Mock).mockImplementation(async (url: string, options?: any): Promise<any> => { const { Response } = await vi.importActual<{ Response: any }>('node-fetch'); const urlString = url.toString(); const parsedUrl = new URL(urlString); if (urlString.includes('/oauth2/token')) { return new Response(JSON.stringify({ access_token: 'mock-epic-access-token', expires_in: 300 }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } if (!options?.headers?.Authorization?.startsWith('Bearer ')) { return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }); } // Mock POST to create patient if (parsedUrl.pathname.endsWith('/api/FHIR/R4/Patient') && options?.method === 'POST') { // Check if the SSN identifier system was correctly changed to OID const body = JSON.parse(options.body); const ssnIdentifier = body.identifier?.find((id: any) => id.value === ssn); expect(ssnIdentifier?.system).toBe('urn:oid:2.16.840.1.113883.4.1'); return new Response(null, { status: 201, // Created headers: { Location: `/api/FHIR/R4/Patient/${newEpicPatientId}` }, }); } // Mock GET search for patient by SSN value only (as generated by searchOne) if ( parsedUrl.pathname.endsWith('/api/FHIR/R4/Patient') && options?.method === 'GET' && parsedUrl.searchParams.get('identifier') === ssn && parsedUrl.searchParams.get('_count') === '1' ) { // Return the patient, assigning the new Epic ID const bundle = createSearchSetBundle<Patient>({ ...epicPatient, // Use the base epic patient data id: newEpicPatientId, // Assign the ID expected after creation identifier: [ // Ensure identifiers match what Epic might return (including OID SSN) ...(epicPatient.identifier?.filter((id) => id.system !== 'http://hl7.org/fhir/sid/us-ssn') ?? []), { system: 'urn:oid:2.16.840.1.113883.4.1', value: ssn }, { system: 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id', value: newEpicPatientId }, ], }); return new Response(JSON.stringify(bundle), { status: 200, headers: { 'Content-Type': 'application/fhir+json' }, }); } console.error(`Mock Fetch: Unhandled URL: ${urlString}`); return new Response(JSON.stringify({ message: 'Not Found in Mock' }), { status: 404 }); }); // Input patient does not have an Epic identifier, but has SSN expect(getIdentifier(medplumPatientWithoutEpicIdentifier, 'http://hl7.org/fhir/sid/us-ssn')).toBeDefined(); expect(getIdentifier(input, 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id')).toBeUndefined(); const patient = await handler(medplum, { bot, input, secrets, contentType, }); expect(patient).toBeDefined(); expect(patient?.resourceType).toStrictEqual('Patient'); // Verify the new Epic identifier was added to the Medplum patient expect(patient.identifier).toHaveLength(2); expect(getIdentifier(patient, 'http://hl7.org/fhir/sid/us-ssn')).toStrictEqual(ssn); expect(getIdentifier(patient, 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id')).toStrictEqual( newEpicPatientId ); }); });

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