Skip to main content
Glama
server.test.js15.7 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import http from 'http'; import path from 'path'; // Mock os module first vi.mock('os', () => ({ default: { homedir: () => '/mock/home', tmpdir: () => '/mock/tmp' } })); // Mock fs module const mockFs = { readFile: vi.fn(), writeFile: vi.fn(), mkdir: vi.fn() }; vi.mock('fs', () => ({ promises: mockFs })); // Import server after mocks are set up const { startServer } = await import('../server.js'); describe('Server', () => { let server; const mockSettingsFile = path.join('/mock/home', '.shrimp-task-viewer-settings.json'); const mockTempDir = '/mock/tmp/shrimp-task-viewer'; beforeEach(() => { // Reset all mocks vi.clearAllMocks(); }); afterEach(async () => { if (server) { await new Promise((resolve) => { server.close(resolve); }); server = null; } }); describe('Settings Management', () => { it('should load settings from file if exists', async () => { const mockAgents = [ { id: 'agent1', name: 'Agent 1', path: '/path/to/agent1.json' }, { id: 'agent2', name: 'Agent 2', path: '/path/to/agent2.json' } ]; mockFs.readFile.mockResolvedValue(JSON.stringify({ agents: mockAgents, lastUpdated: '2024-01-01T00:00:00.000Z', version: '2.0.0' })); // Start server to trigger loadSettings server = await startServer(); expect(mockFs.readFile).toHaveBeenCalledWith(mockSettingsFile, 'utf8'); }); it('should create default settings if file does not exist', async () => { const error = new Error('ENOENT: no such file or directory'); error.code = 'ENOENT'; mockFs.readFile.mockRejectedValue(error); mockFs.writeFile.mockResolvedValue(); server = await startServer(); expect(mockFs.writeFile).toHaveBeenCalledWith( mockSettingsFile, expect.stringContaining('"agents": []') ); }); it('should save settings with proper format', async () => { const mockAgents = [ { id: 'test-agent', name: 'Test Agent', path: '/test/path.json' } ]; mockFs.readFile.mockResolvedValue(JSON.stringify({ agents: [] })); mockFs.writeFile.mockResolvedValue(); mockFs.mkdir.mockResolvedValue(); server = await startServer(); // Make request to add profile to trigger saveSettings const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'name=Test+Agent&taskFile=' + encodeURIComponent('{"tasks":[]}') }); expect(mockFs.writeFile).toHaveBeenCalledWith( mockSettingsFile, expect.stringMatching(/"agents":\s*\[[\s\S]*\]/) ); }); }); describe('API Endpoints', () => { beforeEach(async () => { mockFs.readFile.mockImplementation((filePath) => { if (filePath === mockSettingsFile) { return Promise.resolve(JSON.stringify({ agents: [ { id: 'agent1', name: 'Agent 1', path: '/path/to/agent1.json' }, { id: 'agent2', name: 'Agent 2', path: '/path/to/agent2.json' } ] })); } else if (filePath === '/path/to/agent1.json') { return Promise.resolve(JSON.stringify({ tasks: [ { id: '1', name: 'Task 1', status: 'pending' }, { id: '2', name: 'Task 2', status: 'completed' } ] })); } return Promise.reject(new Error('File not found')); }); mockFs.writeFile.mockResolvedValue(); mockFs.mkdir.mockResolvedValue(); server = await startServer(); }); describe('GET /api/agents', () => { it('should return list of agents', async () => { const response = await makeRequest(server, '/api/agents'); const data = JSON.parse(response.body); expect(response.statusCode).toBe(200); expect(data).toHaveLength(2); expect(data[0]).toEqual({ id: 'agent1', name: 'Agent 1', path: '/path/to/agent1.json' }); }); it('should include CORS headers', async () => { const response = await makeRequest(server, '/api/agents'); expect(response.headers['access-control-allow-origin']).toBe('*'); expect(response.headers['access-control-allow-methods']).toContain('GET'); }); }); describe('POST /api/add-profile', () => { it('should add new profile with URL-encoded data', async () => { const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'name=New+Agent&taskFile=' + encodeURIComponent('{"tasks":[]}') }); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(data).toMatchObject({ id: 'new-agent', name: 'New Agent', path: expect.stringContaining('tasks.json') }); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringContaining('tasks.json'), '{"tasks":[]}' ); }); it('should handle multipart form data', async () => { const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'; const body = [ `------${boundary}`, 'Content-Disposition: form-data; name="name"', '', 'Multipart Agent', `------${boundary}`, 'Content-Disposition: form-data; name="taskFile"', '', '{"tasks":[{"id":"1","name":"Test"}]}', `------${boundary}--` ].join('\r\n'); const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=----${boundary}` }, body }); expect(response.statusCode).toBe(200); const data = JSON.parse(response.body); expect(data.name).toBe('Multipart Agent'); }); it('should return 400 if name is missing', async () => { const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'taskFile=' + encodeURIComponent('{"tasks":[]}') }); expect(response.statusCode).toBe(400); expect(response.body).toBe('Missing name or taskFile'); }); it('should return 400 if taskFile is missing', async () => { const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'name=Test+Agent' }); expect(response.statusCode).toBe(400); expect(response.body).toBe('Missing name or taskFile'); }); it('should sanitize agent ID from name', async () => { const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'name=Test+Agent+123!@%23&taskFile=' + encodeURIComponent('{"tasks":[]}') }); const data = JSON.parse(response.body); expect(data.id).toBe('test-agent-123'); }); it('should update existing agent if ID matches', async () => { // First add an agent await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'name=Agent+1&taskFile=' + encodeURIComponent('{"tasks":[]}') }); // Add again with same name (same ID) const response = await makeRequest(server, '/api/add-profile', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'name=Agent+1&taskFile=' + encodeURIComponent('{"tasks":[{"id":"new"}]}') }); expect(response.statusCode).toBe(200); // Should have updated, not added new expect(mockFs.writeFile).toHaveBeenCalledWith( mockSettingsFile, expect.stringContaining('"agents": [') ); }); }); describe('DELETE /api/remove-profile/:id', () => { it('should remove profile by ID', async () => { const response = await makeRequest(server, '/api/remove-profile/agent1', { method: 'DELETE' }); expect(response.statusCode).toBe(200); expect(response.body).toBe('Profile removed'); expect(mockFs.writeFile).toHaveBeenCalledWith( mockSettingsFile, expect.not.stringContaining('agent1') ); }); it('should handle non-existent profile gracefully', async () => { const response = await makeRequest(server, '/api/remove-profile/non-existent', { method: 'DELETE' }); expect(response.statusCode).toBe(200); expect(response.body).toBe('Profile removed'); }); }); describe('GET /api/tasks/:agentId', () => { it('should return tasks for valid agent', async () => { const response = await makeRequest(server, '/api/tasks/agent1'); const data = JSON.parse(response.body); expect(response.statusCode).toBe(200); expect(data.tasks).toHaveLength(2); expect(data.tasks[0]).toEqual({ id: '1', name: 'Task 1', status: 'pending' }); }); it('should return 404 for non-existent agent', async () => { const response = await makeRequest(server, '/api/tasks/non-existent'); expect(response.statusCode).toBe(404); expect(response.body).toBe('Profile not found'); }); it('should include cache control headers', async () => { const response = await makeRequest(server, '/api/tasks/agent1'); expect(response.headers['cache-control']).toBe('no-store, no-cache, must-revalidate'); expect(response.headers['pragma']).toBe('no-cache'); expect(response.headers['expires']).toBe('0'); }); it('should handle file read errors', async () => { mockFs.readFile.mockImplementation((filePath) => { if (filePath === mockSettingsFile) { return Promise.resolve(JSON.stringify({ agents: [{ id: 'agent-error', name: 'Error Agent', path: '/error/path.json' }] })); } return Promise.reject(new Error('File read error')); }); const response = await makeRequest(server, '/api/tasks/agent-error'); expect(response.statusCode).toBe(500); expect(response.body).toContain('Error reading task file'); }); }); describe('OPTIONS requests', () => { it('should handle preflight requests', async () => { const response = await makeRequest(server, '/api/agents', { method: 'OPTIONS' }); expect(response.statusCode).toBe(200); expect(response.headers['access-control-allow-origin']).toBe('*'); expect(response.headers['access-control-allow-methods']).toContain('GET'); expect(response.headers['access-control-allow-methods']).toContain('POST'); expect(response.headers['access-control-allow-methods']).toContain('DELETE'); }); }); }); describe('Static File Serving', () => { beforeEach(async () => { mockFs.readFile.mockImplementation((filePath) => { if (filePath === mockSettingsFile) { return Promise.resolve(JSON.stringify({ agents: [] })); } else if (filePath.endsWith('index.html')) { return Promise.resolve('<html><body>React App</body></html>'); } else if (filePath.endsWith('.js')) { return Promise.resolve('console.log("JS file");'); } else if (filePath.endsWith('.css')) { return Promise.resolve('body { margin: 0; }'); } const error = new Error('File not found'); error.code = 'ENOENT'; return Promise.reject(error); }); server = await startServer(); }); it('should serve index.html for root path', async () => { const response = await makeRequest(server, '/'); expect(response.statusCode).toBe(200); expect(response.headers['content-type']).toBe('text/html'); expect(response.body).toContain('React App'); }); it('should serve static files with correct MIME types', async () => { const testCases = [ { path: '/app.js', mimeType: 'application/javascript' }, { path: '/styles.css', mimeType: 'text/css' }, { path: '/index.html', mimeType: 'text/html' } ]; for (const { path, mimeType } of testCases) { const response = await makeRequest(server, path); expect(response.headers['content-type']).toBe(mimeType); } }); it('should set cache control headers for static assets', async () => { const response = await makeRequest(server, '/app.js'); expect(response.headers['cache-control']).toBe('public, max-age=31536000'); }); it('should serve index.html for SPA routes', async () => { const response = await makeRequest(server, '/some/spa/route'); expect(response.statusCode).toBe(200); expect(response.headers['content-type']).toBe('text/html'); expect(response.body).toContain('React App'); }); it('should return 404 for non-existent files with extensions', async () => { const response = await makeRequest(server, '/non-existent.js'); expect(response.statusCode).toBe(404); expect(response.body).toBe('File not found'); }); it('should handle missing dist directory', async () => { mockFs.readFile.mockRejectedValue(new Error('ENOENT')); const response = await makeRequest(server, '/'); expect(response.statusCode).toBe(404); expect(response.body).toBe('React app not built. Run: npm run build'); }); }); describe('Error Handling', () => { it('should handle server startup errors', async () => { mockFs.readFile.mockRejectedValue(new Error('Permission denied')); mockFs.writeFile.mockRejectedValue(new Error('Permission denied')); await expect(startServer()).rejects.toThrow(); }); it('should handle malformed JSON in settings file', async () => { mockFs.readFile.mockResolvedValue('{ invalid json }'); mockFs.writeFile.mockResolvedValue(); server = await startServer(); // Should create new settings file expect(mockFs.writeFile).toHaveBeenCalledWith( mockSettingsFile, expect.stringContaining('"agents": []') ); }); }); }); // Helper function to make HTTP requests to the test server function makeRequest(server, path, options = {}) { return new Promise((resolve) => { const port = server.address().port; const reqOptions = { hostname: '127.0.0.1', port, path, method: options.method || 'GET', headers: options.headers || {} }; const req = http.request(reqOptions, (res) => { let body = ''; res.on('data', (chunk) => body += chunk.toString()); res.on('end', () => { resolve({ statusCode: res.statusCode, headers: res.headers, body }); }); }); if (options.body) { req.write(options.body); } req.end(); }); }

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/cjo4m06/mcp-shrimp-task-manager'

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