Skip to main content
Glama
download.test.ts16.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ContentType } from '@medplum/core'; import type { DocumentReference, Media } from '@medplum/fhirtypes'; import type { Job } from 'bullmq'; import { randomUUID } from 'crypto'; import fetch from 'node-fetch'; import { Readable } from 'stream'; import { initAppServices, shutdownApp } from '../app'; import { getConfig, loadTestConfig } from '../config/loader'; import type { Repository } from '../fhir/repo'; import { createTestProject, withTestContext } from '../test.setup'; import { execDownloadJob, getDownloadQueue } from './download'; jest.mock('node-fetch'); let repo: Repository; describe('Download Worker', () => { beforeAll(async () => { const config = await loadTestConfig(); await initAppServices(config); repo = (await createTestProject({ withRepo: true })).repo; }); afterAll(async () => { await shutdownApp(); }); beforeEach(async () => { (fetch as unknown as jest.Mock).mockClear(); getConfig().autoDownloadEnabled = true; }); test('Download external URL', () => withTestContext( async () => { const url = 'https://example.com/download'; const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url, }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalled(); const body = new Readable(); body.push('foo'); body.push(null); (fetch as unknown as jest.Mock).mockImplementation(() => ({ status: 200, headers: { get(name: string): string | undefined { return { 'content-disposition': 'attachment; filename=download', 'content-type': ContentType.TEXT, }[name]; }, }, body, })); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; await execDownloadJob(job); expect(fetch).toHaveBeenCalledWith(url, { headers: { 'x-trace-id': '00-12345678901234567890123456789012-3456789012345678-01', traceparent: '00-12345678901234567890123456789012-3456789012345678-01', }, }); const updatedMedia = await repo.readResource<Media>('Media', media.id); expect(updatedMedia.content?.url).toMatch(/^Binary\//); expect(updatedMedia.meta?.author?.reference).toBe('system'); }, { traceId: '00-12345678901234567890123456789012-3456789012345678-01' } )); test('Ignore media missing URL', () => withTestContext(async () => { const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: '', }, }); expect(media).toBeDefined(); expect(queue.add).not.toHaveBeenCalled(); })); test('Ignore HTTP URL', () => withTestContext(async () => { const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'http://localhost/download', }, }); expect(media).toBeDefined(); expect(queue.add).not.toHaveBeenCalled(); })); test('Retry on 400', () => withTestContext(async () => { const url = 'https://example.com/download'; const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url, }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalled(); (fetch as unknown as jest.Mock).mockImplementation(() => ({ status: 400 })); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; // If the job throws, then the QueueScheduler will retry await expect(execDownloadJob(job)).rejects.toThrow(); })); test('Retry on exception', () => withTestContext(async () => { const url = 'https://example.com/download'; const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url, }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalled(); (fetch as unknown as jest.Mock).mockImplementation(() => { throw new Error(); }); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; // If the job throws, then the QueueScheduler will retry await expect(execDownloadJob(job)).rejects.toThrow(); })); test('Stop retries if Resource deleted', () => withTestContext(async () => { const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(queue.add).toHaveBeenCalled(); // At this point the job should be in the queue // But let's delete the resource await repo.deleteResource('Media', media.id); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; await execDownloadJob(job); // Fetch should not have been called expect(fetch).not.toHaveBeenCalled(); })); test('Stop if URL changed', () => withTestContext(async () => { const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalled(); // At this point the job should be in the queue // But let's change the URL to an internal Binary resource await repo.updateResource({ ...(media as Media), content: { contentType: ContentType.TEXT, url: 'Binary/' + randomUUID(), }, }); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; await execDownloadJob(job); // Fetch should not have been called expect(fetch).not.toHaveBeenCalled(); })); test('Ignore if disabled', () => withTestContext(async () => { const config = getConfig(); config.autoDownloadEnabled = false; const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media).toBeDefined(); expect(queue.add).not.toHaveBeenCalled(); })); test('Ignore if disabled in project', () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true, project: { setting: [ { name: 'autoDownloadEnabled', valueBoolean: false, }, ], }, }); const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media).toBeDefined(); expect(queue.add).not.toHaveBeenCalled(); })); test('Ignore if matches URL prefix', () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true, project: { setting: [ { name: 'autoDownloadIgnoredUrlPrefixes', valueString: 'https://ignore.example.com', }, ], }, }); const queue = getDownloadQueue() as any; queue.add.mockClear(); const media1 = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://ignore.example.com/download', }, }); expect(media1).toBeDefined(); expect(queue.add).not.toHaveBeenCalled(); // Ensure that other URLs still work const media2 = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media2).toBeDefined(); expect(queue.add).toHaveBeenCalled(); })); test('Ignore if does not match allowed URL prefix', () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true, project: { setting: [ { name: 'autoDownloadAllowedUrlPrefixes', valueString: 'https://allowed.example.com', }, ], }, }); const queue = getDownloadQueue() as any; queue.add.mockClear(); const media1 = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://ignore.example.com/download', }, }); expect(media1).toBeDefined(); expect(queue.add).not.toHaveBeenCalled(); // Ensure that other URLs still work const media2 = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://allowed.example.com/download', }, }); expect(media2).toBeDefined(); expect(queue.add).toHaveBeenCalled(); })); test('Stop retries if auto download disabled', () => withTestContext(async () => { const { project, repo } = await createTestProject({ withRepo: true }); const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalled(); // At this point the job should be in the queue // But let's disable auto download in the project await repo.updateResource({ ...project, setting: [{ name: 'autoDownloadEnabled', valueBoolean: false }], }); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; await execDownloadJob(job); // Fetch should not have been called expect(fetch).not.toHaveBeenCalled(); })); test('Does not enqueue when mutating non-URL fields', () => withTestContext(async () => { const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalledTimes(1); queue.add.mockClear(); await repo.updateResource<Media>({ ...media, status: 'in-progress', }); expect(queue.add).not.toHaveBeenCalled(); })); test('Updates only matching attachment paths', () => withTestContext(async () => { const queue = getDownloadQueue() as any; queue.add.mockClear(); const firstUrl = 'https://example.com/download-1'; const secondUrl = 'https://example.com/download-2'; const doc = await repo.createResource<DocumentReference>({ resourceType: 'DocumentReference', status: 'current', content: [ { attachment: { contentType: ContentType.TEXT, url: firstUrl, }, }, { attachment: { contentType: ContentType.TEXT, url: secondUrl, }, }, ], }); expect(doc).toBeDefined(); expect(queue.add).toHaveBeenCalledTimes(2); const body1 = new Readable(); body1.push('foo1'); body1.push(null); const body2 = new Readable(); body2.push('foo2'); body2.push(null); (fetch as unknown as jest.Mock).mockImplementation((url: string) => url === firstUrl ? { status: 200, headers: { get(name: string): string | undefined { return { 'content-disposition': 'attachment; filename=download-1', 'content-type': ContentType.TEXT, }[name]; }, }, body: body1, } : { status: 200, headers: { get(name: string): string | undefined { return { 'content-disposition': 'attachment; filename=download-2', 'content-type': ContentType.TEXT, }[name]; }, }, body: body2, } ); const job1 = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; await execDownloadJob(job1); const afterFirstDownload = await repo.readResource<DocumentReference>('DocumentReference', doc.id); expect(afterFirstDownload.content?.[0]?.attachment?.url).toMatch(/^Binary\//); expect(afterFirstDownload.content?.[1]?.attachment?.url).toBe(secondUrl); expect(afterFirstDownload.meta?.author?.reference).toBe('system'); const job2 = { id: 2, data: queue.add.mock.calls[1][1] } as unknown as Job; await execDownloadJob(job2); const afterSecondDownload = await repo.readResource<DocumentReference>('DocumentReference', doc.id); expect(afterSecondDownload.content?.[0]?.attachment?.url).toBe(afterFirstDownload.content[0].attachment.url); expect(afterSecondDownload.content?.[1]?.attachment?.url).toMatch(/^Binary\//); expect(afterSecondDownload.meta?.author?.reference).toBe('system'); })); test('Stop retries if auto download disabled', () => withTestContext(async () => { const { project, repo } = await createTestProject({ withRepo: true }); const queue = getDownloadQueue() as any; queue.add.mockClear(); const media = await repo.createResource<Media>({ resourceType: 'Media', status: 'completed', content: { contentType: ContentType.TEXT, url: 'https://example.com/download', }, }); expect(media).toBeDefined(); expect(queue.add).toHaveBeenCalled(); // At this point the job should be in the queue // But let's disable auto download in the project await repo.updateResource({ ...project, setting: [{ name: 'autoDownloadIgnoredUrlPrefixes', valueString: 'https://example.com' }], }); const job = { id: 1, data: queue.add.mock.calls[0][1] } as unknown as Job; await execDownloadJob(job); // Fetch should not have been called expect(fetch).not.toHaveBeenCalled(); })); });

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