/** * Native npm CLI Testing * Tests the NPM registry implementation using the actual npm CLI */ import { expect, tap } from '@git.zone/tstest/tapbundle'; import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside'; import { SmartRegistry } from '../ts/index.js'; import { createTestRegistry, createTestTokens } from './helpers/registry.js'; import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js'; import * as http from 'http'; import * as url from 'url'; import * as fs from 'fs'; import * as path from 'path'; // Test context let registry: SmartRegistry; let server: http.Server; let registryUrl: string; let registryPort: number; let npmToken: string; let testDir: string; let npmrcPath: string; /** * Create HTTP server wrapper around SmartRegistry */ async function createHttpServer( registryInstance: SmartRegistry, port: number ): Promise<{ server: http.Server; url: string }> { return new Promise((resolve, reject) => { const httpServer = http.createServer(async (req, res) => { try { // Parse request const parsedUrl = url.parse(req.url || '', true); const pathname = parsedUrl.pathname || '/'; const query = parsedUrl.query; // Read body const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } const bodyBuffer = Buffer.concat(chunks); // Parse body based on content type let body: any; if (bodyBuffer.length > 0) { const contentType = req.headers['content-type'] || ''; if (contentType.includes('application/json')) { try { body = JSON.parse(bodyBuffer.toString('utf-8')); } catch (error) { body = bodyBuffer; } } else { body = bodyBuffer; } } // Convert to IRequestContext const context: IRequestContext = { method: req.method || 'GET', path: pathname, headers: req.headers as Record, query: query as Record, body: body, }; // Handle request const response: IResponse = await registryInstance.handleRequest(context); // Convert IResponse to HTTP response res.statusCode = response.status; // Set headers for (const [key, value] of Object.entries(response.headers || {})) { res.setHeader(key, value); } // Send body if (response.body) { if (Buffer.isBuffer(response.body)) { res.end(response.body); } else if (typeof response.body === 'string') { res.end(response.body); } else { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(response.body)); } } else { res.end(); } } catch (error) { console.error('Server error:', error); res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'INTERNAL_ERROR', message: String(error) })); } }); httpServer.listen(port, () => { const serverUrl = `http://localhost:${port}`; resolve({ server: httpServer, url: serverUrl }); }); httpServer.on('error', reject); }); } /** * Setup .npmrc configuration */ function setupNpmrc(registryUrlArg: string, token: string, testDirArg: string): string { const npmrcContent = `registry=${registryUrlArg}/npm/ //localhost:${registryPort}/npm/:_authToken=${token} `; const npmrcFilePath = path.join(testDirArg, '.npmrc'); fs.writeFileSync(npmrcFilePath, npmrcContent, 'utf-8'); return npmrcFilePath; } /** * Create a test package */ function createTestPackage( packageName: string, version: string, targetDir: string ): string { const packageDir = path.join(targetDir, packageName); fs.mkdirSync(packageDir, { recursive: true }); // Create package.json const packageJson = { name: packageName, version: version, description: `Test package ${packageName}`, main: 'index.js', scripts: { test: 'echo "Test passed"', }, keywords: ['test'], author: 'Test Author', license: 'MIT', }; fs.writeFileSync( path.join(packageDir, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8' ); // Create index.js const indexJs = `module.exports = { name: '${packageName}', version: '${version}', message: 'Hello from ${packageName}@${version}' }; `; fs.writeFileSync(path.join(packageDir, 'index.js'), indexJs, 'utf-8'); // Create README.md const readme = `# ${packageName} Test package for SmartRegistry. Version: ${version} `; fs.writeFileSync(path.join(packageDir, 'README.md'), readme, 'utf-8'); return packageDir; } /** * Run npm command with proper environment */ async function runNpmCommand( command: string, cwd: string ): Promise<{ stdout: string; stderr: string; exitCode: number }> { // Prepare environment variables const envVars = [ `NPM_CONFIG_USERCONFIG="${npmrcPath}"`, `NPM_CONFIG_CACHE="${path.join(testDir, '.npm-cache')}"`, `NPM_CONFIG_PREFIX="${path.join(testDir, '.npm-global')}"`, `NPM_CONFIG_REGISTRY="${registryUrl}/npm/"`, ].join(' '); // Build command with cd to correct directory and environment variables const fullCommand = `cd "${cwd}" && ${envVars} ${command}`; try { const result = await tapNodeTools.runCommand(fullCommand); return { stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode || 0, }; } catch (error: any) { return { stdout: error.stdout || '', stderr: error.stderr || String(error), exitCode: error.exitCode || 1, }; } } /** * Cleanup test directory */ function cleanupTestDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } // ======================================================================== // TESTS // ======================================================================== tap.test('NPM CLI: should setup registry and HTTP server', async () => { // Create registry registry = await createTestRegistry(); const tokens = await createTestTokens(registry); npmToken = tokens.npmToken; expect(registry).toBeInstanceOf(SmartRegistry); expect(npmToken).toBeTypeOf('string'); // Find available port registryPort = 35000; const serverSetup = await createHttpServer(registry, registryPort); server = serverSetup.server; registryUrl = serverSetup.url; expect(server).toBeDefined(); expect(registryUrl).toEqual(`http://localhost:${registryPort}`); // Setup test directory testDir = path.join(process.cwd(), '.nogit', 'test-npm-cli'); cleanupTestDir(testDir); fs.mkdirSync(testDir, { recursive: true }); // Setup .npmrc npmrcPath = setupNpmrc(registryUrl, npmToken, testDir); expect(fs.existsSync(npmrcPath)).toEqual(true); }); tap.test('NPM CLI: should verify server is responding', async () => { const result = await runNpmCommand('npm ping', testDir); console.log('npm ping output:', result.stdout, result.stderr); // npm ping may not work with custom registries, so just check server is up // by doing a direct HTTP request const response = await fetch(`${registryUrl}/npm/`); expect(response.status).toBeGreaterThanOrEqual(200); expect(response.status).toBeLessThan(500); }); tap.test('NPM CLI: should publish a package', async () => { const packageName = 'test-package-cli'; const version = '1.0.0'; const packageDir = createTestPackage(packageName, version, testDir); const result = await runNpmCommand('npm publish', packageDir); console.log('npm publish output:', result.stdout); console.log('npm publish stderr:', result.stderr); expect(result.exitCode).toEqual(0); expect(result.stdout || result.stderr).toContain(packageName); }); tap.test('NPM CLI: should view published package', async () => { const packageName = 'test-package-cli'; const result = await runNpmCommand(`npm view ${packageName}`, testDir); console.log('npm view output:', result.stdout); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain(packageName); expect(result.stdout).toContain('1.0.0'); }); tap.test('NPM CLI: should install published package', async () => { const packageName = 'test-package-cli'; const installDir = path.join(testDir, 'install-test'); fs.mkdirSync(installDir, { recursive: true }); // Create package.json for installation const packageJson = { name: 'install-test', version: '1.0.0', dependencies: { [packageName]: '1.0.0', }, }; fs.writeFileSync( path.join(installDir, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8' ); const result = await runNpmCommand('npm install', installDir); console.log('npm install output:', result.stdout); console.log('npm install stderr:', result.stderr); expect(result.exitCode).toEqual(0); // Verify package was installed const nodeModulesPath = path.join(installDir, 'node_modules', packageName); expect(fs.existsSync(nodeModulesPath)).toEqual(true); expect(fs.existsSync(path.join(nodeModulesPath, 'package.json'))).toEqual(true); expect(fs.existsSync(path.join(nodeModulesPath, 'index.js'))).toEqual(true); // Verify package contents const installedPackageJson = JSON.parse( fs.readFileSync(path.join(nodeModulesPath, 'package.json'), 'utf-8') ); expect(installedPackageJson.name).toEqual(packageName); expect(installedPackageJson.version).toEqual('1.0.0'); }); tap.test('NPM CLI: should publish second version', async () => { const packageName = 'test-package-cli'; const version = '1.1.0'; const packageDir = createTestPackage(packageName, version, testDir); const result = await runNpmCommand('npm publish', packageDir); console.log('npm publish v1.1.0 output:', result.stdout); expect(result.exitCode).toEqual(0); }); tap.test('NPM CLI: should list versions', async () => { const packageName = 'test-package-cli'; const result = await runNpmCommand(`npm view ${packageName} versions`, testDir); console.log('npm view versions output:', result.stdout); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('1.0.0'); expect(result.stdout).toContain('1.1.0'); }); tap.test('NPM CLI: should publish scoped package', async () => { const packageName = '@testscope/scoped-package'; const version = '1.0.0'; const packageDir = createTestPackage(packageName, version, testDir); const result = await runNpmCommand('npm publish --access public', packageDir); console.log('npm publish scoped output:', result.stdout); console.log('npm publish scoped stderr:', result.stderr); expect(result.exitCode).toEqual(0); }); tap.test('NPM CLI: should view scoped package', async () => { const packageName = '@testscope/scoped-package'; const result = await runNpmCommand(`npm view ${packageName}`, testDir); console.log('npm view scoped output:', result.stdout); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('scoped-package'); }); tap.test('NPM CLI: should fail to publish without auth', async () => { const packageName = 'unauth-package'; const version = '1.0.0'; const packageDir = createTestPackage(packageName, version, testDir); // Temporarily remove .npmrc const npmrcBackup = fs.readFileSync(npmrcPath, 'utf-8'); fs.writeFileSync(npmrcPath, 'registry=' + registryUrl + '/npm/\n', 'utf-8'); const result = await runNpmCommand('npm publish', packageDir); console.log('npm publish unauth output:', result.stdout); console.log('npm publish unauth stderr:', result.stderr); // Restore .npmrc fs.writeFileSync(npmrcPath, npmrcBackup, 'utf-8'); // Should fail with auth error expect(result.exitCode).not.toEqual(0); }); tap.postTask('cleanup npm cli tests', async () => { // Stop server if (server) { await new Promise((resolve) => { server.close(() => resolve()); }); } // Cleanup test directory if (testDir) { cleanupTestDir(testDir); } // Destroy registry if (registry) { registry.destroy(); } }); export default tap.start();