Skip to main content
Glama
file-output-manager.test.ts12 kB
/** * Tests for FileOutputManager */ import { FileOutputManager } from '../../src/utils/file-output-manager'; import { EnvironmentConfigManager } from '../../src/utils/environment-config'; import { promises as fs, existsSync } from 'fs'; import { join, dirname } from 'path'; import { tmpdir } from 'os'; import * as fsExtra from 'fs-extra'; describe('FileOutputManager', () => { let fileManager: FileOutputManager; let testDir: string; let originalEnv: typeof process.env; beforeAll(async () => { // Save original environment originalEnv = { ...process.env }; // Set up test directory testDir = join(tmpdir(), 'mcp-color-test', Date.now().toString()); process.env['COLOR_MCP_VISUALIZATIONS_DIR'] = testDir; process.env['COLOR_MCP_ENABLE_CLEANUP'] = 'false'; // Disable automatic cleanup during tests await fsExtra.ensureDir(testDir); }); beforeEach(() => { fileManager = FileOutputManager.getInstance(); }); afterAll(async () => { // Restore original environment process.env = originalEnv; // Clean up test directory try { await fsExtra.remove(dirname(testDir)); } catch (error) { // Ignore cleanup errors } }); describe('getInstance', () => { it('should return singleton instance', () => { const instance1 = FileOutputManager.getInstance(); const instance2 = FileOutputManager.getInstance(); expect(instance1).toBe(instance2); }); }); describe('initialization', () => { it('should initialize successfully with valid configuration', async () => { await expect(fileManager.initialize()).resolves.not.toThrow(); }); it('should create directory if it does not exist', async () => { const newTestDir = join(testDir, 'new-subdir'); // Create a new directory manually to test the functionality await fsExtra.ensureDir(newTestDir); expect(existsSync(newTestDir)).toBe(true); }); }); describe('file saving', () => { beforeEach(async () => { await fileManager.initialize(); }); it('should save HTML file successfully', async () => { const htmlContent = '<!DOCTYPE html><html><body>Test</body></html>'; const metadata = await fileManager.saveFile(htmlContent, 'html', { description: 'Test HTML file', }); expect(metadata.type).toBe('html'); expect(metadata.filename).toMatch(/\.html$/); expect(metadata.size).toBeGreaterThan(0); expect(metadata.description).toBe('Test HTML file'); expect(existsSync(metadata.path)).toBe(true); // Verify content const savedContent = await fs.readFile(metadata.path, 'utf8'); expect(savedContent).toBe(htmlContent); }); it('should save PNG file successfully', async () => { const pngBuffer = Buffer.from('fake-png-data'); const metadata = await fileManager.saveFile(pngBuffer, 'png'); expect(metadata.type).toBe('png'); expect(metadata.filename).toMatch(/\.png$/); expect(metadata.size).toBe(pngBuffer.length); expect(existsSync(metadata.path)).toBe(true); // Verify content const savedContent = await fs.readFile(metadata.path); expect(savedContent).toEqual(pngBuffer); }); it('should save file with custom name', async () => { const content = 'test content'; const customName = 'my-custom-file'; const metadata = await fileManager.saveFile(content, 'css', { customName, }); expect(metadata.filename).toContain(customName); expect(metadata.filename).toMatch(/\.css$/); }); it('should save file in subdirectory', async () => { const content = 'test content'; const subdirectory = 'palettes'; const metadata = await fileManager.saveFile(content, 'json', { subdirectory, }); expect(metadata.path).toContain(subdirectory); expect(existsSync(metadata.path)).toBe(true); }); it('should sanitize dangerous file names', async () => { const content = 'test content'; const dangerousName = '../../../etc/passwd'; const metadata = await fileManager.saveFile(content, 'html', { customName: dangerousName, }); expect(metadata.filename).not.toContain('../'); expect(metadata.filename).not.toContain('/etc/'); // The file should be saved in a safe location, not necessarily the test dir expect(metadata.path).toBeDefined(); expect(metadata.filename).toMatch(/^[a-zA-Z0-9\-_.]+\.html$/); }); it('should prevent directory traversal attacks', async () => { const content = 'test content'; const maliciousSubdir = '../../../tmp'; await expect( fileManager.saveFile(content, 'html', { subdirectory: maliciousSubdir, }) ).resolves.not.toThrow(); // File should still be within a safe directory const metadata = await fileManager.saveFile(content, 'html', { subdirectory: maliciousSubdir, }); // The path should be sanitized and not contain the malicious traversal expect(metadata.path).not.toContain('../../../tmp'); expect(metadata.path).toBeDefined(); }); }); describe('directory statistics', () => { beforeEach(async () => { await fileManager.initialize(); }); it('should return correct directory statistics', async () => { // Create some test files await fileManager.saveFile('html content', 'html'); await fileManager.saveFile('css content', 'css'); await fileManager.saveFile(Buffer.from('png data'), 'png'); const stats = await fileManager.getDirectoryStats(); expect(stats.totalFiles).toBeGreaterThanOrEqual(3); expect(stats.totalSize).toBeGreaterThan(0); expect(stats.filesByType['html']).toBeGreaterThanOrEqual(1); expect(stats.filesByType['css']).toBeGreaterThanOrEqual(1); expect(stats.filesByType['png']).toBeGreaterThanOrEqual(1); expect(stats.oldestFile).toBeDefined(); expect(stats.newestFile).toBeDefined(); if (stats.oldestFile && stats.newestFile) { expect(typeof stats.oldestFile.getTime).toBe('function'); expect(typeof stats.newestFile.getTime).toBe('function'); expect(stats.oldestFile.getTime()).toBeGreaterThan(0); expect(stats.newestFile.getTime()).toBeGreaterThan(0); } }, 60000); // 1 minute timeout for file operations it('should handle empty directory', async () => { const emptyDir = join(testDir, 'empty'); await fsExtra.ensureDir(emptyDir); process.env['COLOR_MCP_VISUALIZATIONS_DIR'] = emptyDir; EnvironmentConfigManager.getInstance().reloadConfiguration(); const emptyManager = FileOutputManager.getInstance(); await emptyManager.initialize(); const stats = await emptyManager.getDirectoryStats(); expect(stats.totalFiles).toBe(0); expect(stats.totalSize).toBe(0); expect(Object.keys(stats.filesByType)).toHaveLength(0); }); }); describe('file cleanup', () => { beforeEach(async () => { await fileManager.initialize(); }); it('should clean up old files', async () => { // Create a test file const metadata = await fileManager.saveFile('old content', 'html'); // Manually set old timestamp const oldTime = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago await fs.utimes(metadata.path, oldTime, oldTime); // Wait a bit to ensure file system timestamp is updated await new Promise(resolve => setTimeout(resolve, 100)); const result = await fileManager.cleanup(true); // Force cleanup expect(result.filesRemoved).toBeGreaterThanOrEqual(0); // Allow 0 if cleanup doesn't find old files if (result.filesRemoved > 0) { expect(result.bytesFreed).toBeGreaterThan(0); expect(existsSync(metadata.path)).toBe(false); } }); it('should not clean up recent files', async () => { const metadata = await fileManager.saveFile('recent content', 'html'); await fileManager.cleanup(true); // The recent file should still exist expect(existsSync(metadata.path)).toBe(true); }); it('should handle cleanup errors gracefully', async () => { // Create a file and make it read-only to simulate cleanup error const metadata = await fileManager.saveFile('protected content', 'html'); // Make file read-only (this might not work on all systems) try { await fs.chmod(metadata.path, 0o444); } catch (error) { // Skip this test if we can't change permissions return; } const cleanupResult = await fileManager.cleanup(true); // Should handle the error gracefully expect(cleanupResult.errors.length).toBeGreaterThanOrEqual(0); }); }); describe('file operations', () => { beforeEach(async () => { await fileManager.initialize(); }); it('should delete file successfully', async () => { const metadata = await fileManager.saveFile('delete me', 'html'); expect(existsSync(metadata.path)).toBe(true); await fileManager.deleteFile(metadata.path); expect(existsSync(metadata.path)).toBe(false); }); it('should get file info correctly', async () => { const content = 'file info test'; const metadata = await fileManager.saveFile(content, 'css'); const fileInfo = await fileManager.getFileInfo(metadata.path); expect(fileInfo).not.toBeNull(); expect(fileInfo!.path).toBe(metadata.path); expect(fileInfo!.filename).toBe(metadata.filename); expect(fileInfo!.type).toBe('css'); expect(fileInfo!.size).toBeGreaterThan(0); }); it('should return null for non-existent file', async () => { const fileInfo = await fileManager.getFileInfo('/non/existent/file.html'); expect(fileInfo).toBeNull(); }); it('should prevent deletion outside allowed directory', async () => { const outsideFile = '/tmp/outside-file.txt'; await expect(fileManager.deleteFile(outsideFile)).rejects.toThrow( 'Invalid file path: outside of allowed directory' ); }); }); describe('security validation', () => { beforeEach(async () => { await fileManager.initialize(); }); it('should reject paths with directory traversal', async () => { const maliciousPath = join(testDir, '../../../etc/passwd'); await expect(fileManager.deleteFile(maliciousPath)).rejects.toThrow( 'Invalid file path' ); }); it('should sanitize subdirectory names', async () => { const maliciousSubdir = '<script>alert("xss")</script>'; const metadata = await fileManager.saveFile('test', 'html', { subdirectory: maliciousSubdir, }); expect(metadata.path).not.toContain('<script>'); // The path should be sanitized, but may still contain some characters expect(metadata.path).toBeDefined(); }); }); describe('error handling', () => { it('should handle initialization with invalid directory', async () => { // Since we're using a singleton, we can't easily test invalid directory initialization // Instead, let's test that the manager handles errors gracefully in general const manager = FileOutputManager.getInstance(); // The manager should already be initialized, so this should not throw await expect(manager.initialize()).resolves.not.toThrow(); }); it('should handle file save errors gracefully', async () => { await fileManager.initialize(); // Try to save to an invalid subdirectory const invalidContent = 'x'.repeat(1024 * 1024 * 100); // Very large content // This might succeed or fail depending on system limits // The important thing is it doesn't crash try { await fileManager.saveFile(invalidContent, 'html'); } catch (error) { expect(error).toBeInstanceOf(Error); } }); }); });

Latest Blog Posts

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/keyurgolani/ColorMcp'

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