import type { BinaryDataQueryDto, BinaryDataSignedQueryDto } from '@n8n/api-types'; import type { Request, Response } from 'express'; import { mock } from 'jest-mock-extended'; import { JsonWebTokenError } from 'jsonwebtoken'; import type { BinaryDataService } from 'n8n-core'; import { FileNotFoundError } from 'n8n-core'; import type { Readable } from 'node:stream'; import { BinaryDataController } from '../binary-data.controller'; describe('BinaryDataController', () => { const request = mock(); const response = mock(); const binaryDataService = mock(); const controller = new BinaryDataController(binaryDataService); beforeEach(() => { jest.resetAllMocks(); response.status.mockReturnThis(); }); describe('get', () => { it('should return 400 if binary data mode is missing', async () => { const query = { id: '123', action: 'view' } as BinaryDataQueryDto; await controller.get(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.end).toHaveBeenCalledWith('Malformed binary data ID'); }); it('should return 400 if binary data mode is invalid', async () => { const query = { id: 'invalidMode:123', action: 'view' } as BinaryDataQueryDto; await controller.get(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.end).toHaveBeenCalledWith('Invalid binary data mode'); }); it('should return 404 if file is not found', async () => { const query = { id: 'filesystem:123', action: 'view', mimeType: 'image/jpeg', } as BinaryDataQueryDto; binaryDataService.getAsStream.mockRejectedValue(new FileNotFoundError('File not found')); await controller.get(request, response, query); expect(response.status).toHaveBeenCalledWith(404); expect(response.end).toHaveBeenCalled(); }); it('should set Content-Type and Content-Length from query if provided', async () => { const query = { id: 'filesystem:123', action: 'view', fileName: 'test.txt', mimeType: 'text/plain', } as BinaryDataQueryDto; binaryDataService.getAsStream.mockResolvedValue(mock()); await controller.get(request, response, query); expect(binaryDataService.getMetadata).toHaveBeenCalledWith(query.id); expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/plain'); expect(response.setHeader).not.toHaveBeenCalledWith('Content-Length'); expect(response.setHeader).not.toHaveBeenCalledWith('Content-Disposition'); }); it('should set Content-Type and Content-Length from metadata if not provided', async () => { const query = { id: 'filesystem:123', action: 'view' } as BinaryDataQueryDto; binaryDataService.getMetadata.mockResolvedValue({ fileName: 'test.txt', mimeType: 'text/plain', fileSize: 100, }); binaryDataService.getAsStream.mockResolvedValue(mock()); await controller.get(request, response, query); expect(binaryDataService.getMetadata).toHaveBeenCalledWith('filesystem:123'); expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'text/plain'); expect(response.setHeader).toHaveBeenCalledWith('Content-Length', 100); expect(response.setHeader).not.toHaveBeenCalledWith('Content-Disposition'); }); it('should set Content-Disposition for download action', async () => { const query = { id: 'filesystem:123', action: 'download', fileName: 'test.txt', } as BinaryDataQueryDto; binaryDataService.getAsStream.mockResolvedValue(mock()); await controller.get(request, response, query); expect(response.setHeader).toHaveBeenCalledWith( 'Content-Disposition', 'attachment; filename="test.txt"', ); }); it('should not allow viewing of html files', async () => { const query = { id: 'filesystem:123', action: 'view', fileName: 'test.html', mimeType: 'text/html', } as BinaryDataQueryDto; binaryDataService.getAsStream.mockResolvedValue(mock()); await controller.get(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.setHeader).not.toHaveBeenCalled(); }); it('should allow viewing of jpeg files, and handle mime-type casing', async () => { const query = { id: 'filesystem:123', action: 'view', fileName: 'test.jpg', mimeType: 'image/Jpeg', } as BinaryDataQueryDto; binaryDataService.getAsStream.mockResolvedValue(mock()); await controller.get(request, response, query); expect(response.status).not.toHaveBeenCalledWith(400); expect(response.setHeader).toHaveBeenCalledWith('Content-Type', query.mimeType); }); it('should return the file stream on success', async () => { const query = { id: 'filesystem:123', action: 'view', mimeType: 'image/jpeg', } as BinaryDataQueryDto; const stream = mock(); binaryDataService.getAsStream.mockResolvedValue(stream); const result = await controller.get(request, response, query); expect(result).toBe(stream); expect(binaryDataService.getAsStream).toHaveBeenCalledWith('filesystem:123'); }); describe('with malicious binary data IDs', () => { it.each([ ['filesystem:'], ['filesystem-v2:'], ['filesystem:/'], ['filesystem-v2:/'], ['filesystem://'], ['filesystem-v2://'], ])('should return 400 for ID "%s"', async (maliciousId) => { const query = { id: maliciousId, action: 'download' } as BinaryDataQueryDto; await controller.get(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.end).toHaveBeenCalledWith('Malformed binary data ID'); }); }); }); describe('getSigned', () => { const query = { token: '12344' } as BinaryDataSignedQueryDto; it('should return 400 if binary data mode is missing', async () => { binaryDataService.validateSignedToken.mockReturnValueOnce('123'); await controller.getSigned(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.end).toHaveBeenCalledWith('Malformed binary data ID'); }); it('should return 400 if binary data mode is invalid', async () => { binaryDataService.validateSignedToken.mockReturnValueOnce('invalid:123'); await controller.getSigned(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.end).toHaveBeenCalledWith('Invalid binary data mode'); }); it('should return 400 if token is invalid', async () => { binaryDataService.validateSignedToken.mockImplementation(() => { throw new JsonWebTokenError('Invalid token'); }); await controller.getSigned(request, response, query); expect(response.status).toHaveBeenCalledWith(400); expect(response.end).toHaveBeenCalledWith('Invalid token'); }); it('should return 404 if file is not found', async () => { binaryDataService.validateSignedToken.mockReturnValueOnce('filesystem:123'); binaryDataService.getAsStream.mockRejectedValue(new FileNotFoundError('File not found')); await controller.getSigned(request, response, query); expect(response.status).toHaveBeenCalledWith(404); expect(response.end).toHaveBeenCalled(); }); it('should return the file stream on a valid signed token', async () => { binaryDataService.validateSignedToken.mockReturnValueOnce('filesystem:123'); const stream = mock(); binaryDataService.getAsStream.mockResolvedValue(stream); const result = await controller.getSigned(request, response, query); expect(result).toBe(stream); expect(binaryDataService.getAsStream).toHaveBeenCalledWith('filesystem:123'); }); }); });