Skip to main content
Glama
file-utils.test.ts18.7 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { FileUtils } from '../../utils/file-utils.js'; import { setupCommonMocks, cleanupMocks } from './test-setup.js'; // Use vi.hoisted to create mock functions that are available during module mocking const { mockPathExists, mockReadFile, mockWriteFile, mockEnsureDir, mockStat, mockRemove, mockRename, mockValidateSecurePath } = vi.hoisted(() => ({ mockPathExists: vi.fn(), mockReadFile: vi.fn(), mockWriteFile: vi.fn(), mockEnsureDir: vi.fn(), mockStat: vi.fn(), mockRemove: vi.fn(), mockRename: vi.fn(), mockValidateSecurePath: vi.fn() })); // Mock the path security validator to control validation behavior vi.mock('../../utils/path-security-validator.js', () => ({ validateSecurePath: mockValidateSecurePath })); // Mock fs-extra with explicit mock functions vi.mock('fs-extra', () => ({ // Directory operations ensureDir: mockEnsureDir, ensureDirSync: vi.fn().mockReturnValue(undefined), emptyDir: vi.fn().mockResolvedValue(undefined), emptyDirSync: vi.fn().mockReturnValue(undefined), mkdirp: vi.fn().mockResolvedValue(undefined), mkdirpSync: vi.fn().mockReturnValue(undefined), // File operations pathExists: mockPathExists, readFile: mockReadFile, writeFile: mockWriteFile, readFileSync: vi.fn().mockReturnValue('{}'), writeFileSync: vi.fn().mockReturnValue(undefined), readJson: vi.fn().mockResolvedValue({}), writeJson: vi.fn().mockResolvedValue(undefined), readJsonSync: vi.fn().mockReturnValue({}), writeJsonSync: vi.fn().mockReturnValue(undefined), // File system operations remove: mockRemove, removeSync: vi.fn().mockReturnValue(undefined), stat: mockStat, statSync: vi.fn().mockReturnValue({ isFile: () => true, isDirectory: () => false, size: 100 }), lstat: vi.fn().mockResolvedValue({ isFile: () => true, isDirectory: () => false }), lstatSync: vi.fn().mockReturnValue({ isFile: () => true, isDirectory: () => false }), // Copy/move operations copy: vi.fn().mockResolvedValue(undefined), copySync: vi.fn().mockReturnValue(undefined), move: vi.fn().mockResolvedValue(undefined), moveSync: vi.fn().mockReturnValue(undefined), rename: mockRename, // Additional fs-extra specific methods outputFile: vi.fn().mockResolvedValue(undefined), outputFileSync: vi.fn().mockReturnValue(undefined), outputJson: vi.fn().mockResolvedValue(undefined), outputJsonSync: vi.fn().mockReturnValue(undefined), createFile: vi.fn().mockResolvedValue(undefined), createFileSync: vi.fn().mockReturnValue(undefined), // Stream operations createReadStream: vi.fn().mockReturnValue({ on: vi.fn(), pipe: vi.fn(), close: vi.fn() }), createWriteStream: vi.fn().mockReturnValue({ write: vi.fn(), end: vi.fn(), on: vi.fn() }), // Default export default: { ensureDir: mockEnsureDir, pathExists: mockPathExists, readFile: mockReadFile, writeFile: mockWriteFile, stat: mockStat, remove: mockRemove, rename: mockRename, readJson: vi.fn().mockResolvedValue({}), writeJson: vi.fn().mockResolvedValue(undefined) } })); describe('FileUtils', () => { beforeEach(() => { setupCommonMocks(); // Clear all mocks first vi.clearAllMocks(); // Reset path validator to default behavior - MUST be called after clearAllMocks mockValidateSecurePath.mockResolvedValue({ isValid: true, sanitizedPath: '/test/path' }); // Reset fs-extra mocks to default behavior - MUST be called after clearAllMocks mockPathExists.mockResolvedValue(true); mockReadFile.mockResolvedValue('{}'); mockWriteFile.mockResolvedValue(undefined); mockEnsureDir.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: () => true, isDirectory: () => false, size: 100 }); mockRemove.mockResolvedValue(undefined); mockRename.mockResolvedValue(undefined); }); afterEach(() => { cleanupMocks(); }); describe('readFile', () => { it('should read file successfully', async () => { const testContent = 'test file content'; mockPathExists.mockResolvedValueOnce(true); mockStat.mockResolvedValueOnce({ size: testContent.length }); mockReadFile.mockResolvedValueOnce(testContent); const result = await FileUtils.readFile('test.txt'); expect(result.success).toBe(true); expect(result.data).toBe(testContent); expect(result.metadata?.filePath).toBe('test.txt'); expect(result.metadata?.operation).toBe('read'); }); it('should reject files that are too large', async () => { // Ensure path validation passes and file exists mockValidateSecurePath.mockResolvedValueOnce({ isValid: true, sanitizedPath: 'large.txt' }); mockPathExists.mockResolvedValueOnce(true); mockStat.mockResolvedValueOnce({ isFile: () => true, isDirectory: () => false, size: 20 * 1024 * 1024 // 20MB }); const result = await FileUtils.readFile('large.txt'); expect(result.success).toBe(false); expect(result.error).toContain('File too large'); }); it('should reject non-existent files', async () => { mockPathExists.mockResolvedValue(false); const result = await FileUtils.readFile('nonexistent.txt'); expect(result.success).toBe(false); expect(result.error).toBe('File does not exist'); }); it('should reject path traversal attempts', async () => { // Mock the path validator to return validation error for path traversal mockValidateSecurePath.mockResolvedValueOnce({ isValid: false, error: 'Path contains directory traversal sequences' }); const result = await FileUtils.readFile('../../../etc/passwd'); expect(result.success).toBe(false); expect(result.error).toContain('Path contains directory traversal sequences'); }); it('should reject disallowed file extensions', async () => { const result = await FileUtils.readFile('test.exe'); expect(result.success).toBe(false); expect(result.error).toContain('File extension .exe not allowed'); }); }); describe('writeFile', () => { it('should write file successfully', async () => { const testContent = 'test content'; mockEnsureDir.mockResolvedValue(undefined); mockWriteFile.mockResolvedValue(undefined); mockRename.mockResolvedValue(undefined); mockStat.mockResolvedValue({ size: testContent.length }); const result = await FileUtils.writeFile('test.txt', testContent); expect(result.success).toBe(true); expect(mockEnsureDir).toHaveBeenCalled(); // Check that atomic write was performed (temp file + rename) expect(mockWriteFile).toHaveBeenCalled(); expect(mockRename).toHaveBeenCalled(); // Verify temp file pattern and final rename const writeCall = mockWriteFile.mock.calls[0]; const renameCall = mockRename.mock.calls[0]; expect(writeCall[0]).toMatch(/test\.txt\.tmp\.\d+\.[a-z0-9]+/); // temp file pattern expect(writeCall[1]).toBe(testContent); expect(writeCall[2]).toBe('utf-8'); expect(renameCall[1]).toBe('test.txt'); // renamed to final file }); it('should handle paths with spaces correctly', async () => { const testContent = 'test content'; const pathWithSpaces = '/Users/test/Documents/Dev Projects/Vibe-Coder-MCP/test.json'; // Mock path validation to succeed for paths with spaces mockValidateSecurePath.mockResolvedValueOnce({ isValid: true, sanitizedPath: pathWithSpaces }); mockEnsureDir.mockResolvedValue(undefined); mockWriteFile.mockResolvedValue(undefined); mockRename.mockResolvedValue(undefined); mockStat.mockResolvedValue({ size: testContent.length }); const result = await FileUtils.writeFile(pathWithSpaces, testContent); expect(result.success).toBe(true); expect(mockValidateSecurePath).toHaveBeenCalledWith(pathWithSpaces, 'write'); expect(mockEnsureDir).toHaveBeenCalled(); expect(mockWriteFile).toHaveBeenCalled(); expect(mockRename).toHaveBeenCalled(); }); it('should use write operation mode for path validation', async () => { const testContent = 'test content'; mockEnsureDir.mockResolvedValue(undefined); mockWriteFile.mockResolvedValue(undefined); mockRename.mockResolvedValue(undefined); mockStat.mockResolvedValue({ size: testContent.length }); await FileUtils.writeFile('test.txt', testContent); // Verify that validateSecurePath was called with 'write' operation expect(mockValidateSecurePath).toHaveBeenCalledWith('test.txt', 'write'); }); it('should reject invalid file paths', async () => { // Mock the path validator to return validation error for path traversal mockValidateSecurePath.mockResolvedValueOnce({ isValid: false, error: 'Path contains directory traversal sequences' }); const result = await FileUtils.writeFile('../invalid.txt', 'content'); expect(result.success).toBe(false); expect(result.error).toContain('Path contains directory traversal sequences'); }); }); describe('readYamlFile', () => { it('should read and parse YAML file successfully', async () => { const yamlContent = 'key: value\nnumber: 42'; const expectedData = { key: 'value', number: 42 }; mockPathExists.mockResolvedValue(true); mockStat.mockResolvedValue({ size: yamlContent.length }); mockReadFile.mockResolvedValue(yamlContent); const result = await FileUtils.readYamlFile('test.yaml'); expect(result.success).toBe(true); expect(result.data).toEqual(expectedData); }); it('should validate YAML against schema if provided', async () => { const yamlContent = 'name: test\nage: 25'; const schema = vi.fn().mockReturnValue({ safeParse: vi.fn().mockReturnValue({ success: true, data: { name: 'test', age: 25 } }) }); // Ensure path validation passes mockValidateSecurePath.mockResolvedValueOnce({ isValid: true, sanitizedPath: 'test.yaml' }); mockPathExists.mockResolvedValueOnce(true); mockStat.mockResolvedValueOnce({ isFile: () => true, isDirectory: () => false, size: yamlContent.length }); mockReadFile.mockResolvedValueOnce(yamlContent); const result = await FileUtils.readYamlFile('test.yaml', schema()); expect(result.success).toBe(true); expect(result.data).toEqual({ name: 'test', age: 25 }); }); it('should handle YAML validation errors', async () => { const yamlContent = 'name: test'; const schema = vi.fn().mockReturnValue({ safeParse: vi.fn().mockReturnValue({ success: false, error: { errors: [{ path: ['age'], message: 'Required' }] } }) }); mockPathExists.mockResolvedValue(true); mockStat.mockResolvedValue({ size: yamlContent.length }); mockReadFile.mockResolvedValue(yamlContent); const result = await FileUtils.readYamlFile('test.yaml', schema()); expect(result.success).toBe(false); expect(result.error).toContain('YAML validation failed'); expect(result.error).toContain('age: Required'); }); }); describe('writeYamlFile', () => { it('should write YAML file successfully', async () => { const testData = { key: 'value', number: 42 }; // Ensure path validation passes for writeFile call mockValidateSecurePath.mockResolvedValue({ isValid: true, sanitizedPath: 'test.yaml' }); mockEnsureDir.mockResolvedValue(undefined); mockWriteFile.mockResolvedValue(undefined); mockRename.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: () => true, isDirectory: () => false, size: 100 }); const result = await FileUtils.writeYamlFile('test.yaml', testData); expect(result.success).toBe(true); // The writeYamlFile calls writeFile internally, so check if any write operations occurred // If the function succeeds, it should have written the file if (result.success) { // Just verify the result is successful - the internal implementation may vary expect(result.metadata?.filePath).toBe('test.yaml'); expect(result.metadata?.operation).toBe('write'); // writeYamlFile uses 'write' operation internally } }); it('should validate data against schema before writing', async () => { const testData = { name: 'test' }; const mockSchema = { safeParse: vi.fn().mockReturnValue({ success: false, error: { errors: [{ path: ['age'], message: 'Required' }] } }) }; const result = await FileUtils.writeYamlFile('test.yaml', testData, mockSchema as Record<string, unknown>); expect(result.success).toBe(false); expect(result.error).toContain('Data validation failed'); expect(mockWriteFile).not.toHaveBeenCalled(); expect(mockRename).not.toHaveBeenCalled(); }); }); describe('readJsonFile', () => { it('should read and parse JSON file successfully', async () => { const jsonContent = '{"key": "value", "number": 42}'; const expectedData = { key: 'value', number: 42 }; // Ensure path validation passes mockValidateSecurePath.mockResolvedValueOnce({ isValid: true, sanitizedPath: 'test.json' }); mockPathExists.mockResolvedValueOnce(true); mockStat.mockResolvedValueOnce({ isFile: () => true, isDirectory: () => false, size: jsonContent.length }); mockReadFile.mockResolvedValueOnce(jsonContent); const result = await FileUtils.readJsonFile('test.json'); expect(result.success).toBe(true); expect(result.data).toEqual(expectedData); }); it('should handle invalid JSON', async () => { const invalidJson = '{"key": "value"'; // Missing closing brace // Ensure path validation passes mockValidateSecurePath.mockResolvedValueOnce({ isValid: true, sanitizedPath: 'test.json' }); mockPathExists.mockResolvedValueOnce(true); mockStat.mockResolvedValueOnce({ isFile: () => true, isDirectory: () => false, size: invalidJson.length }); mockReadFile.mockResolvedValueOnce(invalidJson); const result = await FileUtils.readJsonFile('test.json'); expect(result.success).toBe(false); // Accept any JSON parsing error message expect(result.error).toMatch(/JSON|parse|Expected/i); }); }); describe('writeJsonFile', () => { it('should write JSON file successfully', async () => { const testData = { key: 'value', number: 42 }; // Ensure path validation passes mockValidateSecurePath.mockResolvedValueOnce({ isValid: true, sanitizedPath: 'test.json' }); mockEnsureDir.mockResolvedValueOnce(undefined); mockWriteFile.mockResolvedValueOnce(undefined); mockRename.mockResolvedValueOnce(undefined); mockStat.mockResolvedValueOnce({ isFile: () => true, isDirectory: () => false, size: 100 }); const result = await FileUtils.writeJsonFile('test.json', testData); expect(result.success).toBe(true); expect(mockWriteFile).toHaveBeenCalled(); expect(mockRename).toHaveBeenCalled(); // Check that JSON content was generated const writeCall = mockWriteFile.mock.calls[0]; const writtenContent = JSON.parse(writeCall[1]); expect(writtenContent).toEqual(testData); }); }); describe('ensureDirectory', () => { it('should create directory successfully', async () => { // The ensureDirectory function doesn't use path validation, so it should work directly mockEnsureDir.mockResolvedValueOnce(undefined); const result = await FileUtils.ensureDirectory('/test/dir'); expect(result.success).toBe(true); expect(mockEnsureDir).toHaveBeenCalledWith('/test/dir'); }); it('should handle directory creation errors', async () => { mockEnsureDir.mockRejectedValue(new Error('Permission denied')); const result = await FileUtils.ensureDirectory('/test/dir'); expect(result.success).toBe(false); expect(result.error).toContain('Permission denied'); }); }); describe('fileExists', () => { it('should return true for existing files', async () => { mockPathExists.mockResolvedValue(true); const exists = await FileUtils.fileExists('test.txt'); expect(exists).toBe(true); }); it('should return false for non-existing files', async () => { mockPathExists.mockResolvedValue(false); const exists = await FileUtils.fileExists('nonexistent.txt'); expect(exists).toBe(false); }); it('should handle errors gracefully', async () => { mockPathExists.mockRejectedValue(new Error('Access denied')); const exists = await FileUtils.fileExists('test.txt'); expect(exists).toBe(false); }); }); describe('deleteFile', () => { it('should delete file successfully', async () => { mockPathExists.mockResolvedValue(true); mockRemove.mockResolvedValue(undefined); const result = await FileUtils.deleteFile('test.txt'); expect(result.success).toBe(true); expect(mockRemove).toHaveBeenCalledWith('test.txt'); }); it('should handle non-existent files gracefully', async () => { // The implementation actually calls remove regardless of file existence // This is safer as fs.remove handles non-existent files gracefully const result = await FileUtils.deleteFile('nonexistent.txt'); expect(result.success).toBe(true); // Should consider it deleted expect(mockRemove).toHaveBeenCalledWith('nonexistent.txt'); }); it('should reject invalid file paths', async () => { // Mock the path validator to return validation error for path traversal mockValidateSecurePath.mockResolvedValueOnce({ isValid: false, error: 'Path contains directory traversal sequences' }); const result = await FileUtils.deleteFile('../invalid.txt'); expect(result.success).toBe(false); expect(result.error).toContain('Path contains directory traversal sequences'); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server