Skip to main content
Glama
database-tools-handler.test.js22.8 kB
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; import { mockData, createMockConnectionManager, createMockPerformanceMonitor, createMockRequest, _expectToolSuccess, _expectToolError, cleanupMocks } from './fixtures/modern-fixtures.js'; describe('DatabaseToolsHandler', () => { let handler; let mockConnectionManager; let mockPerformanceMonitor; let mockRequest; beforeEach(async () => { mockRequest = createMockRequest(); const mockPool = { request: vi.fn(() => mockRequest), connected: true, close: vi.fn() }; mockConnectionManager = createMockConnectionManager(mockPool); mockPerformanceMonitor = createMockPerformanceMonitor(); // Import and create handler const { DatabaseToolsHandler } = await import('../../lib/tools/handlers/database-tools.js'); handler = new DatabaseToolsHandler(mockConnectionManager, mockPerformanceMonitor); // Mock the StreamingHandler methods for testing if (handler.streamingHandler) { handler.streamingHandler.streamTableExport = vi.fn(); handler.streamingHandler.reconstructFromChunks = vi.fn(); // Ensure getStreamingStats always returns a valid object with required properties handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: false, memoryEfficient: false, totalRows: 0 }); } }); afterEach(() => { cleanupMocks(); }); describe('listDatabases', () => { test('should list databases successfully', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.databases }); const result = await handler.listDatabases(); // Verify the query was called correctly expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('SELECT')); expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('sys.databases')); expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('WHERE name NOT IN')); // Verify response structure expect(result).toEqual([ { type: 'text', text: expect.stringContaining('database_name') } ]); expect(result[0].text).toContain('TestDB1'); expect(result[0].text).toContain('TestDB2'); }); test('should handle database query errors', async () => { const error = new Error('Database connection failed'); mockRequest.query.mockRejectedValue(error); await expect(handler.listDatabases()).rejects.toThrow('Database connection failed'); }); test('should record performance metrics', async () => { mockRequest.query.mockResolvedValue({ recordset: [] }); await handler.listDatabases(); expect(mockPerformanceMonitor.recordQuery).toHaveBeenCalledWith( expect.objectContaining({ tool: 'list_databases', executionTime: expect.any(Number), success: true, query: expect.any(String), timestamp: expect.any(Date) }) ); }); }); describe('listTables', () => { test('should list tables with default schema', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tables }); const result = await handler.listTables(); // Verify query structure expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('INFORMATION_SCHEMA.TABLES') ); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining("WHERE t.TABLE_SCHEMA = 'dbo'") ); // Verify response expect(result).toEqual([ { type: 'text', text: expect.stringContaining('schema_name') } ]); expect(result[0].text).toContain('Users'); expect(result[0].text).toContain('Orders'); }); test('should list tables with specific database', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tables }); const result = await handler.listTables('TestDB', 'custom'); // Verify database-specific query expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('[TestDB].INFORMATION_SCHEMA.TABLES') ); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining("WHERE t.TABLE_SCHEMA = 'custom'") ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('schema_name') } ]); }); test('should handle empty results', async () => { mockRequest.query.mockResolvedValue({ recordset: [] }); const result = await handler.listTables(); expect(result).toEqual([ { type: 'text', text: 'No data returned' } ]); }); }); describe('describeTable', () => { test('should describe table schema', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tableSchema }); const result = await handler.describeTable('Users'); // Verify query includes necessary joins for primary key info expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('INFORMATION_SCHEMA.COLUMNS') ); expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('LEFT JOIN')); expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('PRIMARY KEY')); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining("WHERE c.TABLE_NAME = 'Users'") ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('column_name') } ]); expect(result[0].text).toContain('id'); expect(result[0].text).toContain('name'); }); test('should describe table in specific database', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tableSchema }); const result = await handler.describeTable('Users', 'TestDB', 'custom'); // Verify database-specific query expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('[TestDB].INFORMATION_SCHEMA.COLUMNS') ); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining("c.TABLE_SCHEMA = 'custom'") ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('column_name') } ]); }); test('should handle non-existent tables', async () => { mockRequest.query.mockResolvedValue({ recordset: [] }); const result = await handler.describeTable('NonExistentTable'); expect(result).toEqual([ { type: 'text', text: 'No data returned' } ]); }); }); describe('listForeignKeys', () => { test('should list foreign keys with default schema', async () => { const mockForeignKeys = [ { foreign_key_name: 'FK_Orders_Users', parent_table: 'Orders', parent_column: 'user_id', referenced_table: 'Users', referenced_column: 'id' } ]; mockRequest.query.mockResolvedValue({ recordset: mockForeignKeys }); const result = await handler.listForeignKeys(); // Verify sys.foreign_keys query expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('sys.foreign_keys')); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining("WHERE s.name = 'dbo'") ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('foreign_key_name') } ]); expect(result[0].text).toContain('FK_Orders_Users'); expect(result[0].text).toContain('Orders'); }); test('should list foreign keys in specific database', async () => { mockRequest.query.mockResolvedValue({ recordset: [] }); const result = await handler.listForeignKeys('TestDB', 'custom'); // Verify database-specific query expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('[TestDB].sys.foreign_keys') ); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining("WHERE s.name = 'custom'") ); expect(result).toEqual([ { type: 'text', text: 'No data returned' } ]); }); }); describe('getTableData', () => { test('should get table data with default pagination', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tableData }); const result = await handler.getTableData('Users'); // Verify pagination query expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('SELECT *')); expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('[dbo].[Users]')); expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('OFFSET 0 ROWS')); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('FETCH NEXT 100 ROWS ONLY') ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('id') } ]); expect(result[0].text).toContain('John Doe'); expect(result[0].text).toContain('Jane Smith'); }); test('should get table data with custom pagination', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tableData.slice(0, 2) }); const result = await handler.getTableData('Users', null, 'dbo', 2, 1); // Verify custom pagination expect(mockRequest.query).toHaveBeenCalledWith(expect.stringContaining('OFFSET 1 ROWS')); expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('FETCH NEXT 2 ROWS ONLY') ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('id') } ]); }); test('should get table data from specific database', async () => { mockRequest.query.mockResolvedValue({ recordset: mockData.tableData }); const result = await handler.getTableData('Users', 'TestDB', 'custom'); // Verify database-specific query expect(mockRequest.query).toHaveBeenCalledWith( expect.stringContaining('[TestDB].[custom].[Users]') ); expect(result).toEqual([ { type: 'text', text: expect.stringContaining('id') } ]); }); }); describe('error handling', () => { test('should handle connection manager errors', async () => { const error = new Error('Connection pool exhausted'); // Mock getPool to simulate a connection failure mockConnectionManager.getPool.mockRejectedValue(error); await expect(handler.listDatabases()).rejects.toThrow('Connection pool exhausted'); // Reset mock to default for other tests const mockPool = { request: vi.fn(() => mockRequest), connected: true, close: vi.fn() }; mockConnectionManager.getPool.mockReturnValue(mockPool); }); test('should handle SQL syntax errors', async () => { const sqlError = new Error('Invalid column name'); sqlError.code = 'EREQUEST'; mockRequest.query.mockRejectedValue(sqlError); await expect(handler.listTables()).rejects.toThrow('Invalid column name'); }); test('should record failed queries in performance monitor', async () => { const error = new Error('Query failed'); mockRequest.query.mockRejectedValue(error); await expect(handler.listDatabases()).rejects.toThrow('Query failed'); expect(mockPerformanceMonitor.recordQuery).toHaveBeenCalledWith( expect.objectContaining({ tool: 'list_databases', success: false, error: 'Query failed', executionTime: expect.any(Number) }) ); }); }); describe('exportTableCsv', () => { test('should export small table data as CSV without streaming', async () => { const mockData = [ { id: 1, name: 'John Doe', email: 'john@example.com' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com' } ]; // Mock the streaming handler to return non-streaming result handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: false, recordset: mockData, totalRows: 2, performance: { duration: 50, rowCount: 2, memoryEfficient: false } }); handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: false, memoryEfficient: false, totalRows: 2 }); const result = await handler.exportTableCsv('Users'); // Verify streamTableExport was called with correct parameters expect(handler.streamingHandler.streamTableExport).toHaveBeenCalledWith( expect.any(Object), 'Users', { schema: 'dbo', database: null, limit: null, outputFormat: 'csv' } ); // Verify CSV output format expect(result).toEqual([ { type: 'text', text: expect.stringContaining('id,name,email') } ]); expect(result[0].text).toContain('1,John Doe,john@example.com'); expect(result[0].text).toContain('2,Jane Smith,jane@example.com'); }); test('should export large table data with streaming', async () => { // Mock streaming response with chunks const mockChunks = [ { chunkNumber: 1, data: 'id,name,email\\n1,John Doe,john@example.com\\n2,Jane Smith,jane@example.com\\n', rowCount: 2, size: 65 }, { chunkNumber: 2, data: '3,Bob Wilson,bob@example.com\\n4,Alice Brown,alice@example.com\\n', rowCount: 2, size: 60 } ]; handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: true, chunks: mockChunks, chunkCount: 2, totalRows: 4, performance: { duration: 120, rowCount: 4, avgBatchSize: 2, memoryEfficient: true } }); // Mock getStreamingStats specifically for this test handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: true, memoryEfficient: true, totalRows: 4, chunkCount: 2, avgChunkSize: 2 }); // Mock reconstructFromChunks handler.streamingHandler.reconstructFromChunks = vi .fn() .mockReturnValue( 'id,name,email\\n1,John Doe,john@example.com\\n2,Jane Smith,jane@example.com\\n3,Bob Wilson,bob@example.com\\n4,Alice Brown,alice@example.com\\n' ); const result = await handler.exportTableCsv('LargeTable', 'TestDB'); // Verify streaming was used expect(handler.streamingHandler.streamTableExport).toHaveBeenCalledWith( expect.any(Object), 'LargeTable', { schema: 'dbo', database: 'TestDB', limit: null, outputFormat: 'csv' } ); expect(handler.streamingHandler.reconstructFromChunks).toHaveBeenCalledWith( mockChunks, 'csv' ); // Verify result structure expect(result).toEqual([ { type: 'text', text: expect.stringContaining('id,name,email') } ]); }); test('should handle empty table results', async () => { handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: false, totalRows: 0, performance: { duration: 10, rowCount: 0, memoryEfficient: false } }); // Mock getStreamingStats specifically for this test handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: false, memoryEfficient: false, totalRows: 0 }); const result = await handler.exportTableCsv('EmptyTable'); expect(result).toEqual([ { type: 'text', text: 'No data found in table' } ]); }); test('should handle CSV escaping correctly', async () => { const mockData = [ { id: 1, description: 'Text with, comma', notes: 'Text with "quotes"' }, { id: 2, description: 'Text with\nnewline', notes: null } ]; handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: false, recordset: mockData, totalRows: 2, performance: { duration: 30, rowCount: 2, memoryEfficient: false } }); // Mock getStreamingStats specifically for this test handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: false, memoryEfficient: false, totalRows: 2 }); const result = await handler.exportTableCsv('TestTable'); const csvText = result[0].text; expect(csvText).toContain('"Text with, comma"'); expect(csvText).toContain('"Text with ""quotes"""'); expect(csvText).toContain('"Text with\nnewline"'); expect(csvText).toContain('2,"Text with\nnewline",'); // null becomes empty }); test('should handle database switching', async () => { mockRequest.query.mockResolvedValue({ recordset: [] }); handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: false, totalRows: 0, performance: { duration: 10, rowCount: 0, memoryEfficient: false } }); // Mock getStreamingStats specifically for this test handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: false, memoryEfficient: false, totalRows: 0 }); await handler.exportTableCsv('Users', 'TestDB', 'custom'); // Verify database switch was attempted expect(mockRequest.query).toHaveBeenCalledWith('USE [TestDB]'); // Verify streaming handler was called with correct parameters expect(handler.streamingHandler.streamTableExport).toHaveBeenCalledWith( expect.any(Object), 'Users', { schema: 'custom', database: 'TestDB', limit: null, outputFormat: 'csv' } ); }); test('should apply limit when specified', async () => { handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: false, totalRows: 0, performance: { duration: 10, rowCount: 0, memoryEfficient: false } }); // Mock getStreamingStats specifically for this test handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: false, memoryEfficient: false, totalRows: 0 }); await handler.exportTableCsv('Users', null, 'dbo', 100); expect(handler.streamingHandler.streamTableExport).toHaveBeenCalledWith( expect.any(Object), 'Users', { schema: 'dbo', database: null, limit: 100, outputFormat: 'csv' } ); }); test('should record performance metrics for successful export', async () => { handler.streamingHandler.streamTableExport = vi.fn().mockResolvedValue({ success: true, streaming: true, totalRows: 1000, performance: { duration: 250, rowCount: 1000, avgBatchSize: 100, memoryEfficient: true } }); handler.streamingHandler.getStreamingStats = vi.fn().mockReturnValue({ streaming: true, memoryEfficient: true, totalRows: 1000 }); await handler.exportTableCsv('LargeTable'); expect(mockPerformanceMonitor.recordQuery).toHaveBeenCalledWith( expect.objectContaining({ tool: 'export_table_csv', success: true, streaming: true, totalRows: 1000, memoryEfficient: true, executionTime: 250 }) ); }); test('should handle streaming export errors', async () => { const error = new Error('Streaming failed'); handler.streamingHandler.streamTableExport = vi.fn().mockRejectedValue(error); await expect(handler.exportTableCsv('Users')).rejects.toThrow('Streaming failed'); expect(mockPerformanceMonitor.recordQuery).toHaveBeenCalledWith( expect.objectContaining({ tool: 'export_table_csv', success: false, error: 'Streaming failed', executionTime: 0 }) ); }); }); describe('performance monitoring integration', () => { test('should record query metrics for successful operations', async () => { mockRequest.query.mockResolvedValue({ recordset: [] }); await handler.listDatabases(); await handler.listTables(); await handler.describeTable('Users'); expect(mockPerformanceMonitor.recordQuery).toHaveBeenCalledTimes(3); expect(mockPerformanceMonitor.recordQuery).toHaveBeenNthCalledWith( 1, expect.objectContaining({ tool: 'list_databases', success: true }) ); expect(mockPerformanceMonitor.recordQuery).toHaveBeenNthCalledWith( 2, expect.objectContaining({ tool: 'list_tables', success: true }) ); expect(mockPerformanceMonitor.recordQuery).toHaveBeenNthCalledWith( 3, expect.objectContaining({ tool: 'describe_table', success: true }) ); }); test('should record timing information', async () => { mockRequest.query.mockImplementation(async () => { // Simulate query delay await new Promise(resolve => setTimeout(resolve, 10)); return { recordset: [] }; }); await handler.listDatabases(); expect(mockPerformanceMonitor.recordQuery).toHaveBeenCalledWith( expect.objectContaining({ tool: 'list_databases', executionTime: expect.any(Number), success: true }) ); // Verify timing is reasonable (should be >= 8ms due to our delay, allowing some variance) const [queryParams] = mockPerformanceMonitor.recordQuery.mock.calls[0]; expect(queryParams.executionTime).toBeGreaterThanOrEqual(8); }); }); });

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/egarcia74/warp-sql-server-mcp'

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