Skip to main content
Glama

SFCC Development MCP Server

by taurgis
base-handler.test.ts•19.2 kB
import { BaseToolHandler, HandlerContext, ToolArguments, ToolExecutionResult, HandlerError, GenericToolSpec, ToolExecutionContext } from '../src/core/handlers/base-handler.js'; import { Logger } from '../src/utils/logger.js'; // Mock implementation for testing class TestHandler extends BaseToolHandler { public initializeCalled = false; public disposeCalled = false; public initializeError: Error | null = null; public disposeError: Error | null = null; private toolConfig: Record<string, GenericToolSpec> = { 'test_tool': { exec: async (args) => `test_tool executed successfully with ${JSON.stringify(args)}`, logMessage: () => 'Testing tool execution', }, 'failing_tool': { exec: async () => { throw new Error('Test operation failed'); }, logMessage: () => 'Testing failing tool', }, 'validate_tool': { validate: (args, toolName) => { this.validateArgs(args, ['required_field'], toolName); }, exec: async (args) => `validate_tool executed successfully with ${JSON.stringify(args)}`, logMessage: () => 'Testing validation tool', }, 'defaults_tool': { defaults: (args) => ({ ...args, defaultValue: args.defaultValue ?? 'default_applied', numericDefault: args.numericDefault ?? 42, }), exec: async (args) => ({ receivedArgs: args }), logMessage: (args) => `Defaults tool with ${JSON.stringify(args)}`, }, 'context_tool': { exec: async (args, context) => ({ hasContext: !!context, hasHandlerContext: !!context.handlerContext, hasLogger: !!context.logger, contextKeys: Object.keys(context), }), logMessage: () => 'Testing execution context', }, 'complex_validation_tool': { validate: (args, toolName) => { if (!args.email?.includes('@')) { throw new HandlerError('Invalid email format', toolName, 'VALIDATION_ERROR'); } if (!args.age || args.age < 18) { throw new HandlerError('Age must be 18 or older', toolName, 'AGE_VALIDATION_ERROR'); } }, exec: async (args) => ({ validated: true, args }), logMessage: () => 'Testing complex validation', }, }; constructor(context: HandlerContext, subLoggerName: string = 'Test') { super(context, subLoggerName); } protected getToolConfig(): Record<string, GenericToolSpec> { return this.toolConfig; } protected getToolNameSet(): Set<string> { return new Set(['test_tool', 'failing_tool', 'validate_tool', 'defaults_tool', 'context_tool', 'complex_validation_tool']); } protected async createExecutionContext(): Promise<ToolExecutionContext> { return { handlerContext: this.context, logger: this.logger, }; } protected async onInitialize(): Promise<void> { this.initializeCalled = true; if (this.initializeError) { throw this.initializeError; } } protected async onDispose(): Promise<void> { this.disposeCalled = true; if (this.disposeError) { throw this.disposeError; } } // Expose protected methods for testing public testValidateArgs(args: ToolArguments, required: string[], toolName: string): void { this.validateArgs(args, required, toolName); } public testCreateResponse(data: any, stringify: boolean = true): ToolExecutionResult { return this.createResponse(data, stringify); } public getIsInitialized(): boolean { return (this as any)._isInitialized; } public getContext(): HandlerContext { return this.context; } } describe('BaseToolHandler', () => { let mockLogger: jest.Mocked<Logger>; let context: HandlerContext; let handler: TestHandler; beforeEach(() => { mockLogger = { debug: jest.fn(), log: jest.fn(), error: jest.fn(), timing: jest.fn(), methodEntry: jest.fn(), methodExit: jest.fn(), } as any; jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); context = { logger: mockLogger, config: { hostname: 'test.demandware.net' }, capabilities: { canAccessLogs: true, canAccessOCAPI: true }, }; handler = new TestHandler(context); }); afterEach(() => { jest.restoreAllMocks(); }); describe('constructor', () => { it('should initialize with context and logger', () => { expect(handler.getContext()).toBe(context); expect(Logger.getChildLogger).toHaveBeenCalledWith('Handler:Test'); expect(handler.getIsInitialized()).toBe(false); }); }); describe('initialization lifecycle', () => { it('should initialize on first use', async () => { expect(handler.initializeCalled).toBe(false); expect(handler.getIsInitialized()).toBe(false); const result = await handler.handle('test_tool', {}, Date.now()); expect(handler.initializeCalled).toBe(true); expect(handler.getIsInitialized()).toBe(true); expect(result.content[0].text).toContain('test_tool executed successfully'); }); it('should not initialize twice', async () => { // First call await handler.handle('test_tool', {}, Date.now()); expect(handler.initializeCalled).toBe(true); // Reset the flag to test it's not called again handler.initializeCalled = false; // Second call await handler.handle('test_tool', {}, Date.now()); expect(handler.initializeCalled).toBe(false); // Should not be called again expect(handler.getIsInitialized()).toBe(true); }); it('should handle initialization errors', async () => { handler.initializeError = new Error('Initialization failed'); const result = await handler.handle('test_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Initialization failed'); expect(handler.getIsInitialized()).toBe(false); }); }); describe('disposal lifecycle', () => { it('should dispose properly', async () => { // Initialize first await handler.handle('test_tool', {}, Date.now()); expect(handler.getIsInitialized()).toBe(true); // Dispose await handler.dispose(); expect(handler.disposeCalled).toBe(true); expect(handler.getIsInitialized()).toBe(false); }); it('should handle disposal errors', async () => { handler.disposeError = new Error('Disposal failed'); await expect(handler.dispose()).rejects.toThrow('Disposal failed'); expect(handler.getIsInitialized()).toBe(false); // Should still reset the flag }); it('should allow disposal without initialization', async () => { expect(handler.getIsInitialized()).toBe(false); await handler.dispose(); expect(handler.disposeCalled).toBe(true); expect(handler.getIsInitialized()).toBe(false); }); }); describe('canHandle', () => { it('should correctly identify handled tools', () => { expect(handler.canHandle('test_tool')).toBe(true); expect(handler.canHandle('failing_tool')).toBe(true); expect(handler.canHandle('validate_tool')).toBe(true); expect(handler.canHandle('unknown_tool')).toBe(false); }); }); describe('handle method', () => { it('should execute tool successfully', async () => { const startTime = Date.now(); const args = { param1: 'value1' }; const result = await handler.handle('test_tool', args, startTime); expect(result.content[0].text).toContain('test_tool executed successfully'); expect(result.content[0].text).toContain('value1'); expect(mockLogger.timing).toHaveBeenCalledWith('test_tool', startTime); }); it('should handle tool execution errors', async () => { const result = await handler.handle('failing_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Test operation failed'); }); it('should validate arguments when required', async () => { const args = { required_field: 'value' }; // Should pass with required field const result = await handler.handle('validate_tool', args, Date.now()); expect(result.content[0].text).toContain('validate_tool executed successfully'); // Should fail without required field const errorResult = await handler.handle('validate_tool', {}, Date.now()); expect(errorResult.isError).toBe(true); expect(errorResult.content[0].text).toContain('required_field is required'); }); }); describe('validateArgs', () => { it('should pass when all required fields are present', () => { const args = { field1: 'value1', field2: 'value2' }; expect(() => { handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); }).not.toThrow(); }); it('should throw HandlerError when required field is missing', () => { const args = { field1: 'value1' }; expect(() => { handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); }).toThrow(HandlerError); try { handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); } catch (error) { expect(error).toBeInstanceOf(HandlerError); expect((error as HandlerError).message).toBe('field2 is required'); expect((error as HandlerError).toolName).toBe('test_tool'); expect((error as HandlerError).code).toBe('MISSING_ARGUMENT'); expect((error as HandlerError).details).toEqual({ required: ['field1', 'field2'], provided: ['field1'], }); } }); it('should throw when required field is null or undefined', () => { expect(() => { handler.testValidateArgs({ field1: null }, ['field1'], 'test_tool'); }).toThrow('field1 is required'); expect(() => { handler.testValidateArgs({ field1: undefined }, ['field1'], 'test_tool'); }).toThrow('field1 is required'); }); it('should handle empty args object', () => { expect(() => { handler.testValidateArgs({}, ['field1'], 'test_tool'); }).toThrow('field1 is required'); }); it('should handle null args', () => { expect(() => { handler.testValidateArgs(null as any, ['field1'], 'test_tool'); }).toThrow('field1 is required'); }); }); describe('createResponse', () => { it('should create stringified response by default', () => { const data = { key: 'value', number: 42 }; const response = handler.testCreateResponse(data); expect(response.content[0].text).toBe(JSON.stringify(data, null, 2)); expect(response.isError).toBe(false); }); it('should create non-stringified response when requested', () => { const data = { key: 'value', number: 42 }; const response = handler.testCreateResponse(data, false); expect(response.content[0].text).toBe(data); expect(response.isError).toBe(false); }); it('should handle null data', () => { const response = handler.testCreateResponse(null); expect(response.content[0].text).toBe('null'); expect(response.isError).toBe(false); }); it('should handle primitive values', () => { expect(handler.testCreateResponse('string').content[0].text).toBe('"string"'); expect(handler.testCreateResponse(42).content[0].text).toBe('42'); expect(handler.testCreateResponse(true).content[0].text).toBe('true'); }); }); describe('HandlerError', () => { it('should create error with all properties', () => { const details = { key: 'value' }; const error = new HandlerError('Test error', 'test_tool', 'TEST_CODE', details); expect(error.message).toBe('Test error'); expect(error.toolName).toBe('test_tool'); expect(error.code).toBe('TEST_CODE'); expect(error.details).toBe(details); expect(error.name).toBe('HandlerError'); expect(error).toBeInstanceOf(Error); }); it('should create error with optional parameters', () => { const error = new HandlerError('Simple error', 'test_tool'); expect(error.message).toBe('Simple error'); expect(error.toolName).toBe('test_tool'); expect(error.code).toBe('HANDLER_ERROR'); expect(error.details).toBeUndefined(); }); }); describe('logging integration', () => { it('should log debug messages during execution', async () => { await handler.handle('test_tool', {}, Date.now()); expect(mockLogger.debug).toHaveBeenCalledWith( 'test_tool completed successfully', expect.any(Object), ); }); it('should log timing information', async () => { const startTime = Date.now(); await handler.handle('test_tool', {}, startTime); expect(mockLogger.timing).toHaveBeenCalledWith('test_tool', startTime); }); }); describe('error handling', () => { it('should preserve original error types', async () => { const customError = new TypeError('Custom type error'); handler.initializeError = customError; const result = await handler.handle('test_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Custom type error'); }); it('should handle async operation errors', async () => { const result = await handler.handle('failing_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Test operation failed'); }); }); describe('config-driven functionality', () => { describe('unsupported tools', () => { it('should throw error for unsupported tools', async () => { await expect(handler.handle('unknown_tool', {}, Date.now())) .rejects.toThrow('Unsupported tool: unknown_tool'); }); it('should return false for canHandle on unsupported tools', () => { expect(handler.canHandle('unknown_tool')).toBe(false); }); }); describe('default values', () => { it('should apply default values when not provided', async () => { const result = await handler.handle('defaults_tool', {}, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('default_applied'); expect(parsedResult.receivedArgs.numericDefault).toBe(42); }); it('should not override provided values with defaults', async () => { const args = { defaultValue: 'custom_value', numericDefault: 100 }; const result = await handler.handle('defaults_tool', args, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('custom_value'); expect(parsedResult.receivedArgs.numericDefault).toBe(100); }); it('should mix provided and default values', async () => { const args = { defaultValue: 'custom_value', otherParam: 'other' }; const result = await handler.handle('defaults_tool', args, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('custom_value'); expect(parsedResult.receivedArgs.numericDefault).toBe(42); // default applied expect(parsedResult.receivedArgs.otherParam).toBe('other'); }); }); describe('execution context', () => { it('should provide ToolExecutionContext to tool functions', async () => { const result = await handler.handle('context_tool', {}, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.hasContext).toBe(true); expect(parsedResult.hasHandlerContext).toBe(true); expect(parsedResult.hasLogger).toBe(true); expect(parsedResult.contextKeys).toContain('handlerContext'); expect(parsedResult.contextKeys).toContain('logger'); }); }); describe('complex validation', () => { it('should pass complex validation with valid args', async () => { const args = { email: 'test@example.com', age: 25 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.validated).toBe(true); expect(parsedResult.args).toEqual(args); }); it('should fail validation with invalid email', async () => { const args = { email: 'invalid-email', age: 25 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid email format'); }); it('should fail validation with invalid age', async () => { const args = { email: 'test@example.com', age: 16 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Age must be 18 or older'); }); it('should handle validation errors with custom error codes', async () => { const args = { email: 'invalid-email', age: 25 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid email format'); // The error should be a HandlerError with VALIDATION_ERROR code }); }); describe('logging with defaults', () => { it('should use log message with applied defaults', async () => { const args = { customParam: 'test' }; // Spy on the debug method to capture log messages const debugSpy = jest.spyOn(mockLogger, 'debug'); await handler.handle('defaults_tool', args, Date.now()); // Check that the log message includes the default values const debugCalls = debugSpy.mock.calls; const logMessageCall = debugCalls.find(call => typeof call[0] === 'string' && call[0].includes('Defaults tool with'), ); expect(logMessageCall).toBeDefined(); expect(logMessageCall?.[0]).toContain('default_applied'); expect(logMessageCall?.[0]).toContain('42'); }); }); describe('tool config edge cases', () => { it('should handle tools with minimal config', async () => { // The test_tool has minimal config - no validation, no defaults const result = await handler.handle('test_tool', { param: 'value' }, Date.now()); expect(result.content[0].text).toContain('test_tool executed successfully'); expect(result.content[0].text).toContain('value'); }); it('should handle empty arguments with defaults', async () => { const result = await handler.handle('defaults_tool', {}, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('default_applied'); expect(parsedResult.receivedArgs.numericDefault).toBe(42); }); }); }); });

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/taurgis/sfcc-dev-mcp'

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