2023-10-10 01:06:06 -07:00
|
|
|
import fs from 'node:fs';
|
|
|
|
import fsp from 'node:fs/promises';
|
|
|
|
import { tmpdir } from 'node:os';
|
2024-09-30 06:38:56 -07:00
|
|
|
import path from 'node:path';
|
|
|
|
|
2023-10-10 01:06:06 -07:00
|
|
|
import { FileSystemManager } from '@/BinaryData/FileSystem.manager';
|
|
|
|
import { isStream } from '@/ObjectStore/utils';
|
2024-09-30 06:38:56 -07:00
|
|
|
|
2023-10-10 01:06:06 -07:00
|
|
|
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);
|
2023-11-03 03:41:15 -07:00
|
|
|
fsp.access = jest.fn().mockImplementation(async () => {});
|
2023-10-10 01:06:06 -07:00
|
|
|
|
|
|
|
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);
|
2023-11-03 03:41:15 -07:00
|
|
|
fsp.access = jest.fn().mockImplementation(async () => {});
|
2023-10-10 01:06:06 -07:00
|
|
|
|
|
|
|
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);
|
2023-11-03 03:41:15 -07:00
|
|
|
fsp.writeFile = jest.fn().mockResolvedValue(undefined);
|
2023-10-10 01:06:06 -07:00
|
|
|
|
|
|
|
const result = await fsManager.copyByFilePath(
|
|
|
|
workflowId,
|
|
|
|
executionId,
|
|
|
|
sourceFilePath,
|
|
|
|
metadata,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(fsp.cp).toHaveBeenCalledWith(sourceFilePath, targetPath);
|
2023-11-03 03:41:15 -07:00
|
|
|
expect(fsp.writeFile).toHaveBeenCalledWith(
|
|
|
|
`${toFullFilePath(otherFileId)}.metadata`,
|
|
|
|
JSON.stringify({ ...metadata, fileSize: mockBuffer.length }),
|
|
|
|
{ encoding: 'utf-8' },
|
|
|
|
);
|
2023-10-10 01:06:06 -07:00
|
|
|
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);
|
|
|
|
});
|
2023-10-13 02:52:17 -07:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
2023-10-10 01:06:06 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
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`);
|
|
|
|
});
|
|
|
|
});
|