Skip to main content
Glama
devkindhq

Boilerplate MCP Server

by devkindhq
swell.products.service.test.ts15.8 kB
import swellProductsService from './swell.products.service.js'; import { swellClient } from '../utils/swell-client.util.js'; import { McpError } from '../utils/error.util.js'; import { SwellProduct, SwellProductsList, ProductListOptions, ProductSearchOptions, ProductGetOptions, } from './swell.products.types.js'; // Mock the swell client utility jest.mock('../utils/swell-client.util.js'); const mockSwellClient = swellClient as jest.Mocked<typeof swellClient>; // Mock axios for the swell-node client const mockAxios = { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn(), }; // Mock swell-node client const mockClient = { get: mockAxios.get, post: mockAxios.post, put: mockAxios.put, delete: mockAxios.delete, getClientStats: jest.fn(), }; describe('SwellProductsService', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); // Setup mock client mockSwellClient.isClientInitialized.mockReturnValue(true); mockSwellClient.getClient.mockReturnValue(mockClient as any); mockSwellClient.initWithAutoConfig.mockImplementation(() => {}); // Reset axios mocks mockAxios.get.mockReset(); mockAxios.post.mockReset(); mockAxios.put.mockReset(); mockAxios.delete.mockReset(); }); afterEach(() => { jest.useRealTimers(); }); describe('list', () => { const mockProductsList: SwellProductsList = { count: 2, results: [ { id: 'product-1', name: 'Test Product 1', slug: 'test-product-1', sku: 'TEST-001', active: true, price: 29.99, stock_level: 10, stock_status: 'in_stock', description: 'Test product description', images: [], date_created: '2023-01-01T00:00:00.000Z', date_updated: '2023-01-01T00:00:00.000Z', }, { id: 'product-2', name: 'Test Product 2', slug: 'test-product-2', sku: 'TEST-002', active: true, price: 49.99, stock_level: 5, stock_status: 'in_stock', description: 'Another test product', images: [], date_created: '2023-01-02T00:00:00.000Z', date_updated: '2023-01-02T00:00:00.000Z', }, ], page: 1, pages: 1, }; it('should successfully fetch products list with default options', async () => { mockAxios.get.mockResolvedValue(mockProductsList); const result = await swellProductsService.list(); expect(mockAxios.get).toHaveBeenCalledWith('/products', {}); expect(result).toEqual(mockProductsList); }); it('should fetch products list with filtering options', async () => { const options: ProductListOptions = { page: 2, limit: 10, active: true, category: 'electronics', tags: ['featured', 'sale'], search: 'laptop', sort: 'price_asc', expand: ['variants', 'categories'], }; mockAxios.get.mockResolvedValue(mockProductsList); const result = await swellProductsService.list(options); expect(mockAxios.get).toHaveBeenCalledWith('/products', { page: 2, limit: 10, active: true, category: 'electronics', tags: 'featured,sale', search: 'laptop', sort: 'price_asc', expand: 'variants,categories', }); expect(result).toEqual(mockProductsList); }); it('should initialize client if not initialized', async () => { mockSwellClient.isClientInitialized.mockReturnValue(false); mockAxios.get.mockResolvedValue(mockProductsList); await swellProductsService.list(); expect(mockSwellClient.initWithAutoConfig).toHaveBeenCalled(); }); it('should handle API errors', async () => { const apiError = new Error('API Error'); mockAxios.get.mockRejectedValue(apiError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); it('should handle Zod validation errors', async () => { const invalidResponse = { invalid: 'data' }; mockAxios.get.mockResolvedValue(invalidResponse); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); it('should handle connection errors with retry logic', async () => { const connectionError = new Error('ECONNABORTED'); (connectionError as any).code = 'ECONNABORTED'; mockAxios.get.mockRejectedValue(connectionError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); it('should handle authentication errors', async () => { const authError = new Error('Unauthorized'); (authError as any).status = 401; mockAxios.get.mockRejectedValue(authError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); }); describe('get', () => { const mockProduct: SwellProduct = { id: 'product-1', name: 'Test Product', slug: 'test-product', sku: 'TEST-001', active: true, price: 29.99, stock_level: 10, stock_status: 'in_stock', description: 'Test product description', images: [], date_created: '2023-01-01T00:00:00.000Z', date_updated: '2023-01-01T00:00:00.000Z', }; it('should successfully fetch product by ID', async () => { mockAxios.get.mockResolvedValue(mockProduct); const result = await swellProductsService.get('product-1'); expect(mockAxios.get).toHaveBeenCalledWith( '/products/product-1', {}, ); expect(result).toEqual(mockProduct); }); it('should fetch product with expand options', async () => { const options: ProductGetOptions = { expand: ['variants', 'categories'], }; mockAxios.get.mockResolvedValue(mockProduct); const result = await swellProductsService.get('product-1', options); expect(mockAxios.get).toHaveBeenCalledWith('/products/product-1', { expand: options.expand, }); expect(result).toEqual(mockProduct); }); it('should throw error for empty product ID', async () => { await expect(swellProductsService.get('')).rejects.toThrow( 'Product ID is required', ); }); it('should handle product not found', async () => { mockAxios.get.mockResolvedValue(null); await expect( swellProductsService.get('nonexistent'), ).rejects.toThrow('Product not found: nonexistent'); }); it('should handle API errors', async () => { const apiError = new Error('API Error'); mockAxios.get.mockRejectedValue(apiError); await expect(swellProductsService.get('product-1')).rejects.toThrow( McpError, ); }); it('should handle validation errors', async () => { const invalidResponse = { invalid: 'data' }; mockAxios.get.mockResolvedValue(invalidResponse); await expect(swellProductsService.get('product-1')).rejects.toThrow( McpError, ); }); }); describe('search', () => { const mockSearchResults: SwellProductsList = { count: 1, results: [ { id: 'product-1', name: 'Laptop Computer', slug: 'laptop-computer', sku: 'LAPTOP-001', active: true, price: 999.99, stock_level: 3, stock_status: 'in_stock', description: 'High-performance laptop', images: [], date_created: '2023-01-01T00:00:00.000Z', date_updated: '2023-01-01T00:00:00.000Z', }, ], page: 1, pages: 1, }; it('should successfully search products', async () => { mockAxios.get.mockResolvedValue(mockSearchResults); const options: ProductSearchOptions = { query: 'laptop', limit: 20, }; const result = await swellProductsService.search(options); expect(mockAxios.get).toHaveBeenCalledWith('/products', { search: 'laptop', limit: 20, }); expect(result).toEqual(mockSearchResults); }); it('should search with additional filters', async () => { mockAxios.get.mockResolvedValue(mockSearchResults); const options: ProductSearchOptions = { query: 'laptop', active: true, category: 'electronics', tags: ['featured'], sort: 'price_desc', expand: ['variants'], }; const result = await swellProductsService.search(options); expect(mockAxios.get).toHaveBeenCalledWith('/products', { search: 'laptop', active: true, category: 'electronics', tags: 'featured', sort: 'price_desc', expand: 'variants', }); expect(result).toEqual(mockSearchResults); }); it('should throw error for empty search query', async () => { const options: ProductSearchOptions = { query: '' }; await expect(swellProductsService.search(options)).rejects.toThrow( 'Search query is required', ); }); it('should handle search API errors', async () => { const apiError = new Error('Search API Error'); mockAxios.get.mockRejectedValue(apiError); const options: ProductSearchOptions = { query: 'laptop' }; await expect(swellProductsService.search(options)).rejects.toThrow( McpError, ); }); }); describe('checkInventory', () => { const mockProductWithInventory: SwellProduct = { id: 'product-1', name: 'Test Product', slug: 'test-product', sku: 'TEST-001', active: true, price: 29.99, stock_level: 10, stock_status: 'in_stock', description: 'Test product description', images: [], variants: [ { id: 'variant-1', name: 'Small', sku: 'TEST-001-S', price: 29.99, stock_level: 5, stock_status: 'in_stock', }, ], date_created: '2023-01-01T00:00:00.000Z', date_updated: '2023-01-01T00:00:00.000Z', }; it('should successfully check inventory for product', async () => { mockAxios.get.mockResolvedValue(mockProductWithInventory); const result = await swellProductsService.checkStock('product-1'); expect(mockAxios.get).toHaveBeenCalledWith('/products/product-1', { expand: ['stock'], }); expect(result).toEqual(mockProductWithInventory); }); it('should check inventory including variants', async () => { mockAxios.get.mockResolvedValue(mockProductWithInventory); const options = { include_variants: true, expand: ['variants', 'stock'], }; const result = await swellProductsService.checkStock( 'product-1', options, ); expect(mockAxios.get).toHaveBeenCalledWith('/products/product-1', { expand: ['variants', 'stock'], }); expect(result).toEqual(mockProductWithInventory); }); it('should throw error for empty product ID', async () => { await expect(swellProductsService.checkStock('')).rejects.toThrow( 'Product ID is required', ); }); it('should handle inventory check API errors', async () => { const apiError = new Error('Inventory API Error'); mockAxios.get.mockRejectedValue(apiError); await expect( swellProductsService.checkStock('product-1'), ).rejects.toThrow(McpError); }); }); describe('parameter validation and formatting', () => { it('should properly format array parameters', async () => { const mockResponse: SwellProductsList = { count: 0, results: [], page: 1, pages: 1, }; mockAxios.get.mockResolvedValue(mockResponse); const options: ProductListOptions = { tags: ['tag1', 'tag2', 'tag3'], expand: ['variants', 'categories', 'images'], }; await swellProductsService.list(options); expect(mockAxios.get).toHaveBeenCalledWith('/products', { tags: 'tag1,tag2,tag3', expand: 'variants,categories,images', }); }); it('should handle undefined and null parameters', async () => { const mockResponse: SwellProductsList = { count: 0, results: [], page: 1, pages: 1, }; mockAxios.get.mockResolvedValue(mockResponse); const options: ProductListOptions = { page: undefined, active: undefined, category: '', tags: [], }; await swellProductsService.list(options); // Should only include defined, non-empty parameters expect(mockAxios.get).toHaveBeenCalledWith('/products', {}); }); it('should validate string parameters are trimmed', async () => { await expect( swellProductsService.get(' \t \n '), ).rejects.toThrow('Product ID is required'); await expect( swellProductsService.search({ query: ' \t \n ' }), ).rejects.toThrow('Search query is required'); }); }); describe('client recycling behavior', () => { it('should handle client recycling during requests', async () => { const mockResponse: SwellProductsList = { count: 1, results: [ { id: 'product-1', name: 'Test Product', slug: 'test-product', sku: 'TEST-001', active: true, price: 29.99, stock_level: 10, stock_status: 'in_stock', description: 'Test product description', images: [], date_created: '2023-01-01T00:00:00.000Z', date_updated: '2023-01-01T00:00:00.000Z', }, ], page: 1, pages: 1, }; // Mock client stats to simulate recycling conditions mockClient.getClientStats.mockReturnValue({ activeClient: { createdAt: Date.now() - 20000, // 20 seconds ago activeRequests: 0, totalRequests: 1500, // Above recycling threshold ageMs: 20000, }, oldClientsCount: 0, oldClients: [], }); mockAxios.get.mockResolvedValue(mockResponse); // Advance timers to simulate time passing jest.advanceTimersByTime(16000); // Advance past recycling time threshold const result = await swellProductsService.list(); expect(result).toEqual(mockResponse); expect(mockAxios.get).toHaveBeenCalledWith('/products', {}); }); it('should handle concurrent requests during client recycling', async () => { const mockResponse: SwellProductsList = { count: 1, results: [ { id: 'product-1', name: 'Test Product', slug: 'test-product', sku: 'TEST-001', active: true, price: 29.99, stock_level: 10, stock_status: 'in_stock', description: 'Test product description', images: [], date_created: '2023-01-01T00:00:00.000Z', date_updated: '2023-01-01T00:00:00.000Z', }, ], page: 1, pages: 1, }; mockAxios.get.mockResolvedValue(mockResponse); // Simulate concurrent requests const promise1 = swellProductsService.list({ page: 1 }); const promise2 = swellProductsService.list({ page: 2 }); // Advance time during concurrent requests jest.advanceTimersByTime(1000); const [result1, result2] = await Promise.all([promise1, promise2]); expect(result1).toEqual(mockResponse); expect(result2).toEqual(mockResponse); expect(mockAxios.get).toHaveBeenCalledTimes(2); }); }); describe('error handling scenarios', () => { it('should handle ECONNREFUSED errors', async () => { const connectionError = new Error('Connection refused'); (connectionError as any).code = 'ECONNREFUSED'; mockAxios.get.mockRejectedValue(connectionError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); it('should handle timeout errors', async () => { const timeoutError = new Error('Request timeout'); (timeoutError as any).code = 'ECONNABORTED'; mockAxios.get.mockRejectedValue(timeoutError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); it('should handle 404 errors', async () => { const notFoundError = new Error('Not Found'); (notFoundError as any).status = 404; mockAxios.get.mockRejectedValue(notFoundError); await expect( swellProductsService.get('nonexistent'), ).rejects.toThrow(McpError); }); it('should handle 500 server errors', async () => { const serverError = new Error('Internal Server Error'); (serverError as any).status = 500; mockAxios.get.mockRejectedValue(serverError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); it('should handle network errors', async () => { const networkError = new Error('Network Error'); (networkError as any).code = 'NETWORK_ERROR'; mockAxios.get.mockRejectedValue(networkError); await expect(swellProductsService.list()).rejects.toThrow(McpError); }); }); });

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/devkindhq/swell-mcp'

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