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.

509 lines (429 loc) 14.8 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { RegistryService } from '../../services/RegistryService.js'; import { URLS } from '../../constants.js'; // Mock dependencies jest.mock('pacote'); jest.mock('undici', () => ({ fetch: jest.fn() })); import pacote from 'pacote'; import { fetch } from 'undici'; const mockPacote = pacote as jest.Mocked<typeof pacote>; const mockFetch = fetch as jest.MockedFunction<typeof fetch>; describe('RegistryService', () => { let registryService: RegistryService; beforeEach(() => { registryService = new RegistryService(); jest.clearAllMocks(); }); describe('constructor', () => { it('should use default URLs when none provided', () => { const service = new RegistryService(); expect(service).toBeInstanceOf(RegistryService); }); it('should accept custom URLs', () => { const customRegistry = 'https://custom-registry.com'; const customBundlephobia = 'https://custom-bundlephobia.com'; const customNpmApi = 'https://custom-npm-api.com'; const service = new RegistryService(customRegistry, customBundlephobia, customNpmApi); expect(service).toBeInstanceOf(RegistryService); }); }); describe('search', () => { const mockSearchResponse = { objects: [ { package: { name: 'lodash', version: '4.17.21', description: 'A modern JavaScript utility library', keywords: ['util', 'functional'], author: 'John Doe <john@example.com>', date: '2021-02-20T16:20:33.919Z' }, score: { final: 0.8, detail: { quality: 0.9, popularity: 0.7, maintenance: 0.8 } }, searchScore: 100000.5 } ] }; beforeEach(() => { mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve(mockSearchResponse) } as any); }); it('should search packages with default parameters', async () => { const results = await registryService.search('lodash'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining(`${URLS.NPM_API}/search`) ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('text=lodash') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('size=25') ); expect(results).toHaveLength(1); expect(results[0].name).toBe('lodash'); }); it('should search packages with custom parameters', async () => { await registryService.search('react', 10, 5); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('text=react') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('size=10') ); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('from=5') ); }); it('should transform search results correctly', async () => { const results = await registryService.search('lodash'); const result = results[0]; expect(result).toMatchObject({ name: 'lodash', version: '4.17.21', description: 'A modern JavaScript utility library', keywords: ['util', 'functional'], publishedAt: '2021-02-20T16:20:33.919Z', score: { final: 0.8, detail: { quality: 0.9, popularity: 0.7, maintenance: 0.8 } }, searchScore: 100000.5 }); }); it('should handle API error responses', async () => { mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error' } as any); await expect(registryService.search('lodash')).rejects.toThrow( 'Failed to search packages: Search failed: 500 Internal Server Error' ); }); it('should handle network errors', async () => { mockFetch.mockRejectedValue(new Error('Network error')); await expect(registryService.search('lodash')).rejects.toThrow( 'Failed to search packages: Network error' ); }); it('should handle empty search results', async () => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({ objects: null }) } as any); const results = await registryService.search('nonexistent'); expect(results).toEqual([]); }); }); describe('getPackageInfo', () => { const mockManifest = { 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: 'John Doe <john@example.com>', dependencies: { 'some-dep': '^1.0.0' }, devDependencies: { 'some-dev-dep': '^2.0.0' }, scripts: { test: 'npm test' }, time: { '4.17.21': '2021-02-20T16:20:33.919Z' }, dist: { tarball: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz' }, deprecated: false }; beforeEach(() => { mockPacote.manifest.mockResolvedValue(mockManifest as any); }); it('should get package info for latest version', async () => { const packageInfo = await registryService.getPackageInfo('lodash'); expect(mockPacote.manifest).toHaveBeenCalledWith('lodash', { registry: URLS.NPM_REGISTRY }); expect(packageInfo.name).toBe('lodash'); expect(packageInfo.version).toBe('4.17.21'); }); it('should get package info for specific version', async () => { await registryService.getPackageInfo('lodash', '4.17.20'); expect(mockPacote.manifest).toHaveBeenCalledWith('lodash@4.17.20', { registry: URLS.NPM_REGISTRY }); }); it('should transform package manifest correctly', async () => { const packageInfo = await registryService.getPackageInfo('lodash'); expect(packageInfo).toMatchObject({ 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', dependencies: { 'some-dep': '^1.0.0' }, devDependencies: { 'some-dev-dep': '^2.0.0' }, scripts: { test: 'npm test' }, publishedAt: '2021-02-20T16:20:33.919Z' }); }); it('should handle pacote errors', async () => { mockPacote.manifest.mockRejectedValue(new Error('Package not found')); await expect(registryService.getPackageInfo('nonexistent')).rejects.toThrow( 'Failed to get package info for nonexistent: Package not found' ); }); }); describe('getDownloadStats', () => { const mockDownloadResponse = { downloads: 50000000, start: '2023-01-01', end: '2023-01-31' }; beforeEach(() => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve(mockDownloadResponse) } as any); }); it('should get download statistics', async () => { const stats = await registryService.getDownloadStats('lodash', 'last-month'); expect(mockFetch).toHaveBeenCalledWith( `${URLS.NPM_API}/downloads/point/last-month/lodash` ); expect(stats).toEqual({ downloads: 50000000, period: 'last-month', start: '2023-01-01', end: '2023-01-31' }); }); it('should handle encoded package names', async () => { await registryService.getDownloadStats('@types/node', 'last-week'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('%40types%2Fnode') ); }); it('should handle API errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 404 } as any); await expect( registryService.getDownloadStats('nonexistent', 'last-month') ).rejects.toThrow('Failed to get download stats: Downloads API failed: 404'); }); it('should handle missing downloads data', async () => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({}) } as any); const stats = await registryService.getDownloadStats('lodash', 'last-month'); expect(stats.downloads).toBe(0); }); }); describe('getBundleSize', () => { const mockBundleResponse = { size: 24000, gzip: 7000, dependencyCount: 5 }; beforeEach(() => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve(mockBundleResponse) } as any); }); it('should get bundle size information', async () => { const bundleSize = await registryService.getBundleSize('lodash'); expect(mockFetch).toHaveBeenCalledWith( `${URLS.BUNDLEPHOBIA_API}/size?package=lodash` ); expect(bundleSize).toEqual({ size: 24000, gzip: 7000, dependency: 5, dependencyCount: 5 }); }); it('should handle specific version', async () => { await registryService.getBundleSize('lodash', '4.17.21'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('package=lodash@4.17.21') ); }); it('should return defaults on API error', async () => { mockFetch.mockRejectedValue(new Error('API error')); const bundleSize = await registryService.getBundleSize('lodash'); expect(bundleSize).toEqual({ size: 0, gzip: 0, dependency: 0, dependencyCount: 0 }); }); }); describe('getPackageMetadata', () => { const mockMetadata = { name: 'lodash', versions: { '4.17.21': {}, '4.17.20': {}, '4.17.19': {} } }; beforeEach(() => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve(mockMetadata) } as any); }); it('should get package metadata', async () => { const metadata = await registryService.getPackageMetadata('lodash'); expect(mockFetch).toHaveBeenCalledWith( `${URLS.NPM_REGISTRY}/lodash` ); expect(metadata).toEqual(mockMetadata); }); it('should handle encoded package names', async () => { await registryService.getPackageMetadata('@types/node'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('%40types%2Fnode') ); }); it('should handle API errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 404 } as any); await expect(registryService.getPackageMetadata('nonexistent')).rejects.toThrow( 'Failed to get package metadata: Registry request failed: 404' ); }); }); describe('getPackageVersions', () => { const mockMetadata = { versions: { '4.17.21': {}, '4.17.20': {}, '4.17.19': {}, '4.16.0': {} } }; beforeEach(() => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve(mockMetadata) } as any); }); it('should get package versions sorted', async () => { const versions = await registryService.getPackageVersions('lodash'); expect(versions).toEqual(['4.17.21', '4.17.20', '4.17.19', '4.16.0']); }); it('should handle packages with no versions', async () => { mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({}) } as any); const versions = await registryService.getPackageVersions('lodash'); expect(versions).toEqual([]); }); }); describe('packageExists', () => { it('should return true for existing package', async () => { mockFetch.mockResolvedValue({ ok: true } as any); const exists = await registryService.packageExists('lodash'); expect(exists).toBe(true); expect(mockFetch).toHaveBeenCalledWith( `${URLS.NPM_REGISTRY}/lodash`, { method: 'HEAD' } ); }); it('should return false for non-existing package', async () => { mockFetch.mockResolvedValue({ ok: false } as any); const exists = await registryService.packageExists('nonexistent'); expect(exists).toBe(false); }); it('should return false on network error', async () => { mockFetch.mockRejectedValue(new Error('Network error')); const exists = await registryService.packageExists('lodash'); expect(exists).toBe(false); }); }); describe('transformAuthor', () => { it('should handle string author format', async () => { const mockManifestWithStringAuthor = { name: 'test', version: '1.0.0', author: 'John Doe <john@example.com> (https://johndoe.com)', dist: { tarball: 'https://registry.npmjs.org/test/-/test-1.0.0.tgz' }, deprecated: false }; mockPacote.manifest.mockResolvedValue(mockManifestWithStringAuthor as any); const packageInfo = await registryService.getPackageInfo('test'); expect(packageInfo.author).toEqual({ name: 'John Doe', email: 'john@example.com', url: 'https://johndoe.com' }); }); it('should handle object author format', async () => { const mockManifestWithObjectAuthor = { name: 'test', version: '1.0.0', author: { name: 'John Doe', email: 'john@example.com', url: 'https://johndoe.com' }, dist: { tarball: 'https://registry.npmjs.org/test/-/test-1.0.0.tgz' }, deprecated: false }; mockPacote.manifest.mockResolvedValue(mockManifestWithObjectAuthor as any); const packageInfo = await registryService.getPackageInfo('test'); expect(packageInfo.author).toEqual({ name: 'John Doe', email: 'john@example.com', url: 'https://johndoe.com' }); }); it('should handle simple string author', async () => { const mockManifestWithSimpleAuthor = { name: 'test', version: '1.0.0', author: 'John Doe', dist: { tarball: 'https://registry.npmjs.org/test/-/test-1.0.0.tgz' }, deprecated: false }; mockPacote.manifest.mockResolvedValue(mockManifestWithSimpleAuthor as any); const packageInfo = await registryService.getPackageInfo('test'); expect(packageInfo.author).toEqual({ name: 'John Doe' }); }); }); });