Skip to main content
Glama
UniversalRetrievalService-validation.test.tsβ€’9.51 kB
/** * Split: UniversalRetrievalService validation & error handling */ import { describe, it, expect, beforeEach, vi } from 'vitest'; // Inline mocks matching this file's specifiers vi.mock('../../src/services/CachingService.js', () => ({ CachingService: { isCached404: vi.fn(), cache404Response: vi.fn() }, })); vi.mock('../../src/services/ValidationService.js', () => ({ ValidationService: { validateUUID: vi.fn() }, })); vi.mock('../../src/middleware/performance-enhanced.js', () => ({ enhancedPerformanceTracker: { startOperation: vi.fn(() => 'perf-123'), markTiming: vi.fn(), markApiStart: vi.fn(() => 100), markApiEnd: vi.fn(), endOperation: vi.fn(), }, })); vi.mock('../../src/services/create/index.js', () => ({ shouldUseMockData: vi.fn(() => false), })); vi.mock('../../src/utils/validation/uuid-validation.js', () => ({ isValidUUID: vi.fn(() => true), createRecordNotFoundError: vi.fn( () => new (class EnhancedApiError extends Error { statusCode = 404; constructor() { super('Record not found'); this.name = 'EnhancedApiError'; } })() ), })); vi.mock('../../src/errors/enhanced-api-errors.js', () => ({ ErrorEnhancer: { autoEnhance: (e: any) => e, getErrorMessage: (e: any) => (e && e.message) || String(e), }, EnhancedApiError: class EnhancedApiError extends Error { statusCode: number; endpoint: string; method: string; constructor( message: string, statusCode = 500, endpoint = '/test', method = 'GET', context?: Record<string, unknown> ) { super(message); this.name = 'EnhancedApiError'; this.statusCode = statusCode; this.endpoint = endpoint; this.method = method; if (context) { Object.assign(this, context); } } }, })); vi.mock('../../src/objects/companies/index.js', () => ({ getCompanyDetails: vi.fn(), })); vi.mock('../../src/objects/lists.js', () => ({ getListDetails: vi.fn() })); vi.mock('../../src/objects/tasks.js', () => ({ getTask: vi.fn() })); vi.mock('../../src/objects/notes.js', () => ({ getNote: vi.fn() })); import { UniversalRetrievalService } from '../../src/services/UniversalRetrievalService.js'; import { UniversalResourceType } from '../../src/handlers/tool-configs/universal/types.js'; import { EnhancedApiError } from '../../src/errors/enhanced-api-errors.js'; import { CachingService } from '../../src/services/CachingService.js'; import { shouldUseMockData } from '../../src/services/create/index.js'; import { createRecordNotFoundError } from '../../src/utils/validation/uuid-validation.js'; import { enhancedPerformanceTracker } from '../../src/middleware/performance-enhanced.js'; import { getCompanyDetails } from '../../src/objects/companies/index.js'; import * as tasks from '../../src/objects/tasks.js'; import * as lists from '../../src/objects/lists.js'; import * as companies from '../../src/objects/companies/index.js'; import * as notes from '../../src/objects/notes.js'; describe('UniversalRetrievalService', () => { beforeEach(() => { vi.clearAllMocks(); // Default to real API for error handling tests (they expect functions to throw) vi.mocked(shouldUseMockData).mockReturnValue(false); }); describe('getRecordDetails - error cases', () => { it('should throw error for unsupported resource type', async () => { await expect( UniversalRetrievalService.getRecordDetails({ resource_type: 'invalid' as any, record_id: 'test_123', }) ).rejects.toThrow('Unsupported resource type for get details: invalid'); }); it('should handle cached 404 responses', async () => { vi.mocked(CachingService.isCached404).mockReturnValue(true); vi.mocked(createRecordNotFoundError).mockReturnValue( new EnhancedApiError('Record not found', 404, '/records/test', 'GET', { resourceType: 'record', }) ); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.COMPANIES, record_id: 'comp_123', }) ).rejects.toThrow('Record not found'); expect(CachingService.isCached404).toHaveBeenCalledWith( UniversalResourceType.COMPANIES, 'comp_123' ); expect(enhancedPerformanceTracker.endOperation).toHaveBeenCalledWith( 'perf-123', false, 'Cached 404 response', 404, { cached: true } ); }); it('should handle 404 errors and cache them', async () => { vi.mocked(CachingService.isCached404).mockReturnValue(false); const notFoundError = { response: { status: 404 }, message: 'Company not found', } as any; vi.mocked(getCompanyDetails).mockRejectedValue(notFoundError); vi.mocked(createRecordNotFoundError).mockReturnValue( new EnhancedApiError('Record not found', 404, '/records/test', 'GET', { resourceType: 'record', }) ); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.COMPANIES, record_id: 'comp_123', }) ).rejects.toThrow('Record not found'); expect(CachingService.cache404Response).toHaveBeenCalledWith( UniversalResourceType.COMPANIES, 'comp_123' ); }); it('should handle task retrieval errors and cache 404s', async () => { vi.mocked(CachingService.isCached404).mockReturnValue(false); const taskError = new Error('Task not found'); vi.mocked(tasks.getTask).mockRejectedValue(taskError); vi.mocked(createRecordNotFoundError).mockReturnValue( new EnhancedApiError('Record not found', 404, '/records/test', 'GET', { resourceType: 'record', }) ); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', }) ).rejects.toThrow('Record not found'); expect(CachingService.cache404Response).toHaveBeenCalledWith( UniversalResourceType.TASKS, 'task_123' ); }); }); describe('Auth Error Handling', () => { it('should re-throw auth errors without caching', async () => { vi.mocked(CachingService.isCached404).mockReturnValue(false); vi.mocked(lists.getListDetails).mockRejectedValue( new EnhancedApiError('Unauthorized', 401, '/lists/123', 'GET', { httpStatus: 401, resourceType: 'lists', }) ); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.LISTS, record_id: '12345678-1234-4000-a000-123456789012', }) ).rejects.toMatchObject({ name: 'EnhancedApiError', statusCode: 401, message: expect.stringMatching(/unauthorized|invalid api key/i), }); expect(CachingService.cache404Response).not.toHaveBeenCalled(); }); }); describe('Rate Limit Error Handling', () => { it('should re-throw rate limit errors without caching', async () => { vi.mocked(CachingService.isCached404).mockReturnValue(false); vi.mocked(tasks.getTask).mockRejectedValue( new EnhancedApiError('Too many requests', 429, '/tasks/111', 'GET', { httpStatus: 429, resourceType: 'tasks', }) ); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.TASKS, record_id: '11111111-1111-4000-a000-111111111111', }) ).rejects.toMatchObject({ name: 'EnhancedApiError', statusCode: 429, message: expect.stringMatching(/rate.{0,10}limit|too many requests/i), }); expect(CachingService.cache404Response).not.toHaveBeenCalled(); }); }); describe('Legitimate 404 Error Handling', () => { it('should cache legitimate 404 errors and convert to structured response', async () => { vi.mocked(notes.getNote).mockRejectedValue( new EnhancedApiError('Note not found', 404, '/notes/222', 'GET', { httpStatus: 404, resourceType: 'notes', }) ); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.NOTES, record_id: '22222222-2222-4000-a000-222222222222', }) ).rejects.toMatchObject({ name: 'EnhancedApiError', statusCode: 404, message: expect.stringMatching(/note.*not found/i), }); expect(CachingService.cache404Response).toHaveBeenCalledWith( 'notes', '22222222-2222-4000-a000-222222222222' ); }); }); describe('Non-HTTP Error Handling', () => { it('should handle TypeError exceptions without masking as 404', async () => { vi.mocked(CachingService.isCached404).mockReturnValue(false); const typeError = new TypeError('Cannot read properties of null'); vi.mocked(companies.getCompanyDetails).mockRejectedValue(typeError); await expect( UniversalRetrievalService.getRecordDetails({ resource_type: UniversalResourceType.COMPANIES, record_id: '33333333-3333-4000-a000-333333333333', }) ).rejects.toThrow('Cannot read properties of null'); expect(CachingService.cache404Response).not.toHaveBeenCalled(); }); }); });

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/kesslerio/attio-mcp-server'

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