Skip to main content
Glama
errorFactory.test.ts14 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AuthenticationErrorFactory, AuthorizationErrorFactory, ResourceNotFoundErrorFactory, ValidationErrorFactory, NetworkErrorFactory, DatabaseErrorFactory, RateLimitErrorFactory, ExportErrorFactory, createErrorFromHttpResponse, } from '../../src/utils/errorFactory.js'; import { validateMetabaseResponse, extractCleanErrorMessage } from '../../src/utils/errorHandling.js'; import { McpError } from '../../src/types/core.js'; describe('ErrorFactory', () => { describe('AuthenticationErrorFactory', () => { it('should create invalid credentials error', () => { const error = AuthenticationErrorFactory.invalidCredentials(); expect(error.message).toBe('Authentication failed: Invalid credentials'); }); it('should create session expired error', () => { const error = AuthenticationErrorFactory.sessionExpired(); expect(error.message).toBe('Authentication failed: Session expired'); }); it('should create invalid API key error', () => { const error = AuthenticationErrorFactory.invalidApiKey(); expect(error.message).toBe('Authentication failed: Invalid API key'); }); }); describe('AuthorizationErrorFactory', () => { it('should create insufficient permissions error', () => { const error = AuthorizationErrorFactory.insufficientPermissions('dashboard', 'access'); expect(error.message).toBe('Access denied: Insufficient permissions for dashboard to access'); }); it('should create insufficient permissions error without resource', () => { const error = AuthorizationErrorFactory.insufficientPermissions(); expect(error.message).toBe('Access denied: Insufficient permissions'); }); it('should create collection access error', () => { const error = AuthorizationErrorFactory.collectionAccess(123); expect(error.message).toBe('Access denied: Cannot access collection 123'); }); }); describe('ResourceNotFoundErrorFactory', () => { it('should create resource not found error', () => { const error = ResourceNotFoundErrorFactory.resource('dashboard', 456); expect(error.message).toBe('dashboard not found: 456'); }); it('should create database not found error', () => { const error = ResourceNotFoundErrorFactory.database(789); expect(error.message).toBe('Database not found: 789'); }); }); describe('ValidationErrorFactory', () => { it('should create invalid parameter error', () => { const error = ValidationErrorFactory.invalidParameter('card_id', 'invalid', 'Must be a positive integer'); expect(error.message).toBe('Invalid parameter: card_id. Expected: Must be a positive integer'); }); it('should create invalid parameter error without format', () => { const error = ValidationErrorFactory.invalidParameter('limit', -1); expect(error.message).toBe('Invalid parameter: limit'); }); it('should create SQL syntax error', () => { const error = ValidationErrorFactory.sqlSyntaxError('SELECT * FROM', 'Missing table name'); expect(error.message).toBe('SQL syntax error: Missing table name'); }); it('should create card parameter mismatch error', () => { const parameterDetails = { tag: { name: 'user_id', type: 'id' }, params: [{ value: 'john_doe' }] }; const error = ValidationErrorFactory.cardParameterMismatch(parameterDetails); expect(error.message).toBe('Card parameter type mismatch: user_id expects id'); }); }); describe('NetworkErrorFactory', () => { it('should create timeout error', () => { const error = NetworkErrorFactory.timeout('Search operation', 30000); expect(error.message).toBe('Operation timed out: Search operation (30000ms)'); }); it('should create connection error', () => { const error = NetworkErrorFactory.connectionError('https://example.com'); expect(error.message).toBe('Cannot connect to Metabase server: https://example.com'); }); }); describe('DatabaseErrorFactory', () => { it('should create query execution error', () => { const error = DatabaseErrorFactory.queryExecutionError('Table not found'); expect(error.message).toBe('Query execution failed: Table not found'); }); it('should create connection lost error', () => { const error = DatabaseErrorFactory.connectionLost(1); expect(error.message).toBe('Database connection lost: 1'); }); }); describe('RateLimitErrorFactory', () => { it('should create rate limit exceeded error with retry time', () => { const error = RateLimitErrorFactory.exceeded(120000); expect(error.message).toBe('Rate limit exceeded. Retry after 120000ms'); }); it('should create rate limit exceeded error without retry time', () => { const error = RateLimitErrorFactory.exceeded(); expect(error.message).toBe('Rate limit exceeded.'); }); }); describe('ExportErrorFactory', () => { it('should create file size exceeded error', () => { const error = ExportErrorFactory.fileSizeExceeded(5000000, 1000000); expect(error.message).toBe('Export file too large: 5000000 bytes (max: 1000000)'); }); it('should create processing failed error', () => { const error = ExportErrorFactory.processingFailed('CSV', 'Memory limit exceeded'); expect(error.message).toBe('Export processing failed for CSV: Memory limit exceeded'); }); }); describe('createErrorFromHttpResponse', () => { it('should create 400 validation error for SQL syntax', () => { const error = createErrorFromHttpResponse( 400, { message: 'SQL syntax error: missing FROM clause' }, 'execute query' ); expect(error.message).toBe('SQL syntax error: SQL syntax error: missing FROM clause'); }); it('should create 400 card parameter mismatch error', () => { const responseData = { error_type: 'invalid-parameter', 'ex-data': { tag: { name: 'user_id', type: 'id' }, params: [{ value: 'john_doe' }] } }; const error = createErrorFromHttpResponse(400, responseData, 'execute card'); expect(error.message).toBe('Card parameter type mismatch: user_id expects id'); }); it('should create 401 authentication error', () => { const error = createErrorFromHttpResponse(401, { message: 'Invalid API key' }, 'search cards'); expect(error.message).toBe('Authentication failed: Invalid API key'); }); it('should create 403 authorization error', () => { const error = createErrorFromHttpResponse(403, { message: 'Access denied' }, 'get dashboard', 'dashboard', 123); expect(error.message).toBe('Access denied: Insufficient permissions for dashboard to access'); }); it('should create 404 resource not found error', () => { const error = createErrorFromHttpResponse(404, { message: 'Not found' }, 'get card', 'card', 456); expect(error.message).toBe('card not found: 456'); }); it('should create 429 rate limit error', () => { const error = createErrorFromHttpResponse(429, { message: 'Rate limit exceeded', retryAfter: 30000 }, 'search'); expect(error.message).toBe('Rate limit exceeded. Retry after 30000ms'); }); it('should create 500 database error for SQL-related errors', () => { const error = createErrorFromHttpResponse(500, { message: 'Database connection timeout' }, 'execute query'); expect(error.message).toBe('Query execution failed: Database connection timeout'); }); it('should create 503 service unavailable error', () => { const error = createErrorFromHttpResponse(503, { message: 'Service unavailable' }, 'list cards'); expect(error.message).toBe('Metabase service temporarily unavailable'); }); }); describe('validateMetabaseResponse', () => { const mockLogError = vi.fn(); beforeEach(() => { mockLogError.mockClear(); }); it('should not throw for successful responses', () => { const successResponse = { data: { rows: [['test']], cols: [{ name: 'col1' }] }, status: 'success' }; expect(() => { validateMetabaseResponse(successResponse, { operation: 'Test operation', resourceId: 123 }, mockLogError); }).not.toThrow(); expect(mockLogError).not.toHaveBeenCalled(); }); // Card-specific error handling tests it('should throw Error for card invalid parameter name (Test 1 pattern)', () => { const errorResponse = { message: 'Invalid parameter: Card 38 does not have a template tag named "fake_param".', 'invalid-parameter': { id: 'fake-uuid', type: 'id', slug: 'fake_param', value: '123' } }; expect(() => { validateMetabaseResponse(errorResponse, { operation: 'Card export', resourceId: 38 }, mockLogError); }).toThrowError('Invalid parameter: Card 38 does not have a template tag named "fake_param".'); expect(mockLogError).toHaveBeenCalledWith('Card export parameter validation failed for 38', errorResponse); }); it('should throw Error for card invalid parameter value with via[].error (Test 2 pattern)', () => { const errorResponse = { error_type: 'invalid-parameter', status: 'failed', error: 'For input string: "not"', via: [{ error_type: 'invalid-parameter', error: 'Error determining value for parameter "account_id": For input string: "not"', 'ex-data': { tag: { name: 'account_id', type: 'dimension' }, params: [{ value: 'not_a_number' }] } }] }; expect(() => { validateMetabaseResponse(errorResponse, { operation: 'Card execution', resourceId: 123 }, mockLogError); }).toThrowError('Error determining value for parameter "account_id": For input string: "not"'); expect(mockLogError).toHaveBeenCalledWith('Card execution parameter validation failed for 123', errorResponse); }); it('should throw Error for card invalid-parameter with fallback to top-level error', () => { const errorResponse = { error_type: 'invalid-parameter', status: 'failed', error: 'Parameter validation failed' }; expect(() => { validateMetabaseResponse(errorResponse, { operation: 'Card execution', resourceId: 456 }, mockLogError); }).toThrowError('Parameter validation failed'); }); // Query-specific error handling tests it('should throw McpError for query invalid-parameter error type with ex-data', () => { const errorResponse = { error_type: 'invalid-parameter', status: 'failed', error: 'For input string: "test"', via: [{ error_type: 'invalid-parameter', 'ex-data': { tag: { name: 'user_id', type: 'id' }, params: [{ value: 'test_value' }] } }] }; expect(() => { validateMetabaseResponse(errorResponse, { operation: 'SQL query execution', resourceId: 123 }, mockLogError); }).toThrow(McpError); expect(mockLogError).toHaveBeenCalledWith('SQL query execution parameter validation failed for 123', errorResponse); }); it('should throw Error for failed status with error message', () => { const errorResponse = { status: 'failed', error: 'Database connection failed' }; expect(() => { validateMetabaseResponse(errorResponse, { operation: 'Query execution' }, mockLogError); }).toThrowError('Database connection failed.'); }); it('should throw Error for other error types with failed status', () => { const errorResponse = { error_type: 'database-error', status: 'failed', error: 'Database timeout' }; expect(() => { validateMetabaseResponse(errorResponse, { operation: 'Query execution' }, mockLogError); }).toThrowError('Database timeout.'); }); }); describe('extractCleanErrorMessage', () => { it('should extract table not found error', () => { const error = 'Table "ORDERSASD" not found; SQL statement:\n-- Metabase:: userID: 1 queryType: native queryHash: 22df1740fa126b46d6d53bc2fd61d90c042ba4c4a1802d544dfd4449e29c2eae\nSELECT ID FROM ORDERSASD [42102-214]'; expect(extractCleanErrorMessage(error)).toBe('Table "ORDERSASD" not found.'); }); it('should extract column not found error', () => { const error = 'Column "IDZ" not found; SQL statement:\nSELECT IDZ FROM ORDERS [42122-214]'; expect(extractCleanErrorMessage(error)).toBe('Column "IDZ" not found.'); }); it('should handle error without SQL statement suffix', () => { const error = 'Only SELECT statements are allowed in a native query.'; expect(extractCleanErrorMessage(error)).toBe('Only SELECT statements are allowed in a native query.'); }); it('should handle empty error string', () => { expect(extractCleanErrorMessage('')).toBe('Unknown query error'); }); it('should handle null/undefined error', () => { expect(extractCleanErrorMessage(null as any)).toBe('Unknown query error'); expect(extractCleanErrorMessage(undefined as any)).toBe('Unknown query error'); }); it('should add period if missing', () => { const error = 'Database timeout'; expect(extractCleanErrorMessage(error)).toBe('Database timeout.'); }); it('should not add period if already present', () => { const error = 'Connection failed.'; expect(extractCleanErrorMessage(error)).toBe('Connection failed.'); }); it('should remove H2 database error codes', () => { const error = 'Syntax error in SQL statement [42001-214]'; expect(extractCleanErrorMessage(error)).toBe('Syntax error in SQL statement.'); }); }); });

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