| import { BinaryDataService, FileNotFoundError } from 'n8n-core'; | |
| import fsp from 'node:fs/promises'; | |
| import { Readable } from 'node:stream'; | |
| import { createOwner } from './shared/db/users'; | |
| import type { SuperAgentTest } from './shared/types'; | |
| import { setupTestServer } from './shared/utils'; | |
| import { mockInstance } from '../shared/mocking'; | |
| jest.mock('fs/promises'); | |
| const throwFileNotFound = () => { | |
| throw new FileNotFoundError('non/existing/path'); | |
| }; | |
| const binaryDataService = mockInstance(BinaryDataService); | |
| let testServer = setupTestServer({ endpointGroups: ['binaryData'] }); | |
| let authOwnerAgent: SuperAgentTest; | |
| beforeAll(async () => { | |
| const owner = await createOwner(); | |
| authOwnerAgent = testServer.authAgentFor(owner); | |
| }); | |
| afterEach(() => { | |
| jest.restoreAllMocks(); | |
| }); | |
| describe('GET /binary-data', () => { | |
| const fileId = '599c5f84007-7d14-4b63-8f1e-d726098d0cc0'; | |
| const fsBinaryDataId = `filesystem:${fileId}`; | |
| const s3BinaryDataId = `s3:${fileId}`; | |
| const binaryFilePath = `/Users/john/.n8n/binaryData/${fileId}`; | |
| const mimeType = 'text/plain'; | |
| const fileName = 'test.txt'; | |
| const buffer = Buffer.from('content'); | |
| const mockStream = new Readable(); | |
| mockStream.push(buffer); | |
| mockStream.push(null); | |
| describe('should reject on missing or invalid binary data ID', () => { | |
| test.each([['view'], ['download']])('on request to %s', async (action) => { | |
| binaryDataService.getPath.mockReturnValue(binaryFilePath); | |
| fsp.readFile = jest.fn().mockResolvedValue(buffer); | |
| await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| fileName, | |
| mimeType, | |
| action, | |
| }) | |
| .expect(400); | |
| await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| id: 'invalid', | |
| fileName, | |
| mimeType, | |
| action, | |
| }) | |
| .expect(400); | |
| }); | |
| }); | |
| describe('should return binary data [filesystem]', () => { | |
| test.each([['view'], ['download']])('on request to %s', async (action) => { | |
| binaryDataService.getAsStream.mockResolvedValue(mockStream); | |
| const res = await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| id: fsBinaryDataId, | |
| fileName, | |
| mimeType, | |
| action, | |
| }) | |
| .expect(200); | |
| const contentDisposition = | |
| action === 'download' ? `attachment; filename="${fileName}"` : undefined; | |
| expect(binaryDataService.getAsStream).toHaveBeenCalledWith(fsBinaryDataId); | |
| expect(res.headers['content-type']).toBe(mimeType); | |
| expect(res.headers['content-disposition']).toBe(contentDisposition); | |
| }); | |
| }); | |
| describe('should handle non-ASCII filename [filesystem]', () => { | |
| test('on request to download', async () => { | |
| const nonAsciiFileName = 'äöüß.png'; | |
| const res = await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| id: fsBinaryDataId, | |
| fileName: nonAsciiFileName, | |
| mimeType, | |
| action: 'download', | |
| }) | |
| .expect(200); | |
| expect(res.headers['content-disposition']).toBe( | |
| `attachment; filename="${encodeURIComponent(nonAsciiFileName)}"`, | |
| ); | |
| }); | |
| }); | |
| describe('should return 404 on file not found [filesystem]', () => { | |
| test.each(['view', 'download'])('on request to %s', async (action) => { | |
| binaryDataService.getAsStream.mockImplementation(throwFileNotFound); | |
| await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| id: fsBinaryDataId, | |
| fileName, | |
| mimeType, | |
| action, | |
| }) | |
| .expect(404); | |
| }); | |
| }); | |
| describe('should return binary data [s3]', () => { | |
| test.each([['view'], ['download']])('on request to %s', async (action) => { | |
| binaryDataService.getAsStream.mockResolvedValue(mockStream); | |
| const res = await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| id: s3BinaryDataId, | |
| fileName, | |
| mimeType, | |
| action, | |
| }) | |
| .expect(200); | |
| expect(binaryDataService.getAsStream).toHaveBeenCalledWith(s3BinaryDataId); | |
| const contentDisposition = | |
| action === 'download' ? `attachment; filename="${fileName}"` : undefined; | |
| expect(res.headers['content-type']).toBe(mimeType); | |
| expect(res.headers['content-disposition']).toBe(contentDisposition); | |
| }); | |
| }); | |
| describe('should return 404 on file not found [s3]', () => { | |
| test.each(['view', 'download'])('on request to %s', async (action) => { | |
| binaryDataService.getAsStream.mockImplementation(throwFileNotFound); | |
| await authOwnerAgent | |
| .get('/binary-data') | |
| .query({ | |
| id: s3BinaryDataId, | |
| fileName, | |
| mimeType, | |
| action, | |
| }) | |
| .expect(404); | |
| }); | |
| }); | |
| }); | |