/** * 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, cleanupS3Bucket } 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 state 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 { const parsedUrl = new url.URL(req.url || '/', `http://localhost:${port}`); const pathname = parsedUrl.pathname; const query: Record = {}; parsedUrl.searchParams.forEach((value, key) => { query[key] = value; }); // Read body let body: any = undefined; if (req.method === 'PUT' || req.method === 'POST' || req.method === 'PATCH') { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.from(chunk)); } const bodyBuffer = Buffer.concat(chunks); 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 (response.body is always ReadableStream or undefined) if (response.body) { const { Readable } = await import('stream'); Readable.fromWeb(response.body).pipe(res); } 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.replace(/\//g, '-')); fs.mkdirSync(packageDir, { recursive: true }); // Create package.json const packageJson = { name: packageName, version: version, description: `Test package ${packageName}`, main: 'index.js', scripts: {}, }; fs.writeFileSync( path.join(packageDir, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf-8' ); // Create a simple index.js fs.writeFileSync( path.join(packageDir, 'index.js'), `module.exports = { name: '${packageName}', version: '${version}' };\n`, 'utf-8' ); // Create README.md fs.writeFileSync( path.join(packageDir, 'README.md'), `# ${packageName}\n\nTest package version ${version}\n`, 'utf-8' ); // Copy .npmrc into the package directory if (npmrcPath && fs.existsSync(npmrcPath)) { fs.copyFileSync(npmrcPath, path.join(packageDir, '.npmrc')); } return packageDir; } /** * Run npm command with proper environment */ async function runNpmCommand( command: string, cwd: string ): Promise<{ stdout: string; stderr: string; exitCode: number }> { const { exec } = await import('child_process'); // Build isolated env that prevents npm from reading ~/.npmrc const env: Record = {}; // Copy only essential env vars (PATH, etc.) — exclude HOME to prevent ~/.npmrc reading for (const key of ['PATH', 'NODE', 'NVM_DIR', 'NVM_BIN', 'LANG', 'TERM', 'SHELL']) { if (process.env[key]) env[key] = process.env[key]!; } env.HOME = testDir; env.NPM_CONFIG_USERCONFIG = npmrcPath; env.NPM_CONFIG_GLOBALCONFIG = '/dev/null'; env.NPM_CONFIG_CACHE = path.join(testDir, '.npm-cache'); env.NPM_CONFIG_PREFIX = path.join(testDir, '.npm-global'); env.NPM_CONFIG_REGISTRY = `${registryUrl}/npm/`; return new Promise((resolve) => { exec(command, { cwd, env, timeout: 30000 }, (error, stdout, stderr) => { resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: error ? (error as any).code ?? 1 : 0, }); }); }); } /** * 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 () => { // Find available port registryPort = 35000; // Create registry with correct registryUrl for CLI tests registry = await createTestRegistry({ registryUrl: `http://localhost:${registryPort}` }); const tokens = await createTestTokens(registry); npmToken = tokens.npmToken; // Clean up stale npm CLI test data via unpublish API for (const pkg of ['test-package-cli', '@testscope%2fscoped-package']) { await registry.handleRequest({ method: 'DELETE', path: `/npm/${pkg}/-rev/cleanup`, headers: { Authorization: `Bearer ${npmToken}` }, query: {}, }); } expect(registry).toBeInstanceOf(SmartRegistry); expect(npmToken).toBeTypeOf('string'); const serverSetup = await createHttpServer(registry, registryPort); server = serverSetup.server; registryUrl = serverSetup.url; expect(server).toBeDefined(); expect(registryUrl).toEqual(`http://localhost:${registryPort}`); // Setup test directory — use /tmp to isolate from project tree testDir = path.join('/tmp', 'smartregistry-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 a minimal package.json for install target fs.writeFileSync( path.join(installDir, 'package.json'), JSON.stringify({ name: 'install-test', version: '1.0.0', dependencies: { [packageName]: '1.0.0' } }), 'utf-8' ); // Copy .npmrc if (npmrcPath && fs.existsSync(npmrcPath)) { fs.copyFileSync(npmrcPath, path.join(installDir, '.npmrc')); } 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 installed = fs.existsSync(path.join(installDir, 'node_modules', packageName, 'package.json')); expect(installed).toEqual(true); }); 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 (write one without auth) const noAuthNpmrc = path.join(packageDir, '.npmrc'); fs.writeFileSync(noAuthNpmrc, `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); // 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 cleanupTestDir(testDir); }); export default tap.start();