move.test.ts•8.11 kB
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { moveFileTool } from '../../src/tools/filesystem/move.js';
import { SecurityValidator } from '../../src/security/validator.js';
import type { Config } from '../../src/types/config.js';
import { tmpdir } from 'node:os';
import { mkdirSync, writeFileSync, rmSync, realpathSync, existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
describe('move_file tool', () => {
  let testDir: string;
  let validator: SecurityValidator;
  let mockLogger: any;
  beforeEach(() => {
    // Create temp test directory
    testDir = join(tmpdir(), `absd-mcp-test-move-${Date.now()}`);
    mkdirSync(testDir, { recursive: true });
    // Resolve symlinks AFTER directory creation (macOS /tmp -> /private/var/folders)
    testDir = realpathSync(testDir);
    // Mock logger
    mockLogger = {
      debug: () => {},
      info: () => {},
      warn: () => {},
      error: () => {},
    };
    const config: Config = {
      // Use resolved path for config to handle macOS symlinks
      allowedDirectories: [testDir],
      blockedCommands: [],
      fileReadLineLimit: 1000,
      fileWriteLineLimit: 50,
      sessionTimeout: 30000,
      logLevel: 'error',
      urlDenylist: [],
      urlTimeout: 10000,
    };
    validator = new SecurityValidator(config, mockLogger);
  });
  afterEach(() => {
    // Cleanup
    try {
      rmSync(testDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
  });
  describe('Successful moves', () => {
    it('should move a file successfully', async () => {
      const sourcePath = join(testDir, 'source.txt');
      const destPath = join(testDir, 'destination.txt');
      writeFileSync(sourcePath, 'Test content');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      expect(result.content[0].text).toContain('Successfully moved');
      // Verify file was moved (source no longer exists, destination exists)
      expect(existsSync(sourcePath)).toBe(false);
      expect(existsSync(destPath)).toBe(true);
      expect(readFileSync(destPath, 'utf-8')).toBe('Test content');
    });
    it('should rename a file in the same directory', async () => {
      const sourcePath = join(testDir, 'old-name.txt');
      const destPath = join(testDir, 'new-name.txt');
      writeFileSync(sourcePath, 'Rename test');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].text).toContain('Successfully moved');
      expect(existsSync(sourcePath)).toBe(false);
      expect(existsSync(destPath)).toBe(true);
      expect(readFileSync(destPath, 'utf-8')).toBe('Rename test');
    });
    it('should move a file to a subdirectory', async () => {
      const sourcePath = join(testDir, 'file.txt');
      const subdir = join(testDir, 'subdir');
      mkdirSync(subdir, { recursive: true });
      const destPath = join(subdir, 'file.txt');
      writeFileSync(sourcePath, 'Move to subdir');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].text).toContain('Successfully moved');
      expect(existsSync(sourcePath)).toBe(false);
      expect(existsSync(destPath)).toBe(true);
      expect(readFileSync(destPath, 'utf-8')).toBe('Move to subdir');
    });
    it('should move a directory', async () => {
      const sourceDir = join(testDir, 'source-dir');
      const destDir = join(testDir, 'dest-dir');
      mkdirSync(sourceDir, { recursive: true });
      writeFileSync(join(sourceDir, 'file.txt'), 'Content in directory');
      const result = await moveFileTool(
        { source: sourceDir, destination: destDir },
        validator,
        mockLogger
      );
      expect(result.content[0].text).toContain('Successfully moved');
      expect(existsSync(sourceDir)).toBe(false);
      expect(existsSync(destDir)).toBe(true);
      expect(existsSync(join(destDir, 'file.txt'))).toBe(true);
    });
    it('should overwrite destination file if it exists', async () => {
      const sourcePath = join(testDir, 'source.txt');
      const destPath = join(testDir, 'destination.txt');
      writeFileSync(sourcePath, 'New content');
      writeFileSync(destPath, 'Old content');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].text).toContain('Successfully moved');
      expect(existsSync(sourcePath)).toBe(false);
      expect(existsSync(destPath)).toBe(true);
      expect(readFileSync(destPath, 'utf-8')).toBe('New content');
    });
  });
  describe('Security validation', () => {
    it('should reject source path outside allowed directories', async () => {
      const sourcePath = '/tmp/outside-allowed.txt';
      const destPath = join(testDir, 'destination.txt');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].type).toBe('text');
      expect(result.content[0].text).toContain('Error (source)');
    });
    it('should reject destination path outside allowed directories', async () => {
      const sourcePath = join(testDir, 'source.txt');
      const destPath = '/tmp/outside-allowed.txt';
      writeFileSync(sourcePath, 'Test');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].type).toBe('text');
      expect(result.content[0].text).toContain('Error (destination)');
      // Source should still exist since move failed
      expect(existsSync(sourcePath)).toBe(true);
    });
    it('should reject path traversal attempts in source', async () => {
      const sourcePath = join(testDir, '../../../etc/passwd');
      const destPath = join(testDir, 'destination.txt');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].text).toContain('Error (source)');
    });
    it('should reject path traversal attempts in destination', async () => {
      const sourcePath = join(testDir, 'source.txt');
      const destPath = join(testDir, '../../../tmp/evil.txt');
      writeFileSync(sourcePath, 'Test');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].text).toContain('Error (destination)');
      expect(existsSync(sourcePath)).toBe(true);
    });
  });
  describe('Error handling', () => {
    it('should handle non-existent source file', async () => {
      const sourcePath = join(testDir, 'nonexistent.txt');
      const destPath = join(testDir, 'destination.txt');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].type).toBe('text');
      expect(result.content[0].text).toContain('Error:');
    });
    it('should handle moving to non-existent directory', async () => {
      const sourcePath = join(testDir, 'source.txt');
      const destPath = join(testDir, 'nonexistent-dir', 'destination.txt');
      writeFileSync(sourcePath, 'Test');
      const result = await moveFileTool(
        { source: sourcePath, destination: destPath },
        validator,
        mockLogger
      );
      expect(result.content[0].type).toBe('text');
      expect(result.content[0].text).toContain('Error:');
      // Source should still exist since move failed
      expect(existsSync(sourcePath)).toBe(true);
    });
  });
});