Skip to main content
Glama

Cut-Copy-Paste Clipboard Server

file-handler.test.ts•16.4 kB
import { FileHandler } from '../file-handler.js'; import { PathAccessControl } from '../path-access-control.js'; import { existsSync, rmSync, writeFileSync, mkdirSync, chmodSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; describe('FileHandler', () => { let testDir: string; let fileHandler: FileHandler; beforeEach(() => { testDir = join(tmpdir(), `test-files-${Date.now()}`); mkdirSync(testDir, { recursive: true }); fileHandler = new FileHandler(); }); afterEach(() => { if (existsSync(testDir)) { // Reset permissions before deleting try { chmodSync(testDir, 0o755); } catch {} rmSync(testDir, { recursive: true, force: true }); } }); describe('Read Lines - RED', () => { it('should read specific line range from file', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\nline 4\nline 5\n'); const result = fileHandler.readLines(filePath, 2, 4); expect(result.content).toBe('line 2\nline 3\nline 4'); expect(result.lines).toEqual(['line 2', 'line 3', 'line 4']); }); it('should handle 1-indexed line numbers correctly', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'first\nsecond\nthird\n'); const result = fileHandler.readLines(filePath, 1, 1); expect(result.content).toBe('first'); expect(result.lines).toEqual(['first']); }); it('should throw error for invalid file path', () => { expect(() => { fileHandler.readLines('/non/existent/file.txt', 1, 1); }).toThrow('File not found'); }); it('should throw error for invalid line range (start > end)', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.readLines(filePath, 5, 2); }).toThrow('Invalid line range'); }); it('should throw error for negative line numbers', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.readLines(filePath, -1, 2); }).toThrow('Line numbers must be positive'); }); it('should throw error for non-integer line numbers', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.readLines(filePath, 1.5, 2); }).toThrow('Line numbers must be integers'); }); it('should throw error for zero line numbers', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.readLines(filePath, 0, 2); }).toThrow('Line numbers must be positive'); }); it('should enforce file size limit before reading content', () => { const filePath = join(testDir, 'large.txt'); const oversized = Buffer.alloc(10 * 1024 * 1024 + 1, 'a'); writeFileSync(filePath, oversized); expect(() => { fileHandler.readLines(filePath, 1, 1); }).toThrow('File size exceeds'); }); it('should throw error for line numbers beyond file length', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.readLines(filePath, 1, 10); }).toThrow('Line range exceeds file length'); }); it('should handle empty files', () => { const filePath = join(testDir, 'empty.txt'); writeFileSync(filePath, ''); expect(() => { fileHandler.readLines(filePath, 1, 1); }).toThrow('File is empty or line range exceeds file length'); }); it('should preserve line ending information', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\r\nline 2\r\nline 3\r\n'); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lineEnding).toBe('\r\n'); }); it('should detect Unix line endings', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lineEnding).toBe('\n'); }); it('should reject binary files', () => { const filePath = join(testDir, 'binary.bin'); // Write some binary data (null bytes) writeFileSync(filePath, Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe])); expect(() => { fileHandler.readLines(filePath, 1, 1); }).toThrow('Binary files are not supported'); }); it('should handle files without trailing newline', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3'); const result = fileHandler.readLines(filePath, 2, 3); expect(result.lines).toEqual(['line 2', 'line 3']); }); }); describe('Insert Lines - RED', () => { it('should insert lines at specified position', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); fileHandler.insertLines(filePath, 2, ['inserted 1', 'inserted 2']); const content = fileHandler.readLines(filePath, 1, 5); expect(content.lines).toEqual(['line 1', 'inserted 1', 'inserted 2', 'line 2', 'line 3']); }); it('should handle inserting at start of file', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); fileHandler.insertLines(filePath, 1, ['new first']); const content = fileHandler.readLines(filePath, 1, 3); expect(content.lines).toEqual(['new first', 'line 1', 'line 2']); }); it('should handle inserting at end of file', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); fileHandler.insertLines(filePath, 3, ['line 3', 'line 4']); const content = fileHandler.readLines(filePath, 1, 4); expect(content.lines).toEqual(['line 1', 'line 2', 'line 3', 'line 4']); }); it('should preserve line endings when inserting', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\r\nline 2\r\n'); fileHandler.insertLines(filePath, 2, ['inserted']); const result = fileHandler.readLines(filePath, 1, 3); expect(result.content).toContain('\r\n'); }); it('should throw error for read-only files', () => { const filePath = join(testDir, 'readonly.txt'); writeFileSync(filePath, 'line 1\n'); chmodSync(filePath, 0o444); // Read-only expect(() => { fileHandler.insertLines(filePath, 1, ['new line']); }).toThrow(/Permission denied|EACCES/); }); it('should throw error for invalid insert position', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.insertLines(filePath, 10, ['new line']); }).toThrow('Insert position exceeds file length'); }); it('should require integer insert positions', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.insertLines(filePath, 1.2, ['new line']); }).toThrow('Insert position must be an integer'); }); it('should handle empty line array', () => { const filePath = join(testDir, 'test.txt'); const originalContent = 'line 1\nline 2\n'; writeFileSync(filePath, originalContent); fileHandler.insertLines(filePath, 2, []); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lines).toEqual(['line 1', 'line 2']); }); }); describe('Delete Lines - RED', () => { it('should delete specified line range', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\nline 4\nline 5\n'); fileHandler.deleteLines(filePath, 2, 4); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lines).toEqual(['line 1', 'line 5']); }); it('should handle deleting first lines', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); fileHandler.deleteLines(filePath, 1, 1); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lines).toEqual(['line 2', 'line 3']); }); it('should handle deleting last lines', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); fileHandler.deleteLines(filePath, 3, 3); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lines).toEqual(['line 1', 'line 2']); }); it('should handle deleting all content', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); fileHandler.deleteLines(filePath, 1, 3); expect(() => { fileHandler.readLines(filePath, 1, 1); }).toThrow('File is empty'); }); it('should preserve line endings after deletion', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\r\nline 2\r\nline 3\r\n'); fileHandler.deleteLines(filePath, 2, 2); const result = fileHandler.readLines(filePath, 1, 2); expect(result.lineEnding).toBe('\r\n'); }); it('should throw error for invalid delete range', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { fileHandler.deleteLines(filePath, 1, 10); }).toThrow('Line range exceeds file length'); }); it('should require integer delete positions', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); expect(() => { fileHandler.deleteLines(filePath, 1, 2.5); }).toThrow('Line numbers must be integers'); }); }); describe('Get File Snapshot - RED', () => { it('should capture entire file content', () => { const filePath = join(testDir, 'test.txt'); const content = 'line 1\nline 2\nline 3\n'; writeFileSync(filePath, content); const snapshot = fileHandler.getFileSnapshot(filePath); expect(snapshot.content).toBe(content); }); it('should include file metadata', () => { const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'line 1\nline 2\nline 3\n'); const snapshot = fileHandler.getFileSnapshot(filePath); expect(snapshot.lineCount).toBe(3); expect(snapshot.filePath).toBe(filePath); expect(snapshot.timestamp).toBeGreaterThan(0); }); it('should handle empty files', () => { const filePath = join(testDir, 'empty.txt'); writeFileSync(filePath, ''); const snapshot = fileHandler.getFileSnapshot(filePath); expect(snapshot.content).toBe(''); expect(snapshot.lineCount).toBe(0); }); it('should throw error for non-existent files', () => { expect(() => { fileHandler.getFileSnapshot('/non/existent/file.txt'); }).toThrow('File not found'); }); }); describe('Is Text File - RED', () => { it('should identify text files correctly', () => { const filePath = join(testDir, 'text.txt'); writeFileSync(filePath, 'This is plain text\n'); expect(fileHandler.isTextFile(filePath)).toBe(true); }); it('should identify binary files correctly', () => { const filePath = join(testDir, 'binary.bin'); writeFileSync(filePath, Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe])); expect(fileHandler.isTextFile(filePath)).toBe(false); }); it('should handle common text file extensions', () => { const extensions = ['.txt', '.js', '.ts', '.py', '.java', '.md', '.json', '.yml']; extensions.forEach((ext) => { const filePath = join(testDir, `test${ext}`); writeFileSync(filePath, 'text content'); expect(fileHandler.isTextFile(filePath)).toBe(true); }); }); }); describe('Path Access Control', () => { let allowFilePath: string; let restrictedHandler: FileHandler; beforeEach(() => { allowFilePath = join(testDir, 'paths.allow'); }); it('should allow all paths when no access control is provided', () => { const handler = new FileHandler(); const filePath = join(testDir, 'test.txt'); writeFileSync(filePath, 'content'); expect(() => handler.readLines(filePath, 1, 1)).not.toThrow(); }); it('should block read access to paths not in allowlist', () => { // Create a restricted allowlist writeFileSync(allowFilePath, '/allowed/path/**\n'); const pathControl = new PathAccessControl(allowFilePath); restrictedHandler = new FileHandler(pathControl); const filePath = join(testDir, 'blocked.txt'); writeFileSync(filePath, 'content'); expect(() => { restrictedHandler.readLines(filePath, 1, 1); }).toThrow('Access denied: path not in allowlist'); }); it('should allow read access to paths in allowlist', () => { // Allow testDir writeFileSync(allowFilePath, `${testDir}/**\n`); const pathControl = new PathAccessControl(allowFilePath); restrictedHandler = new FileHandler(pathControl); const filePath = join(testDir, 'allowed.txt'); writeFileSync(filePath, 'line 1\nline 2'); const result = restrictedHandler.readLines(filePath, 1, 2); expect(result.lines).toEqual(['line 1', 'line 2']); }); it('should block write operations to paths not in allowlist', () => { writeFileSync(allowFilePath, '/allowed/path/**\n'); const pathControl = new PathAccessControl(allowFilePath); restrictedHandler = new FileHandler(pathControl); const filePath = join(testDir, 'blocked.txt'); writeFileSync(filePath, 'content'); expect(() => { restrictedHandler.insertLines(filePath, 1, ['new line']); }).toThrow('Access denied: path not in allowlist'); expect(() => { restrictedHandler.deleteLines(filePath, 1, 1); }).toThrow('Access denied: path not in allowlist'); }); it('should allow write operations to paths in allowlist', () => { writeFileSync(allowFilePath, `${testDir}/**\n`); const pathControl = new PathAccessControl(allowFilePath); restrictedHandler = new FileHandler(pathControl); const filePath = join(testDir, 'allowed.txt'); writeFileSync(filePath, 'line 1\nline 2\n'); expect(() => { restrictedHandler.insertLines(filePath, 1, ['new line']); }).not.toThrow(); expect(() => { restrictedHandler.deleteLines(filePath, 1, 1); }).not.toThrow(); }); it('should respect negation patterns in allowlist', () => { writeFileSync(allowFilePath, `${testDir}/**\n!${testDir}/blocked/**\n`); const pathControl = new PathAccessControl(allowFilePath); restrictedHandler = new FileHandler(pathControl); // Create blocked subdirectory const blockedDir = join(testDir, 'blocked'); mkdirSync(blockedDir, { recursive: true }); const allowedFile = join(testDir, 'allowed.txt'); const blockedFile = join(blockedDir, 'blocked.txt'); writeFileSync(allowedFile, 'content'); writeFileSync(blockedFile, 'content'); expect(() => { restrictedHandler.readLines(allowedFile, 1, 1); }).not.toThrow(); expect(() => { restrictedHandler.readLines(blockedFile, 1, 1); }).toThrow('Access denied: path not in allowlist'); }); it('should block getFileSnapshot for paths not in allowlist', () => { writeFileSync(allowFilePath, '/allowed/path/**\n'); const pathControl = new PathAccessControl(allowFilePath); restrictedHandler = new FileHandler(pathControl); const filePath = join(testDir, 'blocked.txt'); writeFileSync(filePath, 'content'); expect(() => { restrictedHandler.getFileSnapshot(filePath); }).toThrow('Access denied: path not in allowlist'); }); }); });

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/Pr0j3c7t0dd-Ltd/cut-copy-paste-mcp'

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