Skip to main content
Glama
UniversalUpdateService-validation.test.tsβ€’15 kB
/** * Split: UniversalUpdateService validation & error handling */ import { describe, it, expect, beforeEach, vi } from 'vitest'; vi.mock('../../src/objects/tasks.js', () => ({ getTask: vi.fn(), updateTask: vi.fn(), })); vi.mock('../../src/handlers/tool-configs/universal/field-mapper.js', () => ({ mapRecordFields: vi.fn(), mapTaskFields: vi.fn((_: string, i: any) => i), validateResourceType: vi.fn(), getFieldSuggestions: vi.fn(), validateFields: vi.fn(), })); vi.mock('../../src/utils/validation-utils.js', () => ({ validateRecordFields: vi.fn(), })); vi.mock('../../src/utils/logger.js', () => ({ debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn(), LogLevel: {}, OperationType: {}, createScopedLogger: vi.fn(() => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), })), setLogContext: vi.fn(), getLogContext: vi.fn(), clearLogContext: vi.fn(), generateCorrelationId: vi.fn(), PerformanceTimer: vi.fn(), operationStart: vi.fn(), operationSuccess: vi.fn(), operationFailure: vi.fn(), fallbackStart: vi.fn(), withLogging: vi.fn(), safeMcpLog: vi.fn(), default: {}, })); // Mock the create service factory to return a mock service const mockCreateService = { createCompany: vi.fn(), createPerson: vi.fn(), createTask: vi.fn(), createList: vi.fn(), createNote: vi.fn(), createDeal: vi.fn(), updateTask: vi.fn(), }; vi.mock('../../src/services/create/index.js', () => ({ getCreateService: vi.fn(() => mockCreateService), shouldUseMockData: vi.fn(() => true), })); vi.mock('../../src/objects/companies/index.js', () => ({ updateCompany: vi.fn(() => ({ id: { record_id: 'comp_123' }, values: {} })), })); vi.mock('../../src/objects/lists.js', () => ({ updateList: vi.fn(() => ({ id: { record_id: 'list_123' }, values: {} })), })); vi.mock('../../src/objects/people-write.js', () => ({ updatePerson: vi.fn(() => ({ id: { record_id: 'person_123' }, values: {} })), })); vi.mock('../../src/objects/records/index.js', () => ({ updateObjectRecord: vi.fn(() => ({ id: { record_id: 'record_123' }, values: {}, })), })); import { UniversalUpdateService } from '../../src/services/UniversalUpdateService.js'; import { UniversalResourceType } from '../../src/handlers/tool-configs/universal/types.js'; import { getFieldSuggestions, mapRecordFields, validateFields, validateResourceType, } from '../../src/handlers/tool-configs/universal/field-mapper.js'; import { validateRecordFields } from '../../src/utils/validation-utils.js'; import { getCreateService, shouldUseMockData, } from '../../src/services/create/index.js'; import * as tasks from '../../src/objects/tasks.js'; // Ensure enhanced validation is disabled for these unit tests beforeEach(() => { delete process.env.ENABLE_ENHANCED_VALIDATION; }); describe('UniversalUpdateService', () => { beforeEach(() => { vi.clearAllMocks(); process.env.SKIP_FIELD_VERIFICATION = 'true'; // Default to using mock data for most tests (offline mode) vi.mocked(shouldUseMockData).mockReturnValue(true); vi.mocked(validateFields).mockReturnValue({ warnings: [], suggestions: [], } as any); vi.mocked(mapRecordFields).mockReturnValue({ mapped: {}, warnings: [], errors: [], } as any); }); describe('Field validation & suggestions', () => { it('should log warnings and suggestions when present', async () => { vi.mocked(validateFields).mockReturnValue({ warnings: ['Field warning 1', 'Field warning 2'], suggestions: ['Suggestion 1', 'Suggestion 2'], } as any); // Mock the debug logger to verify structured logging is used const { debug } = await import('../../src/utils/logger.js'); const debugSpy = vi.mocked(debug); mockCreateService.updateTask.mockResolvedValue({ id: { record_id: 'comp_123' }, values: { name: 'Test Company' }, } as any); await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.COMPANIES, record_id: 'comp_123', record_data: { values: { name: 'Test Company' } }, }); // Verify structured logging was used instead of console.warn expect(debugSpy).toHaveBeenCalledWith( 'UniversalUpdateService', 'Field validation warnings', expect.objectContaining({ warnings: expect.stringContaining('Field warning 1'), }) ); expect(debugSpy).toHaveBeenCalledWith( 'UniversalUpdateService', 'Field suggestions:', expect.objectContaining({ suggestions: expect.stringContaining('Suggestion 1'), }) ); }); it('should handle field mapping errors', async () => { vi.mocked(mapRecordFields).mockReturnValue({ mapped: {}, warnings: [], errors: ['Mapping error 1', 'Mapping error 2'], } as any); await expect( UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.COMPANIES, record_id: 'comp_123', record_data: { values: { name: 'Test Company' } }, }) ).rejects.toThrow('Mapping error 1 Mapping error 2'); }); it('should handle enhanced validation when enabled', async () => { process.env.ENABLE_ENHANCED_VALIDATION = 'true'; vi.mocked(validateRecordFields).mockResolvedValue({ isValid: false, error: 'Validation failed: invalid field', } as any); await expect( UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.COMPANIES, record_id: 'comp_123', record_data: { values: { name: 'Test Company' } }, }) ).rejects.toThrow('Validation failed: invalid field'); }); it('should handle attribute not found errors with suggestions', async () => { // Simulate downstream update error const { updateCompany } = await import( '../../src/objects/companies/index.js' ); vi.mocked(updateCompany as any).mockRejectedValue( new Error('Cannot find attribute with slug/ID "invalid_field"') ); const error = new Error( 'Cannot find attribute with slug/ID "invalid_field"' ); vi.mocked(getFieldSuggestions).mockReturnValue('Did you mean "name"?'); await expect( UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.COMPANIES, record_id: 'comp_123', record_data: { values: { name: 'Test Company' } }, }) ).rejects.toThrow('Cannot find attribute with slug/ID "invalid_field"'); expect(getFieldSuggestions).toHaveBeenCalled(); }); // Removed: unsupported resource type validation is not part of current service behavior }); describe('Task-specific validation', () => { beforeEach(() => { vi.mocked(validateFields).mockReturnValue({ warnings: [], suggestions: [], } as any); mockCreateService.updateTask.mockResolvedValue({ id: { record_id: 'task_123', task_id: 'task_123' }, values: { content: 'Test Task' }, } as any); }); it('should transform is_completed to status', async () => { vi.mocked(mapRecordFields).mockReturnValue({ mapped: { is_completed: true }, warnings: [], errors: [], } as any); await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', record_data: { values: { is_completed: true } }, }); expect(mockCreateService.updateTask).toHaveBeenCalledWith('task_123', { status: 'completed', }); }); it('should transform assignees to assigneeId', async () => { vi.mocked(mapRecordFields).mockReturnValue({ mapped: { assignees: 'user_456' }, warnings: [], errors: [], } as any); await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', record_data: { values: { assignees: 'user_456' } }, }); expect(mockCreateService.updateTask).toHaveBeenCalledWith('task_123', { assigneeId: 'user_456', }); }); it('should transform deadline_at to dueDate', async () => { vi.mocked(mapRecordFields).mockReturnValue({ mapped: { deadline_at: '2024-02-01' }, warnings: [], errors: [], } as any); await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', record_data: { values: { deadline_at: '2024-02-01' } }, }); expect(mockCreateService.updateTask).toHaveBeenCalledWith('task_123', { dueDate: '2024-02-01', }); }); it('should transform linked_records array to recordIds', async () => { vi.mocked(mapRecordFields).mockReturnValue({ mapped: { linked_records: [ { record_id: 'comp_123' }, { id: 'comp_456' }, 'comp_789', ], }, warnings: [], errors: [], } as any); await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', record_data: { values: { linked_records: [] } }, }); expect(mockCreateService.updateTask).toHaveBeenCalledWith('task_123', { recordIds: ['comp_123', 'comp_456', 'comp_789'], }); }); }); describe('Mock data handling & existence checks', () => { beforeEach(() => { vi.mocked(validateFields).mockReturnValue({ warnings: [], suggestions: [], } as any); vi.mocked(mapRecordFields).mockReturnValue({ mapped: { status: 'completed' }, warnings: [], errors: [], } as any); }); it('should use mock data in E2E mode', async () => { process.env.E2E_MODE = 'true'; mockCreateService.updateTask.mockResolvedValue({ id: { record_id: 'task_123', task_id: 'task_123' }, values: { title: 'Mock Task', status: 'completed' }, } as any); const result = await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', record_data: { values: { status: 'completed' } }, }); expect(result.id.task_id).toBe('task_123'); expect(result.values.title).toBe('Mock Task'); }); it('should convert task to record in normal mode', async () => { // Only existence check needed here; transformation covered elsewhere vi.mocked(tasks.getTask).mockResolvedValue({ id: { task_id: 'task_123' }, } as any); mockCreateService.updateTask.mockResolvedValue({ id: { record_id: 'task_123', task_id: 'task_123' }, values: {}, } as any); const result = await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'task_123', record_data: { values: { status: 'completed' } }, }); expect(result.id.record_id).toBe('task_123'); }); it('should return 404 when task not found', async () => { // For this test, we want to test the real API path, so disable mock mode vi.mocked(shouldUseMockData).mockReturnValue(false); vi.mocked(tasks.getTask).mockRejectedValue( Object.assign(new Error('Not found'), { response: { status: 404 } }) ); await expect( UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'non-existent-task', record_data: { content: 'New content' }, }) ).rejects.toMatchObject({ status: 404 }); expect(tasks.getTask).toHaveBeenCalledWith('non-existent-task'); }); it('should return 404 when task existence check fails with generic error', async () => { // For this test, we want to test the real API path, so disable mock mode vi.mocked(shouldUseMockData).mockReturnValue(false); vi.mocked(tasks.getTask).mockRejectedValue(new Error('Network timeout')); await expect( UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'network-error-task', record_data: { content: 'New content' }, }) ).rejects.toMatchObject({ status: 404 }); }); it('should validate immutability for existing task content updates', async () => { // For this test, we want to test the real API path for immutability validation vi.mocked(shouldUseMockData).mockReturnValue(false); vi.mocked(mapRecordFields).mockReturnValue({ mapped: { content: 'Updated content' }, warnings: [], errors: [], } as any); vi.mocked(tasks.getTask).mockResolvedValue({ id: { task_id: 'existing-task' }, content: 'Original content', status: 'pending', } as any); vi.mocked(tasks.updateTask).mockResolvedValue({ id: { task_id: 'existing-task' }, content: 'Updated content', } as any); await expect( UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.TASKS, record_id: 'existing-task', record_data: { content: 'Updated content' }, }) ).rejects.toThrow(/Task content cannot be updated|Content is immutable/); expect(tasks.getTask).toHaveBeenCalledWith('existing-task'); expect(tasks.updateTask).not.toHaveBeenCalled(); }); }); describe('Resource Type Edge Cases', () => { it('should handle unsupported resource types gracefully', async () => { await expect( UniversalUpdateService.updateRecord({ resource_type: 'UNSUPPORTED' as UniversalResourceType, record_id: 'test-record', record_data: { name: 'Test' }, }) ).rejects.toMatchObject({ name: 'UniversalValidationError', httpStatusCode: 400, }); }); it('should handle empty record data', async () => { // Reset the updateCompany mock from previous tests const { updateCompany } = await import( '../../src/objects/companies/index.js' ); vi.mocked(updateCompany).mockResolvedValue({ id: { record_id: 'comp_123' }, values: {}, } as any); vi.mocked(validateFields).mockReturnValue({ warnings: [], suggestions: [], } as any); vi.mocked(mapRecordFields).mockResolvedValue({ mapped: {}, warnings: [], errors: [], }); const result = await UniversalUpdateService.updateRecord({ resource_type: UniversalResourceType.COMPANIES, record_id: 'test-company', record_data: {}, }); expect(result).toBeDefined(); expect(result.id).toMatchObject({ record_id: 'comp_123' }); }); }); });

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