Skip to main content
Glama
list.test.ts19.1 kB
/** * Unit tests for the list handler */ import { describe, it, expect, beforeEach } from 'vitest'; import { handleList } from '../../src/handlers/list/index.js'; import { McpError } from '../../src/types/core.js'; import { mockApiClient, mockLogger, resetAllMocks, createMockRequest, createCachedResponse, getLoggerFunctions, sampleCard, sampleDashboard, sampleTable, sampleDatabase, sampleCollection } from '../setup.js'; describe('handleList', () => { beforeEach(() => { resetAllMocks(); }); describe('Parameter validation', () => { it('should throw error when model parameter is missing', async () => { const request = createMockRequest('list', {}); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow(McpError); expect(mockLogger.logWarn).toHaveBeenCalledWith( 'Missing or invalid model parameter in list request', { requestId: 'test-request-id' } ); }); it('should throw error when model parameter is invalid', async () => { const request = createMockRequest('list', { model: 'invalid-model' }); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow(McpError); expect(mockLogger.logWarn).toHaveBeenCalledWith( 'Invalid model parameter: invalid-model', expect.objectContaining({ requestId: 'test-request-id', validValues: expect.any(Array) }) ); }); it('should throw error when model parameter is not a string', async () => { const request = createMockRequest('list', { model: 123 }); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow(McpError); expect(mockLogger.logWarn).toHaveBeenCalledWith( 'Missing or invalid model parameter in list request', { requestId: 'test-request-id' } ); }); }); describe('Cards listing', () => { it('should successfully list cards', async () => { const mockCards = [sampleCard]; mockApiClient.getCardsList.mockResolvedValue(createCachedResponse(mockCards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockApiClient.getCardsList).toHaveBeenCalled(); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Test Card'); }); it('should handle empty cards list', async () => { mockApiClient.getCardsList.mockResolvedValue(createCachedResponse([])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.total_items).toBe(0); }); it('should handle API errors for cards', async () => { const apiError = new Error('API Error'); mockApiClient.getCardsList.mockRejectedValue(apiError); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow(); }); }); describe('Dashboards listing', () => { it('should successfully list dashboards', async () => { const mockDashboards = [sampleDashboard]; mockApiClient.getDashboardsList.mockResolvedValue(createCachedResponse(mockDashboards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'dashboards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockApiClient.getDashboardsList).toHaveBeenCalled(); expect(result.content[0].text).toContain('Test Dashboard'); }); it('should handle empty dashboards list', async () => { mockApiClient.getDashboardsList.mockResolvedValue(createCachedResponse([])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'dashboards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.total_items).toBe(0); }); }); describe('Tables listing', () => { it('should successfully list tables', async () => { const mockTables = [sampleTable]; mockApiClient.getTablesList.mockResolvedValue(createCachedResponse(mockTables)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'tables' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockApiClient.getTablesList).toHaveBeenCalled(); expect(result.content[0].text).toContain('Test Table'); }); it('should handle empty tables list', async () => { mockApiClient.getTablesList.mockResolvedValue(createCachedResponse([])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'tables' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.total_items).toBe(0); }); }); describe('Databases listing', () => { it('should successfully list databases', async () => { const mockDatabases = [sampleDatabase]; mockApiClient.getDatabasesList.mockResolvedValue(createCachedResponse(mockDatabases)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'databases' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockApiClient.getDatabasesList).toHaveBeenCalled(); expect(result.content[0].text).toContain('Test Database'); }); it('should handle empty databases list', async () => { mockApiClient.getDatabasesList.mockResolvedValue(createCachedResponse([])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'databases' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.total_items).toBe(0); }); }); describe('Collections listing', () => { it('should successfully list collections', async () => { const mockCollections = [sampleCollection]; mockApiClient.getCollectionsList.mockResolvedValue(createCachedResponse(mockCollections)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'collections' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockApiClient.getCollectionsList).toHaveBeenCalled(); expect(result.content[0].text).toContain('Test Collection'); }); it('should handle empty collections list', async () => { mockApiClient.getCollectionsList.mockResolvedValue(createCachedResponse([])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'collections' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.total_items).toBe(0); }); }); describe('Logging', () => { it('should log debug information', async () => { mockApiClient.getCardsList.mockResolvedValue(createCachedResponse([sampleCard])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockLogger.logDebug).toHaveBeenCalledWith('Listing cards from Metabase (all items)'); }); it('should log success information', async () => { mockApiClient.getCardsList.mockResolvedValue(createCachedResponse([sampleCard])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockLogger.logInfo).toHaveBeenCalledWith('Successfully listed 1 cards'); }); }); describe('Cache source handling', () => { it('should indicate cache source in response', async () => { mockApiClient.getCardsList.mockResolvedValue(createCachedResponse([sampleCard], 'cache')); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.source).toBe('cache'); }); it('should indicate API source in response', async () => { mockApiClient.getCardsList.mockResolvedValue(createCachedResponse([sampleCard], 'api')); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.source).toBe('api'); }); }); describe('List pagination', () => { it('should support pagination with offset and limit for cards', async () => { const manyCards = Array.from({ length: 50 }, (_, i) => ({ ...sampleCard, id: i + 1, name: `Test Card ${i + 1}`, })); mockApiClient.getCardsList.mockResolvedValue(createCachedResponse(manyCards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', offset: 10, limit: 20 }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); expect(mockApiClient.getCardsList).toHaveBeenCalled(); const responseData = JSON.parse(result.content[0].text); // Check pagination metadata expect(responseData.pagination).toBeDefined(); expect(responseData.pagination.total_items).toBe(50); expect(responseData.pagination.offset).toBe(10); expect(responseData.pagination.limit).toBe(20); expect(responseData.pagination.current_page_size).toBe(20); expect(responseData.pagination.has_more).toBe(true); expect(responseData.pagination.next_offset).toBe(30); // Check that only the requested slice of items is returned expect(responseData.results).toHaveLength(20); expect(responseData.results[0].name).toBe('Test Card 11'); // 0-based slice starting at index 10 expect(responseData.results[19].name).toBe('Test Card 30'); // Last item in the slice }); it('should handle pagination for the last page correctly', async () => { const cards = Array.from({ length: 25 }, (_, i) => ({ ...sampleCard, id: i + 1, name: `Test Card ${i + 1}`, })); mockApiClient.getCardsList.mockResolvedValue(createCachedResponse(cards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', offset: 20, limit: 20 }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); // Check pagination metadata for last page expect(responseData.pagination.total_items).toBe(25); expect(responseData.pagination.offset).toBe(20); expect(responseData.pagination.limit).toBe(20); expect(responseData.pagination.current_page_size).toBe(5); // Only 5 items left expect(responseData.pagination.has_more).toBe(false); expect(responseData.pagination.next_offset).toBeUndefined(); // Check that only the remaining items are returned expect(responseData.results).toHaveLength(5); expect(responseData.results[0].name).toBe('Test Card 21'); expect(responseData.results[4].name).toBe('Test Card 25'); }); it('should reject limit greater than 1000', async () => { const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', limit: 1500 }); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow('Invalid parameter: limit'); }); it('should work without pagination parameters (backward compatibility)', async () => { mockApiClient.getCardsList.mockResolvedValue(createCachedResponse([sampleCard])); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards' }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); // No pagination metadata should be present expect(responseData.pagination).toBeUndefined(); expect(responseData.results).toHaveLength(1); expect(responseData.usage_guidance).toContain('For large datasets exceeding token limits, use offset and limit parameters'); }); it('should include pagination guidance in usage_guidance when pagination is used', async () => { const cards = Array.from({ length: 10 }, (_, i) => ({ ...sampleCard, id: i + 1, name: `Test Card ${i + 1}`, })); mockApiClient.getCardsList.mockResolvedValue(createCachedResponse(cards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', limit: 5 }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.usage_guidance).toContain('paginated overview'); expect(responseData.usage_guidance).toContain('offset and limit parameters'); }); it('should accept offset of 0', async () => { const cards = Array.from({ length: 10 }, (_, i) => ({ ...sampleCard, id: i + 1, name: `Test Card ${i + 1}`, })); mockApiClient.getCardsList.mockResolvedValue(createCachedResponse(cards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', offset: 0, limit: 5 }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.pagination.offset).toBe(0); expect(responseData.results).toHaveLength(5); }); it('should reject negative offset', async () => { const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', offset: -1, limit: 5 }); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow('offset must be non-negative'); }); it('should accept exact limit boundary value of 1000', async () => { const cards = Array.from({ length: 1000 }, (_, i) => ({ ...sampleCard, id: i + 1, name: `Test Card ${i + 1}`, })); mockApiClient.getCardsList.mockResolvedValue(createCachedResponse(cards)); const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', limit: 1000 }); const result = await handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError); const responseData = JSON.parse(result.content[0].text); expect(responseData.pagination.limit).toBe(1000); expect(responseData.results).toHaveLength(1000); }); it('should reject non-numeric offset parameter', async () => { const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', offset: 'invalid', limit: 5 }); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow('offset must be a number'); }); it('should reject non-numeric limit parameter', async () => { const [logDebug, logInfo, logWarn, logError] = getLoggerFunctions(); const request = createMockRequest('list', { model: 'cards', limit: 'invalid' }); await expect( handleList(request, 'test-request-id', mockApiClient as any, logDebug, logInfo, logWarn, logError) ).rejects.toThrow('limit must be a number'); }); }); });

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/jerichosequitin/Metabase'

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