/** * Native cargo CLI Testing * Tests the Cargo registry implementation using the actual cargo 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 cargoToken: string; let testDir: string; let cargoHome: 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 Cargo configuration */ function setupCargoConfig(registryUrlArg: string, token: string, cargoHomeArg: string): void { const cargoConfigDir = path.join(cargoHomeArg, '.cargo'); fs.mkdirSync(cargoConfigDir, { recursive: true }); // Create config.toml with sparse protocol const configContent = `[registries.test-registry] index = "sparse+${registryUrlArg}/cargo/" [source.crates-io] replace-with = "test-registry" [net] retry = 0 `; fs.writeFileSync(path.join(cargoConfigDir, 'config.toml'), configContent, 'utf-8'); // Create credentials.toml (Cargo uses plain token, no "Bearer" prefix) const credentialsContent = `[registries.test-registry] token = "${token}" `; fs.writeFileSync(path.join(cargoConfigDir, 'credentials.toml'), credentialsContent, 'utf-8'); } /** * Create a test Cargo crate */ function createTestCrate( crateName: string, version: string, targetDir: string ): string { const crateDir = path.join(targetDir, crateName); fs.mkdirSync(crateDir, { recursive: true }); // Create Cargo.toml const cargoToml = `[package] name = "${crateName}" version = "${version}" edition = "2021" description = "Test crate ${crateName}" license = "MIT" authors = ["Test Author "] [dependencies] `; fs.writeFileSync(path.join(crateDir, 'Cargo.toml'), cargoToml, 'utf-8'); // Create src directory const srcDir = path.join(crateDir, 'src'); fs.mkdirSync(srcDir, { recursive: true }); // Create lib.rs const libRs = `//! Test crate ${crateName} /// Returns a greeting message pub fn greet() -> String { format!("Hello from {}@{}", "${crateName}", "${version}") } #[cfg(test)] mod tests { use super::*; #[test] fn test_greet() { let greeting = greet(); assert!(greeting.contains("${crateName}")); } } `; fs.writeFileSync(path.join(srcDir, 'lib.rs'), libRs, 'utf-8'); // Create README.md const readme = `# ${crateName} Test crate for SmartRegistry. Version: ${version} `; fs.writeFileSync(path.join(crateDir, 'README.md'), readme, 'utf-8'); return crateDir; } /** * Run cargo command with proper environment */ async function runCargoCommand( command: string, cwd: string, includeToken: boolean = true ): Promise<{ stdout: string; stderr: string; exitCode: number }> { // Prepare environment variables // NOTE: Cargo converts registry name "test-registry" to "TEST_REGISTRY" for env vars const envVars = [ `CARGO_HOME="${cargoHome}"`, `CARGO_REGISTRIES_TEST_REGISTRY_INDEX="sparse+${registryUrl}/cargo/"`, includeToken ? `CARGO_REGISTRIES_TEST_REGISTRY_TOKEN="${cargoToken}"` : '', `CARGO_NET_RETRY="0"`, ].filter(Boolean).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('Cargo CLI: should setup registry and HTTP server', async () => { // Create registry registry = await createTestRegistry(); const tokens = await createTestTokens(registry); cargoToken = tokens.cargoToken; expect(registry).toBeInstanceOf(SmartRegistry); expect(cargoToken).toBeTypeOf('string'); // Clean up any existing index from previous test runs const storage = registry.getStorage(); try { await storage.putCargoIndex('test-crate-cli', []); } catch (error) { // Ignore error if operation fails } // Use port 5000 (hardcoded in CargoRegistry default config) // TODO: Once registryUrl is configurable, use dynamic port like npm test (35001) registryPort = 5000; 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-cargo-cli'); cleanupTestDir(testDir); fs.mkdirSync(testDir, { recursive: true }); // Setup CARGO_HOME cargoHome = path.join(testDir, '.cargo-home'); fs.mkdirSync(cargoHome, { recursive: true }); // Setup Cargo config setupCargoConfig(registryUrl, cargoToken, cargoHome); expect(fs.existsSync(path.join(cargoHome, '.cargo', 'config.toml'))).toEqual(true); expect(fs.existsSync(path.join(cargoHome, '.cargo', 'credentials.toml'))).toEqual(true); }); tap.test('Cargo CLI: should verify server is responding', async () => { // Check server is up by doing a direct HTTP request to the cargo index const response = await fetch(`${registryUrl}/cargo/`); expect(response.status).toBeGreaterThanOrEqual(200); expect(response.status).toBeLessThan(500); }); tap.test('Cargo CLI: should publish a crate', async () => { const crateName = 'test-crate-cli'; const version = '0.1.0'; const crateDir = createTestCrate(crateName, version, testDir); const result = await runCargoCommand('cargo publish --registry test-registry --allow-dirty', crateDir); console.log('cargo publish output:', result.stdout); console.log('cargo publish stderr:', result.stderr); expect(result.exitCode).toEqual(0); expect(result.stdout || result.stderr).toContain(crateName); }); tap.test('Cargo CLI: should verify crate in index', async () => { const crateName = 'test-crate-cli'; // Cargo uses a specific index structure // For crate "test-crate-cli", the index path is based on the first characters // 1 char: // 2 char: 2/ // 3 char: 3// // 4+ char: // // "test-crate-cli" is 14 chars, so it should be at: te/st/test-crate-cli const indexPath = `/cargo/te/st/${crateName}`; const response = await fetch(`${registryUrl}${indexPath}`); expect(response.status).toEqual(200); const indexData = await response.text(); console.log('Index data:', indexData); // Index should contain JSON line with crate info expect(indexData).toContain(crateName); expect(indexData).toContain('0.1.0'); }); tap.test('Cargo CLI: should download published crate', async () => { const crateName = 'test-crate-cli'; const version = '0.1.0'; // Cargo downloads crates from /cargo/api/v1/crates/{name}/{version}/download const downloadPath = `/cargo/api/v1/crates/${crateName}/${version}/download`; const response = await fetch(`${registryUrl}${downloadPath}`); expect(response.status).toEqual(200); const crateData = await response.arrayBuffer(); expect(crateData.byteLength).toBeGreaterThan(0); }); tap.test('Cargo CLI: should publish second version', async () => { const crateName = 'test-crate-cli'; const version = '0.2.0'; const crateDir = createTestCrate(crateName, version, testDir); const result = await runCargoCommand('cargo publish --registry test-registry --allow-dirty', crateDir); console.log('cargo publish v0.2.0 output:', result.stdout); expect(result.exitCode).toEqual(0); }); tap.test('Cargo CLI: should list versions in index', async () => { const crateName = 'test-crate-cli'; const indexPath = `/cargo/te/st/${crateName}`; const response = await fetch(`${registryUrl}${indexPath}`); expect(response.status).toEqual(200); const indexData = await response.text(); const lines = indexData.trim().split('\n'); // Should have 2 lines (2 versions) expect(lines.length).toEqual(2); // Parse JSON lines const version1 = JSON.parse(lines[0]); const version2 = JSON.parse(lines[1]); expect(version1.vers).toEqual('0.1.0'); expect(version2.vers).toEqual('0.2.0'); }); tap.test('Cargo CLI: should search for crate', async () => { const crateName = 'test-crate-cli'; // Cargo search endpoint: /cargo/api/v1/crates?q={query} const response = await fetch(`${registryUrl}/cargo/api/v1/crates?q=${crateName}`); expect(response.status).toEqual(200); const searchResults = await response.json(); console.log('Search results:', searchResults); expect(searchResults).toHaveProperty('crates'); expect(searchResults.crates).toBeInstanceOf(Array); expect(searchResults.crates.length).toBeGreaterThan(0); expect(searchResults.crates[0].name).toEqual(crateName); }); tap.test('Cargo CLI: should yank a version', async () => { const crateName = 'test-crate-cli'; const crateDir = path.join(testDir, crateName); const result = await runCargoCommand('cargo yank --registry test-registry --vers 0.1.0', crateDir); console.log('cargo yank output:', result.stdout); console.log('cargo yank stderr:', result.stderr); expect(result.exitCode).toEqual(0); // Verify version is yanked in index const indexPath = `/cargo/te/st/${crateName}`; const response = await fetch(`${registryUrl}${indexPath}`); const indexData = await response.text(); const lines = indexData.trim().split('\n'); const version1 = JSON.parse(lines[0]); expect(version1.yanked).toEqual(true); }); tap.test('Cargo CLI: should unyank a version', async () => { const crateName = 'test-crate-cli'; const crateDir = path.join(testDir, crateName); const result = await runCargoCommand('cargo yank --registry test-registry --vers 0.1.0 --undo', crateDir); console.log('cargo unyank output:', result.stdout); console.log('cargo unyank stderr:', result.stderr); expect(result.exitCode).toEqual(0); // Verify version is not yanked in index const indexPath = `/cargo/te/st/${crateName}`; const response = await fetch(`${registryUrl}${indexPath}`); const indexData = await response.text(); const lines = indexData.trim().split('\n'); const version1 = JSON.parse(lines[0]); expect(version1.yanked).toEqual(false); }); tap.test('Cargo CLI: should fail to publish without auth', async () => { const crateName = 'unauth-crate'; const version = '0.1.0'; const crateDir = createTestCrate(crateName, version, testDir); // Run without token (includeToken: false) const result = await runCargoCommand('cargo publish --registry test-registry --allow-dirty', crateDir, false); console.log('cargo publish unauth output:', result.stdout); console.log('cargo publish unauth stderr:', result.stderr); // Should fail with auth error expect(result.exitCode).not.toEqual(0); expect(result.stderr).toContain('token'); }); tap.postTask('cleanup cargo 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();