mirror of
https://github.com/n8n-io/n8n
synced 2026-04-19 13:05:54 +02:00
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Albert Alises <albert.alises@gmail.com> Co-authored-by: Jaakko Husso <jaakko@n8n.io> Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Tuukka Kantola <Tuukkaa@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Co-authored-by: Raúl Gómez Morales <raul00gm@gmail.com> Co-authored-by: Elias Meire <elias@meire.dev> Co-authored-by: Dimitri Lavrenük <dimitri.lavrenuek@n8n.io> Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
import type { Stats } from 'node:fs';
|
|
import * as fs from 'node:fs/promises';
|
|
|
|
import { textOf } from '../test-utils';
|
|
import { deleteTool } from './delete';
|
|
|
|
jest.mock('node:fs/promises');
|
|
|
|
const CONTEXT = { dir: '/base' };
|
|
|
|
function mockStatFile(): void {
|
|
jest.mocked(fs.stat).mockResolvedValue({ isDirectory: () => false } as unknown as Stats);
|
|
}
|
|
|
|
function mockStatDirectory(): void {
|
|
jest.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as unknown as Stats);
|
|
}
|
|
|
|
function mockStatNotFound(): void {
|
|
const error = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
|
|
jest.mocked(fs.stat).mockRejectedValue(error);
|
|
}
|
|
|
|
describe('deleteTool', () => {
|
|
beforeEach(() => {
|
|
jest.resetAllMocks();
|
|
(fs.realpath as jest.Mock).mockImplementation(async (p: string) => {
|
|
if (p === '/base') return await Promise.resolve('/base');
|
|
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
});
|
|
});
|
|
|
|
describe('metadata', () => {
|
|
it('has the correct name', () => {
|
|
expect(deleteTool.name).toBe('delete');
|
|
});
|
|
|
|
it('has a non-empty description', () => {
|
|
expect(deleteTool.description).not.toBe('');
|
|
});
|
|
});
|
|
|
|
describe('inputSchema validation', () => {
|
|
it('accepts a valid input', () => {
|
|
expect(() => deleteTool.inputSchema.parse({ path: 'src/old-file.ts' })).not.toThrow();
|
|
});
|
|
|
|
it('throws when path is missing', () => {
|
|
expect(() => deleteTool.inputSchema.parse({})).toThrow();
|
|
});
|
|
|
|
it('throws when path is not a string', () => {
|
|
expect(() => deleteTool.inputSchema.parse({ path: 123 })).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('execute', () => {
|
|
it('deletes a file using unlink', async () => {
|
|
mockStatFile();
|
|
jest.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
|
|
const result = await deleteTool.execute({ path: 'src/old.ts' }, CONTEXT);
|
|
|
|
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse
|
|
const data = JSON.parse(textOf(result)) as { path: string };
|
|
expect(data.path).toBe('src/old.ts');
|
|
expect(fs.unlink).toHaveBeenCalledWith('/base/src/old.ts');
|
|
expect(fs.rm).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('deletes a directory recursively using rm', async () => {
|
|
mockStatDirectory();
|
|
(fs.rm as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
const result = await deleteTool.execute({ path: 'old-dir' }, CONTEXT);
|
|
|
|
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse
|
|
const data = JSON.parse(textOf(result)) as { path: string };
|
|
expect(data.path).toBe('old-dir');
|
|
expect(fs.rm).toHaveBeenCalledWith('/base/old-dir', { recursive: true, force: false });
|
|
expect(fs.unlink).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns a single text content block', async () => {
|
|
mockStatFile();
|
|
jest.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
|
|
const result = await deleteTool.execute({ path: 'file.ts' }, CONTEXT);
|
|
|
|
expect(result.content).toHaveLength(1);
|
|
expect(result.content[0].type).toBe('text');
|
|
});
|
|
|
|
it('propagates error when path does not exist', async () => {
|
|
mockStatNotFound();
|
|
|
|
await expect(deleteTool.execute({ path: 'missing.ts' }, CONTEXT)).rejects.toThrow('ENOENT');
|
|
});
|
|
|
|
it('rejects path traversal', async () => {
|
|
await expect(deleteTool.execute({ path: '../../../etc/passwd' }, CONTEXT)).rejects.toThrow(
|
|
'escapes',
|
|
);
|
|
});
|
|
});
|
|
});
|