UNPKG

sfcc-dev-mcp

Version:

MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools

434 lines (349 loc) 16.4 kB
import { SFCCLogClient } from '../src/clients/log-client'; import { SFCCConfig, LogLevel } from '../src/types/types'; // Use manual mock for webdav // eslint-disable-next-line @typescript-eslint/no-require-imports const webdav = require('webdav'); const mockWebdavClient = webdav.__mockWebdavClient; // Mock the logger jest.mock('../src/utils/logger', () => ({ Logger: jest.fn().mockImplementation(() => ({ methodEntry: jest.fn(), methodExit: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn(), timing: jest.fn(), })), })); // Mock utils jest.mock('../src/utils/utils', () => ({ getCurrentDate: jest.fn(() => '20240809'), formatBytes: jest.fn((bytes: number) => `${bytes} bytes`), parseLogEntries: jest.fn((content: string, level: string) => { // Simple mock implementation - split by lines and filter by level return content.split('\n').filter(line => line.includes(level)); }), extractUniqueErrors: jest.fn((errors: string[]) => { // Mock implementation that returns unique error messages return [...new Set(errors.map(error => error.split(' ').slice(0, 5).join(' ')))]; }), normalizeFilePath: jest.fn((path: string) => path), })); describe('SFCCLogClient', () => { let logClient: SFCCLogClient; let config: SFCCConfig; beforeEach(() => { // Setup test config config = { hostname: 'test.demandware.net', username: 'testuser', password: 'testpass', }; logClient = new SFCCLogClient(config); }); afterEach(() => { jest.clearAllMocks(); }); describe('constructor', () => { it('should create client with username/password authentication', () => { const config: SFCCConfig = { hostname: 'test.demandware.net', username: 'testuser', password: 'testpass', }; new SFCCLogClient(config); expect(webdav.createClient).toHaveBeenCalledWith( 'https://test.demandware.net/on/demandware.servlet/webdav/Sites/Logs/', { username: 'testuser', password: 'testpass', }, ); }); it('should create client with OAuth authentication', () => { const config: SFCCConfig = { hostname: 'test.demandware.net', clientId: 'testclientid', clientSecret: 'testclientsecret', }; new SFCCLogClient(config); expect(webdav.createClient).toHaveBeenCalledWith( 'https://test.demandware.net/on/demandware.servlet/webdav/Sites/Logs/', { username: 'testclientid', password: 'testclientsecret', }, ); }); it('should throw error when no authentication provided', () => { const config: SFCCConfig = { hostname: 'test.demandware.net', }; expect(() => new SFCCLogClient(config)).toThrow( 'Either username/password or clientId/clientSecret must be provided', ); }); }); describe('getLogFiles', () => { it('should return log files for specified date', async () => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log' }, { type: 'file', filename: 'warn-20240809-blade1-001.log' }, { type: 'file', filename: 'info-20240809-blade1-001.log' }, { type: 'file', filename: 'debug-20240809-blade1-001.log' }, { type: 'file', filename: 'error-20240808-blade1-001.log' }, // Different date { type: 'directory', filename: 'somedir' }, // Directory { type: 'file', filename: 'not-a-log.txt' }, // Not a log file ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); const result = await logClient.getLogFiles('20240809'); expect(result).toEqual([ 'error-20240809-blade1-001.log', 'warn-20240809-blade1-001.log', 'info-20240809-blade1-001.log', 'debug-20240809-blade1-001.log', ]); expect(mockWebdavClient.getDirectoryContents).toHaveBeenCalledWith('/'); }); it('should use current date when no date provided', async () => { mockWebdavClient.getDirectoryContents.mockResolvedValue([]); await logClient.getLogFiles(); // Should call getCurrentDate() from utils which returns '20240809' expect(mockWebdavClient.getDirectoryContents).toHaveBeenCalledWith('/'); }); it('should return empty array when no log files found', async () => { mockWebdavClient.getDirectoryContents.mockResolvedValue([]); const result = await logClient.getLogFiles('20240809'); expect(result).toEqual([]); }); }); describe('getLatestLogs', () => { beforeEach(() => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log' }, { type: 'file', filename: 'error-20240809-blade1-002.log' }, { type: 'file', filename: 'warn-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); }); it('should return latest error logs', async () => { const mockLogContent = 'ERROR Line 1\nERROR Line 2\nERROR Line 3\nERROR Line 4\nERROR Line 5'; mockWebdavClient.getFileContents.mockResolvedValue(mockLogContent); const result = await logClient.getLatestLogs('error' as LogLevel, 3, '20240809'); expect(result).toContain('Latest 3 error messages'); expect(result).toContain('error-20240809-blade1-002.log'); // Should use the latest file expect(mockWebdavClient.getFileContents).toHaveBeenCalledWith( 'error-20240809-blade1-002.log', { format: 'text' }, ); }); it('should return warning when no files found for level', async () => { const mockContents = [ { type: 'file', filename: 'info-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); const result = await logClient.getLatestLogs('error' as LogLevel, 10, '20240809'); expect(result).toContain('No error log files found'); expect(result).toContain('Available files: info-20240809-blade1-001.log'); }); it('should handle different log levels', async () => { const mockLogContent = 'WARN Line 1\nWARN Line 2'; mockWebdavClient.getFileContents.mockResolvedValue(mockLogContent); await logClient.getLatestLogs('warn' as LogLevel, 2, '20240809'); expect(mockWebdavClient.getFileContents).toHaveBeenCalledWith( 'warn-20240809-blade1-001.log', { format: 'text' }, ); }); it('should return latest debug logs', async () => { const mockContents = [ { type: 'file', filename: 'debug-20240809-blade1-001.log' }, { type: 'file', filename: 'debug-20240809-blade1-002.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); const mockLogContent = 'DEBUG Line 1\nDEBUG Line 2\nDEBUG Line 3'; mockWebdavClient.getFileContents.mockResolvedValue(mockLogContent); const result = await logClient.getLatestLogs('debug' as LogLevel, 2, '20240809'); expect(result).toContain('Latest 2 debug messages'); expect(result).toContain('debug-20240809-blade1-002.log'); // Should use the latest file expect(mockWebdavClient.getFileContents).toHaveBeenCalledWith( 'debug-20240809-blade1-002.log', { format: 'text' }, ); }); }); describe('summarizeLogs', () => { it('should return summary when no log files found', async () => { mockWebdavClient.getDirectoryContents.mockResolvedValue([]); const result = await logClient.summarizeLogs('20240809'); expect(result).toBe('No log files found for date 20240809'); }); it('should generate comprehensive log summary', async () => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log' }, { type: 'file', filename: 'warn-20240809-blade1-001.log' }, { type: 'file', filename: 'info-20240809-blade1-001.log' }, { type: 'file', filename: 'debug-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); const mockErrorContent = ' ERROR Something went wrong\n ERROR Another error\n INFO Some info'; const mockWarnContent = ' WARN Warning message\n INFO Info message'; const mockInfoContent = ' INFO First info\n INFO Second info\n INFO Third info'; const mockDebugContent = ' DEBUG Debug message 1\n DEBUG Debug message 2\n DEBUG Debug message 3'; mockWebdavClient.getFileContents .mockResolvedValueOnce(mockErrorContent) .mockResolvedValueOnce(mockWarnContent) .mockResolvedValueOnce(mockInfoContent) .mockResolvedValueOnce(mockDebugContent); const result = await logClient.summarizeLogs('20240809'); expect(result).toContain('Log Summary for 20240809'); expect(result).toContain('Errors: 2'); // The parseLogEntries mock filters properly expect(result).toContain('Warnings: 1'); expect(result).toContain('Info: 5'); expect(result).toContain('Debug: 3'); expect(result).toContain('Log Files (4)'); expect(result).toContain('ERROR Something went wrong'); // Key issues section expect(result).toContain('ERROR Another error'); }); it('should handle file read errors gracefully', async () => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); mockWebdavClient.getFileContents.mockRejectedValue(new Error('File read error')); const result = await logClient.summarizeLogs('20240809'); expect(result).toContain('Log Summary for 20240809'); expect(result).toContain('Errors: 0'); // Should handle the error and continue expect(result).toContain('Debug: 0'); // Should initialize debug count to 0 }); }); describe('searchLogs', () => { beforeEach(() => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log' }, { type: 'file', filename: 'warn-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); }); it('should search across all log files when no level specified', async () => { const mockErrorContent = 'ERROR: Database connection failed\nINFO: System started'; const mockWarnContent = 'WARN: Database connection slow\nINFO: Cache cleared'; mockWebdavClient.getFileContents .mockResolvedValueOnce(mockErrorContent) .mockResolvedValueOnce(mockWarnContent); const result = await logClient.searchLogs('database', undefined, 20, '20240809'); expect(result).toContain('Found 2 matches for "database"'); expect(result).toContain('Database connection failed'); expect(result).toContain('Database connection slow'); }); it('should filter by log level when specified', async () => { const mockErrorContent = 'ERROR: Database connection failed'; mockWebdavClient.getFileContents.mockResolvedValue(mockErrorContent); await logClient.searchLogs('database', 'error' as LogLevel, 20, '20240809'); expect(mockWebdavClient.getFileContents).toHaveBeenCalledTimes(1); expect(mockWebdavClient.getFileContents).toHaveBeenCalledWith( 'error-20240809-blade1-001.log', { format: 'text' }, ); }); it('should return no matches message when pattern not found', async () => { mockWebdavClient.getFileContents.mockResolvedValue('No matching content here'); const result = await logClient.searchLogs('nonexistent', undefined, 20, '20240809'); expect(result).toContain('No matches found for "nonexistent"'); }); it('should handle case-insensitive search', async () => { const mockContent = 'ERROR: DATABASE CONNECTION FAILED'; mockWebdavClient.getFileContents.mockResolvedValue(mockContent); const result = await logClient.searchLogs('database', undefined, 20, '20240809'); expect(result).toContain('Found 2 matches for "database"'); // Both files return the same content expect(result).toContain('DATABASE CONNECTION FAILED'); }); it('should respect the limit parameter', async () => { const lines = Array.from({ length: 50 }, (_, i) => `ERROR: Test error ${i}`); const mockContent = lines.join('\n'); mockWebdavClient.getFileContents.mockResolvedValue(mockContent); const result = await logClient.searchLogs('test', undefined, 10, '20240809'); // Should only return 10 matches despite 50 being available const matches = result.split('\n\n').length - 1; // Subtract header line expect(matches).toBeLessThanOrEqual(11); // 10 matches + header }); it('should filter by debug log level when specified', async () => { const mockContents = [ { type: 'file', filename: 'debug-20240809-blade1-001.log' }, { type: 'file', filename: 'error-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); const mockDebugContent = 'DEBUG: Database query executed'; mockWebdavClient.getFileContents.mockResolvedValue(mockDebugContent); const result = await logClient.searchLogs('database', 'debug' as LogLevel, 20, '20240809'); expect(mockWebdavClient.getFileContents).toHaveBeenCalledTimes(1); expect(mockWebdavClient.getFileContents).toHaveBeenCalledWith( 'debug-20240809-blade1-001.log', { format: 'text' }, ); expect(result).toContain('Database query executed'); }); }); describe('listLogFiles', () => { it('should list all log files with metadata', async () => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log', size: 1024, lastmod: '2024-08-09T10:00:00Z', }, { type: 'file', filename: 'warn-20240809-blade1-001.log', size: 2048, lastmod: '2024-08-09T11:00:00Z', }, { type: 'file', filename: 'not-a-log.txt', size: 512, lastmod: '2024-08-09T12:00:00Z', }, { type: 'directory', filename: 'somedir', }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); const result = await logClient.listLogFiles(); expect(result).toContain('Available log files:'); expect(result).toContain('error-20240809-blade1-001.log'); expect(result).toContain('warn-20240809-blade1-001.log'); expect(result).toContain('1024 bytes'); // formatBytes mock expect(result).toContain('2048 bytes'); expect(result).toContain('2024-08-09T10:00:00Z'); expect(result).not.toContain('not-a-log.txt'); // Should filter out non-log files expect(result).not.toContain('somedir'); // Should filter out directories }); it('should handle WebDAV errors', async () => { mockWebdavClient.getDirectoryContents.mockRejectedValue(new Error('Connection failed')); await expect(logClient.listLogFiles()).rejects.toThrow('Failed to list log files: Connection failed'); }); it('should handle empty directory', async () => { mockWebdavClient.getDirectoryContents.mockResolvedValue([]); const result = await logClient.listLogFiles(); expect(result).toContain('Available log files:'); expect(result.split('\n')).toHaveLength(3); // Header, empty line, and extra empty line }); }); describe('error handling', () => { it('should handle WebDAV connection errors in getLogFiles', async () => { mockWebdavClient.getDirectoryContents.mockRejectedValue(new Error('Connection timeout')); await expect(logClient.getLogFiles()).rejects.toThrow('Connection timeout'); }); it('should handle file read errors in getLatestLogs', async () => { const mockContents = [ { type: 'file', filename: 'error-20240809-blade1-001.log' }, ]; mockWebdavClient.getDirectoryContents.mockResolvedValue(mockContents); mockWebdavClient.getFileContents.mockRejectedValue(new Error('File not found')); await expect(logClient.getLatestLogs('error' as LogLevel, 10)).rejects.toThrow('File not found'); }); }); });