diff --git a/e2e/package.json b/e2e/package.json index 60d429ad3..50503715f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -21,6 +21,7 @@ "build:esm": "mkdir -p dist-esm && bun build src/index.ts --outdir dist-esm --format esm --target node --external '@lit-protocol/*' && mv dist-esm/index.js dist/index.mjs && rm -rf dist-esm", "prepublishOnly": "bun run build", "test": "bun test", + "test:e2e": "dotenvx run --env-file=../.env -- bun test ./src/e2e.spec.ts --timeout 50000000 -t", "test:watch": "bun test --watch" }, "keywords": [ diff --git a/package.json b/package.json index ae85b044b..22e316879 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "unlink-all": "for dir in packages/*/; do echo \"Unlinking in $dir\"; (cd \"$dir\" && bun unlink) || { echo \"ERROR: Failed to unlink in $dir\"; exit 1; }; done", "auth-services": "cd packages/auth-services && bun run start", "test:e2e": "bun test ./e2e/src/e2e.spec.ts -t", + "test:e2e:published": "node test-e2e-published.mjs", "artillery:init": "bun run ./e2e/artillery/src/init.ts", "artillery:balance-status": "LOG_LEVEL=silent bun run ./e2e/artillery/src/balance-status.ts", "artillery:pkp-sign": "DEBUG_HTTP=true LOG_LEVEL=silent dotenvx run --env-file=.env -- sh -c 'artillery run ./e2e/artillery/configs/pkp-sign.yml ${ARTILLERY_KEY:+--record --key $ARTILLERY_KEY}'", diff --git a/test-e2e-published.mjs b/test-e2e-published.mjs new file mode 100644 index 000000000..c530a1600 --- /dev/null +++ b/test-e2e-published.mjs @@ -0,0 +1,403 @@ +#!/usr/bin/env node + +/** + * E2E Published Package Test Script + * + * This script replaces peer dependency versions in ./e2e/package.json with a specified version + * and then runs the e2e tests in the naga-local network environment. + * + * Usage: bun run test:e2e:published + * Example: bun run test:e2e:published 8.0.0-prealpha-886.4 + */ + +import { readFile, writeFile, rm } from 'fs/promises'; +import { spawn } from 'child_process'; + +import path from 'path'; +import { existsSync } from 'fs'; + +// Configuration +const E2E_PACKAGE_JSON_PATH = './e2e/package.json'; +const E2E_DIRECTORY = './e2e'; + +// Global state to track if cleanup is needed +let needsCleanup = false; +let isCleaningUp = false; +let currentTestProcess = null; +let isShuttingDown = false; + +/** + * Main execution function + */ +async function main() { + const version = process.argv[2]; + + if (!version) { + console.error('โŒ Error: Version number is required'); + console.log('Usage: bun run test:e2e:published '); + console.log('Example: bun run test:e2e:published 8.0.0-prealpha-886.4'); + process.exit(1); + } + + console.log( + `๐Ÿš€ Starting E2E published package test with version: ${version}` + ); + + let testsPassed = false; + + try { + // Step 1: Update peer dependencies in e2e/package.json + await updatePeerDependencies(version); + needsCleanup = true; // Mark that cleanup is needed after this point + + // Step 2: Install dependencies to ensure we're using published packages + await installDependencies(); + + // Step 3: Verify installed package versions + await verifyInstalledVersions(version); + + // Step 4: Run e2e tests in naga-local network + await runE2ETests(); + + testsPassed = true; + console.log('โœ… E2E published package test completed successfully!'); + } catch (error) { + console.error('โŒ E2E published package test failed:', error.message); + testsPassed = false; + } finally { + // Step 3: Always cleanup regardless of success or failure + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error('โš ๏ธ Cleanup failed:', cleanupError.message); + } + } + } + + if (!testsPassed) { + process.exit(1); + } +} + +/** + * Updates peer dependencies in e2e/package.json + * @param {string} version - The version to replace "*" with + */ +async function updatePeerDependencies(version) { + console.log('๐Ÿ“ Updating peer dependencies in e2e/package.json...'); + + try { + // Read the current package.json + const packageJsonContent = await readFile(E2E_PACKAGE_JSON_PATH, 'utf8'); + const packageJson = JSON.parse(packageJsonContent); + + // Replace all "*" versions in peerDependencies with the specified version + if (packageJson.peerDependencies) { + for (const [depName, depVersion] of Object.entries( + packageJson.peerDependencies + )) { + if (depVersion === '*') { + packageJson.peerDependencies[depName] = version; + console.log(` โœ“ Updated ${depName}: "*" โ†’ "${version}"`); + } + } + } + + // Write the updated package.json + await writeFile( + E2E_PACKAGE_JSON_PATH, + JSON.stringify(packageJson, null, 2) + '\n' + ); + console.log('โœ… Peer dependencies updated successfully'); + } catch (error) { + throw new Error(`Failed to update peer dependencies: ${error.message}`); + } +} + +/** + * Installs dependencies to ensure we're using published packages + */ +async function installDependencies() { + console.log('๐Ÿ“ฆ Installing dependencies from npm...'); + + return new Promise((resolve, reject) => { + const command = 'bun'; + const args = ['install']; + + console.log(` Running: ${command} ${args.join(' ')}`); + console.log('๐Ÿ“‹ Install Output:'); + console.log('โ”€'.repeat(60)); + + const installProcess = spawn(command, args, { + cwd: E2E_DIRECTORY, + stdio: 'inherit', + }); + + currentTestProcess = installProcess; // Track for signal handling + + installProcess.on('close', (code) => { + currentTestProcess = null; + console.log('โ”€'.repeat(60)); + if (code === 0) { + console.log('โœ… Dependencies installed successfully'); + resolve(); + } else { + reject( + new Error(`Dependency installation failed with exit code ${code}`) + ); + } + }); + + installProcess.on('error', (error) => { + currentTestProcess = null; + reject( + new Error(`Failed to start dependency installation: ${error.message}`) + ); + }); + }); +} + +/** + * Verifies that the correct package versions are installed + */ +async function verifyInstalledVersions(expectedVersion) { + console.log('๐Ÿ” Verifying installed package versions...'); + + try { + const nodeModulesPath = path.join(E2E_DIRECTORY, 'node_modules'); + const packagesToCheck = [ + '@lit-protocol/auth', + '@lit-protocol/lit-client', + '@lit-protocol/networks', + ]; + + for (const packageName of packagesToCheck) { + const packageJsonPath = path.join( + nodeModulesPath, + packageName, + 'package.json' + ); + + if (existsSync(packageJsonPath)) { + const packageContent = await readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageContent); + const installedVersion = packageJson.version; + + if (installedVersion === expectedVersion) { + console.log(` โœ… ${packageName}: ${installedVersion} โœ“`); + } else { + console.log( + ` โŒ ${packageName}: expected ${expectedVersion}, got ${installedVersion}` + ); + throw new Error( + `Version mismatch for ${packageName}: expected ${expectedVersion}, got ${installedVersion}` + ); + } + } else { + throw new Error(`Package ${packageName} not found in node_modules`); + } + } + + console.log('โœ… All package versions verified successfully'); + } catch (error) { + throw new Error(`Failed to verify package versions: ${error.message}`); + } +} + +/** + * Runs the e2e tests in the naga-local network environment + */ +async function runE2ETests() { + console.log('๐Ÿงช Running E2E tests in naga-local network...'); + + return new Promise((resolve, reject) => { + const command = 'bun'; + const args = ['run', 'test:e2e', 'all', '--timeout', '50000000']; + + console.log(` Running: NETWORK=naga-local ${command} ${args.join(' ')}`); + console.log('๐Ÿ“‹ Test Output (real-time):'); + console.log('โ”€'.repeat(60)); + + const testProcess = spawn(command, args, { + cwd: E2E_DIRECTORY, + env: { ...process.env, NETWORK: 'naga-local' }, + stdio: 'inherit', // This enables real-time output + }); + + currentTestProcess = testProcess; // Store reference for signal handling + + testProcess.on('close', (code) => { + currentTestProcess = null; // Clear reference + console.log('โ”€'.repeat(60)); + if (code === 0) { + console.log('โœ… E2E tests completed successfully'); + resolve(); + } else { + reject(new Error(`E2E tests failed with exit code ${code}`)); + } + }); + + testProcess.on('error', (error) => { + currentTestProcess = null; // Clear reference + reject(new Error(`Failed to start E2E tests: ${error.message}`)); + }); + }); +} + +/** + * Reverts peer dependencies back to "*" in e2e/package.json + */ +async function revertPeerDependencies() { + console.log('๐Ÿ”„ Reverting peer dependencies back to "*"...'); + + try { + // Read the current package.json + const packageJsonContent = await readFile(E2E_PACKAGE_JSON_PATH, 'utf8'); + const packageJson = JSON.parse(packageJsonContent); + + // Revert all @lit-protocol peer dependencies back to "*" + if (packageJson.peerDependencies) { + for (const [depName, depVersion] of Object.entries( + packageJson.peerDependencies + )) { + if (depName.startsWith('@lit-protocol/') && depVersion !== '*') { + packageJson.peerDependencies[depName] = '*'; + console.log(` โœ“ Reverted ${depName}: "${depVersion}" โ†’ "*"`); + } + } + } + + // Write the reverted package.json + await writeFile( + E2E_PACKAGE_JSON_PATH, + JSON.stringify(packageJson, null, 2) + '\n' + ); + console.log('โœ… Peer dependencies reverted successfully'); + } catch (error) { + throw new Error(`Failed to revert peer dependencies: ${error.message}`); + } +} + +/** + * Cleans up files and directories created during the test + */ +async function cleanup() { + if (isCleaningUp) return; // Prevent multiple cleanup attempts + isCleaningUp = true; + + console.log('๐Ÿงน Starting cleanup...'); + + // Step 1: Remove node_modules in e2e directory + const nodeModulesPath = path.join(E2E_DIRECTORY, 'node_modules'); + if (existsSync(nodeModulesPath)) { + console.log(' ๐Ÿ—‘๏ธ Removing ./e2e/node_modules...'); + await rm(nodeModulesPath, { recursive: true, force: true }); + console.log(' โœ“ node_modules removed'); + } else { + console.log(' โ„น๏ธ node_modules not found, skipping'); + } + + // Step 2: Remove bun.lock in e2e directory + const bunLockPath = path.join(E2E_DIRECTORY, 'bun.lock'); + if (existsSync(bunLockPath)) { + console.log(' ๐Ÿ—‘๏ธ Removing ./e2e/bun.lock...'); + await rm(bunLockPath, { force: true }); + console.log(' โœ“ bun.lock removed'); + } else { + console.log(' โ„น๏ธ bun.lock not found, skipping'); + } + + // Step 3: Revert peer dependencies back to "*" + await revertPeerDependencies(); + + console.log('โœ… Cleanup completed successfully'); +} + +/** + * Handles graceful shutdown when the process is interrupted + */ +async function handleGracefulShutdown(signal) { + // Prevent multiple signal handlers from running + if (isShuttingDown) { + console.log(`\nโณ Already shutting down, ignoring ${signal}...`); + return; + } + + isShuttingDown = true; + console.log(`\nโš ๏ธ Received ${signal}. Performing cleanup before exit...`); + + // Kill the test process if it's running + if (currentTestProcess && !currentTestProcess.killed) { + console.log('๐Ÿ›‘ Terminating running test process...'); + try { + currentTestProcess.kill('SIGTERM'); + // Give it a moment to terminate gracefully + await new Promise((resolve) => setTimeout(resolve, 2000)); + if (!currentTestProcess.killed) { + currentTestProcess.kill('SIGKILL'); + } + } catch (error) { + console.log('โš ๏ธ Error terminating test process:', error.message); + } + } + + // Only cleanup if we haven't already started + if (needsCleanup && !isCleaningUp) { + try { + // Temporarily ignore signals during cleanup to prevent interruption + process.removeAllListeners('SIGINT'); + process.removeAllListeners('SIGTERM'); + process.removeAllListeners('SIGHUP'); + + await cleanup(); + } catch (cleanupError) { + console.error('โŒ Cleanup failed during shutdown:', cleanupError.message); + } + } + + console.log('๐Ÿ‘‹ Cleanup completed. Exiting...'); + process.exit(0); +} + +// Register signal handlers for graceful shutdown +process.on('SIGINT', handleGracefulShutdown.bind(null, 'SIGINT')); // Ctrl+C +process.on('SIGTERM', handleGracefulShutdown.bind(null, 'SIGTERM')); // Termination signal +process.on('SIGHUP', handleGracefulShutdown.bind(null, 'SIGHUP')); // Hang up signal + +// Handle uncaught exceptions and unhandled rejections +process.on('uncaughtException', async (error) => { + console.error('๐Ÿ’ฅ Uncaught exception:', error); + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error( + 'โŒ Cleanup failed during exception handling:', + cleanupError.message + ); + } + } + process.exit(1); +}); + +process.on('unhandledRejection', async (reason, promise) => { + console.error('๐Ÿ’ฅ Unhandled rejection at:', promise, 'reason:', reason); + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error( + 'โŒ Cleanup failed during rejection handling:', + cleanupError.message + ); + } + } + process.exit(1); +}); + +// Run the script +main().catch((error) => { + console.error('๐Ÿ’ฅ Unexpected error:', error); + process.exit(1); +});