n8n/packages/core/test/FileSystem.manager.test.ts
Iván Ovejero 6d42fad31a
refactor(core): Consolidate binary file not found errors (no-changelog) (#7585)
Logging was originally to see if there was a binary data file failing to
be written for [this
user](https://linear.app/n8n/issue/PAY-844/filesystem-binary-data-mode-causing-alerts-in-cloud)
but the cause was not a file failed to be written but a missing `fileId`
in a binary data item in an execution. The error should no longer be
thrown as of 1.12. See story for more info.

This PR is for cleanup and to consolidate any file not found errors in
the context of binary data, to track if this happens again.
2023-11-03 11:41:15 +01:00

193 lines
5.5 KiB
TypeScript

import path from 'node:path';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { FileSystemManager } from '@/BinaryData/FileSystem.manager';
import { isStream } from '@/ObjectStore/utils';
import { toFileId, toStream } from './utils';
jest.mock('fs');
jest.mock('fs/promises');
const storagePath = tmpdir();
const fsManager = new FileSystemManager(storagePath);
const toFullFilePath = (fileId: string) => path.join(storagePath, fileId);
const workflowId = 'ObogjVbqpNOQpiyV';
const executionId = '999';
const fileUuid = '71f6209b-5d48-41a2-a224-80d529d8bb32';
const fileId = toFileId(workflowId, executionId, fileUuid);
const otherWorkflowId = 'FHio8ftV6SrCAfPJ';
const otherExecutionId = '888';
const otherFileUuid = '71f6209b-5d48-41a2-a224-80d529d8bb33';
const otherFileId = toFileId(otherWorkflowId, otherExecutionId, otherFileUuid);
const mockBuffer = Buffer.from('Test data');
const mockStream = toStream(mockBuffer);
afterAll(() => {
jest.restoreAllMocks();
});
describe('store()', () => {
it('should store a buffer', async () => {
const metadata = { mimeType: 'text/plain' };
const result = await fsManager.store(workflowId, executionId, mockBuffer, metadata);
expect(result.fileSize).toBe(mockBuffer.length);
});
});
describe('getPath()', () => {
it('should return a path', async () => {
const filePath = fsManager.getPath(fileId);
expect(filePath).toBe(toFullFilePath(fileId));
});
});
describe('getAsBuffer()', () => {
it('should return a buffer', async () => {
fsp.readFile = jest.fn().mockResolvedValue(mockBuffer);
fsp.access = jest.fn().mockImplementation(async () => {});
const result = await fsManager.getAsBuffer(fileId);
expect(Buffer.isBuffer(result)).toBe(true);
expect(fsp.readFile).toHaveBeenCalledWith(toFullFilePath(fileId));
});
});
describe('getAsStream()', () => {
it('should return a stream', async () => {
fs.createReadStream = jest.fn().mockReturnValue(mockStream);
fsp.access = jest.fn().mockImplementation(async () => {});
const stream = await fsManager.getAsStream(fileId);
expect(isStream(stream)).toBe(true);
expect(fs.createReadStream).toHaveBeenCalledWith(toFullFilePath(fileId), {
highWaterMark: undefined,
});
});
});
describe('getMetadata()', () => {
it('should return metadata', async () => {
const mimeType = 'text/plain';
const fileName = 'file.txt';
fsp.readFile = jest.fn().mockResolvedValue(
JSON.stringify({
fileSize: 1,
mimeType,
fileName,
}),
);
const metadata = await fsManager.getMetadata(fileId);
expect(metadata).toEqual(expect.objectContaining({ fileSize: 1, mimeType, fileName }));
});
});
describe('copyByFileId()', () => {
it('should copy by file ID and return the file ID', async () => {
fsp.copyFile = jest.fn().mockResolvedValue(undefined);
// @ts-expect-error - private method
jest.spyOn(fsManager, 'toFileId').mockReturnValue(otherFileId);
const targetFileId = await fsManager.copyByFileId(workflowId, executionId, fileId);
const sourcePath = toFullFilePath(fileId);
const targetPath = toFullFilePath(targetFileId);
expect(fsp.copyFile).toHaveBeenCalledWith(sourcePath, targetPath);
});
});
describe('copyByFilePath()', () => {
test('should copy by file path and return the file ID and size', async () => {
const sourceFilePath = tmpdir();
const metadata = { mimeType: 'text/plain' };
// @ts-expect-error - private method
jest.spyOn(fsManager, 'toFileId').mockReturnValue(otherFileId);
// @ts-expect-error - private method
jest.spyOn(fsManager, 'getSize').mockReturnValue(mockBuffer.length);
const targetPath = toFullFilePath(otherFileId);
fsp.cp = jest.fn().mockResolvedValue(undefined);
fsp.writeFile = jest.fn().mockResolvedValue(undefined);
const result = await fsManager.copyByFilePath(
workflowId,
executionId,
sourceFilePath,
metadata,
);
expect(fsp.cp).toHaveBeenCalledWith(sourceFilePath, targetPath);
expect(fsp.writeFile).toHaveBeenCalledWith(
`${toFullFilePath(otherFileId)}.metadata`,
JSON.stringify({ ...metadata, fileSize: mockBuffer.length }),
{ encoding: 'utf-8' },
);
expect(result.fileSize).toBe(mockBuffer.length);
});
});
describe('deleteMany()', () => {
it('should delete many files by workflow ID and execution ID', async () => {
const ids = [
{ workflowId, executionId },
{ workflowId: otherWorkflowId, executionId: otherExecutionId },
];
fsp.rm = jest.fn().mockResolvedValue(undefined);
const promise = fsManager.deleteMany(ids);
await expect(promise).resolves.not.toThrow();
expect(fsp.rm).toHaveBeenCalledTimes(2);
});
it('should suppress error on non-existing filepath', async () => {
const ids = [{ workflowId: 'does-not-exist', executionId: 'does-not-exist' }];
fsp.rm = jest.fn().mockResolvedValue(undefined);
const promise = fsManager.deleteMany(ids);
await expect(promise).resolves.not.toThrow();
expect(fsp.rm).toHaveBeenCalledTimes(1);
});
});
describe('rename()', () => {
it('should rename a file', async () => {
fsp.rename = jest.fn().mockResolvedValue(undefined);
fsp.rm = jest.fn().mockResolvedValue(undefined);
const promise = fsManager.rename(fileId, otherFileId);
const oldPath = toFullFilePath(fileId);
const newPath = toFullFilePath(otherFileId);
await expect(promise).resolves.not.toThrow();
expect(fsp.rename).toHaveBeenCalledTimes(2);
expect(fsp.rename).toHaveBeenCalledWith(oldPath, newPath);
expect(fsp.rename).toHaveBeenCalledWith(`${oldPath}.metadata`, `${newPath}.metadata`);
});
});