UNPKG

npmplus-mcp-server

Version:

Production-ready MCP server for intelligent JavaScript package management. Works with Claude, Windsurf, Cursor, VS Code, and any MCP-compatible AI editor.

462 lines (379 loc) 16.5 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { PackageService } from '../../services/PackageService.js'; import { CacheService } from '../../services/CacheService.js'; import { PackageManagerService } from '../../services/PackageManagerService.js'; import { SecurityService } from '../../services/SecurityService.js'; import { RegistryService } from '../../services/RegistryService.js'; import { PackageSearchResult, Package, PackageInstallRequest, PackageOperationResult } from '../../models/Package.js'; // Mock all dependencies jest.mock('../../services/CacheService.js'); jest.mock('../../services/PackageManagerService.js'); jest.mock('../../services/SecurityService.js'); jest.mock('../../services/RegistryService.js'); const MockCacheService = CacheService as jest.MockedClass<typeof CacheService>; const MockPackageManagerService = PackageManagerService as jest.MockedClass<typeof PackageManagerService>; const MockSecurityService = SecurityService as jest.MockedClass<typeof SecurityService>; const MockRegistryService = RegistryService as jest.MockedClass<typeof RegistryService>; describe('PackageService', () => { let packageService: PackageService; let mockCacheService: jest.Mocked<CacheService>; let mockPackageManagerService: jest.Mocked<PackageManagerService>; let mockSecurityService: jest.Mocked<SecurityService>; let mockRegistryService: jest.Mocked<RegistryService>; const mockSearchResults: PackageSearchResult[] = [ { name: 'lodash', version: '4.17.21', description: 'A modern JavaScript utility library', keywords: ['util', 'functional'], author: { name: 'John Doe', email: 'john@example.com' }, publishedAt: '2021-02-20T16:20:33.919Z', score: { final: 0.8, detail: { quality: 0.9, popularity: 0.7, maintenance: 0.8 } }, searchScore: 100000.5 } ]; const mockPackage: Package = { name: 'lodash', version: '4.17.21', description: 'A modern JavaScript utility library', keywords: ['util', 'functional'], homepage: 'https://lodash.com', repository: { type: 'git', url: 'https://github.com/lodash/lodash.git' }, license: 'MIT', author: { name: 'John Doe', email: 'john@example.com' }, dependencies: { 'some-dep': '^1.0.0' }, publishedAt: '2021-02-20T16:20:33.919Z' }; const mockOperationResult: PackageOperationResult = { success: true, packages: ['lodash'], operation: 'install', output: 'added 1 package', duration: 1500, packageManager: 'npm' }; beforeEach(() => { // Create mock instances mockCacheService = new MockCacheService() as jest.Mocked<CacheService>; mockPackageManagerService = new MockPackageManagerService() as jest.Mocked<PackageManagerService>; mockSecurityService = new MockSecurityService() as jest.Mocked<SecurityService>; mockRegistryService = new MockRegistryService() as jest.Mocked<RegistryService>; packageService = new PackageService( mockCacheService, mockPackageManagerService, mockSecurityService, mockRegistryService ); // Reset all mocks jest.clearAllMocks(); }); describe('constructor', () => { it('should create instance with required dependencies', () => { expect(packageService).toBeInstanceOf(PackageService); }); }); describe('searchPackages', () => { beforeEach(() => { mockRegistryService.search.mockResolvedValue(mockSearchResults); }); it('should return cached results if available', async () => { mockCacheService.get.mockResolvedValue(mockSearchResults); const results = await packageService.searchPackages('lodash'); expect(mockCacheService.get).toHaveBeenCalledWith('search:lodash:25:0'); expect(mockRegistryService.search).not.toHaveBeenCalled(); expect(results).toEqual(mockSearchResults); }); it('should search registry when no cache available', async () => { mockCacheService.get.mockResolvedValue(null); const results = await packageService.searchPackages('lodash'); expect(mockCacheService.get).toHaveBeenCalledWith('search:lodash:25:0'); expect(mockRegistryService.search).toHaveBeenCalledWith('lodash', 25, 0); expect(mockCacheService.set).toHaveBeenCalledWith('search:lodash:25:0', mockSearchResults, 900); expect(results).toEqual(mockSearchResults); }); it('should support custom limit and from parameters', async () => { mockCacheService.get.mockResolvedValue(null); await packageService.searchPackages('react', 10, 5); expect(mockCacheService.get).toHaveBeenCalledWith('search:react:10:5'); expect(mockRegistryService.search).toHaveBeenCalledWith('react', 10, 5); expect(mockCacheService.set).toHaveBeenCalledWith('search:react:10:5', mockSearchResults, 900); }); it('should use default parameters when none provided', async () => { mockCacheService.get.mockResolvedValue(null); await packageService.searchPackages('test'); expect(mockRegistryService.search).toHaveBeenCalledWith('test', 25, 0); }); }); describe('getPackageInfo', () => { const mockDownloadStats = { downloads: 50000000, period: 'last-week' as const, start: '2023-01-01', end: '2023-01-07' }; const mockBundleSize = { size: 24000, gzip: 7000, dependency: 5, dependencyCount: 5 }; const mockSecurityInfo = { vulnerabilities: [], hasVulnerabilities: false, severity: 'info' as const }; beforeEach(() => { mockRegistryService.getPackageInfo.mockResolvedValue(mockPackage); mockRegistryService.getDownloadStats.mockResolvedValue(mockDownloadStats); mockRegistryService.getBundleSize.mockResolvedValue(mockBundleSize); mockSecurityService.checkVulnerabilities.mockResolvedValue(mockSecurityInfo); }); it('should return cached package info if available', async () => { const cachedPackage = { ...mockPackage, downloadStats: mockDownloadStats }; mockCacheService.get.mockResolvedValue(cachedPackage); const result = await packageService.getPackageInfo('lodash'); expect(mockCacheService.get).toHaveBeenCalledWith('package:lodash:latest'); expect(mockRegistryService.getPackageInfo).not.toHaveBeenCalled(); expect(result).toEqual(cachedPackage); }); it('should fetch and enhance package info when not cached', async () => { mockCacheService.get.mockResolvedValue(null); const result = await packageService.getPackageInfo('lodash'); expect(mockRegistryService.getPackageInfo).toHaveBeenCalledWith('lodash', undefined); expect(mockRegistryService.getDownloadStats).toHaveBeenCalledWith('lodash', 'last-week'); expect(mockRegistryService.getBundleSize).toHaveBeenCalledWith('lodash', '4.17.21'); expect(mockSecurityService.checkVulnerabilities).toHaveBeenCalledWith('lodash', '4.17.21'); expect(result).toEqual({ ...mockPackage, downloadStats: mockDownloadStats, bundleSize: mockBundleSize, securityInfo: mockSecurityInfo }); expect(mockCacheService.set).toHaveBeenCalledWith( 'package:lodash:latest', expect.objectContaining({ ...mockPackage, downloadStats: mockDownloadStats, bundleSize: mockBundleSize, securityInfo: mockSecurityInfo }), 3600 ); }); it('should support specific version requests', async () => { mockCacheService.get.mockResolvedValue(null); await packageService.getPackageInfo('lodash', '4.17.20'); expect(mockCacheService.get).toHaveBeenCalledWith('package:lodash:4.17.20'); expect(mockRegistryService.getPackageInfo).toHaveBeenCalledWith('lodash', '4.17.20'); }); it('should handle errors in enhancement data gracefully', async () => { mockCacheService.get.mockResolvedValue(null); mockRegistryService.getDownloadStats.mockRejectedValue(new Error('Download stats failed')); mockRegistryService.getBundleSize.mockRejectedValue(new Error('Bundle size failed')); mockSecurityService.checkVulnerabilities.mockRejectedValue(new Error('Security check failed')); const result = await packageService.getPackageInfo('lodash'); expect(result).toEqual({ ...mockPackage, downloadStats: undefined, bundleSize: undefined, securityInfo: undefined }); }); }); describe('installPackages', () => { beforeEach(() => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.install.mockResolvedValue(mockOperationResult); }); it('should install packages with detected package manager', async () => { const request: PackageInstallRequest = { packages: ['lodash', 'axios'], cwd: '/test/project' }; const result = await packageService.installPackages(request); expect(mockPackageManagerService.detectPackageManager).toHaveBeenCalledWith('/test/project'); expect(mockPackageManagerService.install).toHaveBeenCalledWith( ['lodash', 'axios'], '/test/project', undefined, undefined, 'npm' ); expect(result).toEqual(mockOperationResult); }); it('should use specified package manager instead of detecting', async () => { const request: PackageInstallRequest = { packages: ['react'], cwd: '/test/project', packageManager: 'yarn' }; await packageService.installPackages(request); expect(mockPackageManagerService.detectPackageManager).not.toHaveBeenCalled(); expect(mockPackageManagerService.install).toHaveBeenCalledWith( ['react'], '/test/project', undefined, undefined, 'yarn' ); }); it('should pass through dev and global options', async () => { const request: PackageInstallRequest = { packages: ['typescript'], cwd: '/test/project', dev: true, global: false }; await packageService.installPackages(request); expect(mockPackageManagerService.install).toHaveBeenCalledWith( ['typescript'], '/test/project', true, false, 'npm' ); }); }); describe('updatePackages', () => { beforeEach(() => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.update.mockResolvedValue(mockOperationResult); }); it('should update packages with detected package manager', async () => { const result = await packageService.updatePackages(['lodash'], '/test/project'); expect(mockPackageManagerService.detectPackageManager).toHaveBeenCalledWith('/test/project'); expect(mockPackageManagerService.update).toHaveBeenCalledWith(['lodash'], '/test/project', 'npm'); expect(result).toEqual(mockOperationResult); }); it('should use specified package manager', async () => { await packageService.updatePackages(['lodash'], '/test/project', 'yarn'); expect(mockPackageManagerService.detectPackageManager).not.toHaveBeenCalled(); expect(mockPackageManagerService.update).toHaveBeenCalledWith(['lodash'], '/test/project', 'yarn'); }); it('should handle undefined packages', async () => { await packageService.updatePackages(undefined, '/test/project'); expect(mockPackageManagerService.update).toHaveBeenCalledWith(undefined, '/test/project', 'npm'); }); }); describe('removePackages', () => { beforeEach(() => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.remove.mockResolvedValue(mockOperationResult); }); it('should remove packages with detected package manager', async () => { const result = await packageService.removePackages(['lodash'], '/test/project'); expect(mockPackageManagerService.detectPackageManager).toHaveBeenCalledWith('/test/project'); expect(mockPackageManagerService.remove).toHaveBeenCalledWith( ['lodash'], '/test/project', undefined, 'npm' ); expect(result).toEqual(mockOperationResult); }); it('should pass through global option', async () => { await packageService.removePackages(['lodash'], '/test/project', true, 'yarn'); expect(mockPackageManagerService.detectPackageManager).not.toHaveBeenCalled(); expect(mockPackageManagerService.remove).toHaveBeenCalledWith( ['lodash'], '/test/project', true, 'yarn' ); }); }); describe('checkOutdated', () => { beforeEach(() => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.checkOutdated.mockResolvedValue(mockOperationResult); }); it('should check outdated packages with detected package manager', async () => { const result = await packageService.checkOutdated('/test/project'); expect(mockPackageManagerService.detectPackageManager).toHaveBeenCalledWith('/test/project'); expect(mockPackageManagerService.checkOutdated).toHaveBeenCalledWith( '/test/project', undefined, 'npm' ); expect(result).toEqual(mockOperationResult); }); it('should pass through global option', async () => { await packageService.checkOutdated('/test/project', true, 'yarn'); expect(mockPackageManagerService.checkOutdated).toHaveBeenCalledWith('/test/project', true, 'yarn'); }); }); describe('auditDependencies', () => { beforeEach(() => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.audit.mockResolvedValue(mockOperationResult); }); it('should audit dependencies with detected package manager', async () => { const result = await packageService.auditDependencies('/test/project'); expect(mockPackageManagerService.detectPackageManager).toHaveBeenCalledWith('/test/project'); expect(mockPackageManagerService.audit).toHaveBeenCalledWith( '/test/project', undefined, undefined, undefined, 'npm' ); expect(result).toEqual(mockOperationResult); }); it('should pass through audit options', async () => { await packageService.auditDependencies('/test/project', true, false, true, 'yarn'); expect(mockPackageManagerService.audit).toHaveBeenCalledWith( '/test/project', true, false, true, 'yarn' ); }); }); describe('cleanCache', () => { beforeEach(() => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.cleanCache.mockResolvedValue(mockOperationResult); }); it('should clean cache with detected package manager', async () => { const result = await packageService.cleanCache('/test/project'); expect(mockPackageManagerService.detectPackageManager).toHaveBeenCalledWith('/test/project'); expect(mockPackageManagerService.cleanCache).toHaveBeenCalledWith( '/test/project', undefined, 'npm' ); expect(result).toEqual(mockOperationResult); }); it('should pass through global option', async () => { await packageService.cleanCache('/test/project', true, 'yarn'); expect(mockPackageManagerService.cleanCache).toHaveBeenCalledWith('/test/project', true, 'yarn'); }); }); describe('error handling', () => { it('should propagate registry service errors', async () => { mockCacheService.get.mockResolvedValue(null); mockRegistryService.search.mockRejectedValue(new Error('Registry error')); await expect(packageService.searchPackages('test')).rejects.toThrow('Registry error'); }); it('should propagate package manager service errors', async () => { mockPackageManagerService.detectPackageManager.mockResolvedValue('npm'); mockPackageManagerService.install.mockRejectedValue(new Error('Install error')); const request: PackageInstallRequest = { packages: ['test-package'], cwd: '/test/project' }; await expect(packageService.installPackages(request)).rejects.toThrow('Install error'); }); }); });