From 0c56c3d1f91534f28ff447fde84a4fda63430f62 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Tue, 29 Jul 2025 11:09:49 +0200 Subject: [PATCH] Basic e2e test --- .github/workflows/e2e.yml | 37 +++++++ package.json | 1 + test/e2e/Dockerfile | 32 ++++++ test/e2e/setup.e2e.spec.js | 195 +++++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 .github/workflows/e2e.yml create mode 100644 test/e2e/Dockerfile create mode 100644 test/e2e/setup.e2e.spec.js diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..5fa8d3f --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,37 @@ +name: E2E Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test + + - name: Run E2E tests + run: npm run test:e2e + + - name: Clean up Docker resources + if: always() + run: | + # Clean up any remaining containers and images + docker ps -aq --filter "name=safe-chain-e2e-test" | xargs -r docker rm -f + docker images -q safe-chain-e2e-test | xargs -r docker rmi -f \ No newline at end of file diff --git a/package.json b/package.json index 4ab4c95..8049b02 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "test": "node --test --experimental-test-module-mocks **/*.spec.js", "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js", + "test:e2e": "node --test test/e2e/**/*.e2e.spec.js", "lint": "eslint ." }, "repository": { diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile new file mode 100644 index 0000000..0b9e24d --- /dev/null +++ b/test/e2e/Dockerfile @@ -0,0 +1,32 @@ +FROM node:18-alpine + +# Install bash and basic utilities (Alpine uses apk, not apt-get) +RUN apk add --no-cache bash curl + +# Create a test user to simulate real user environment (Alpine syntax) +RUN addgroup -S testuser && adduser -S testuser -G testuser -s /bin/bash + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the application +COPY . . + +# Switch to test user +USER testuser + +# Create home directory structure that bash expects +RUN mkdir -p /home/testuser + +# Set environment variables for testing +ENV HOME=/home/testuser +ENV SHELL=/bin/bash + +# Default command runs our test +CMD ["bash", "test/e2e/test-setup.sh"] \ No newline at end of file diff --git a/test/e2e/setup.e2e.spec.js b/test/e2e/setup.e2e.spec.js new file mode 100644 index 0000000..7c4d905 --- /dev/null +++ b/test/e2e/setup.e2e.spec.js @@ -0,0 +1,195 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert"; +import { execSync, spawn } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, "../.."); + +describe("E2E: safe-chain setup command", () => { + const imageName = "safe-chain-e2e-test"; + const containerName = "safe-chain-e2e-test-container"; + + before(async () => { + console.log("Building Docker image for e2e tests..."); + try { + execSync(`docker build -t ${imageName} -f test/e2e/Dockerfile .`, { + cwd: projectRoot, + stdio: "inherit", + }); + console.log("Docker image built successfully"); + } catch (error) { + throw new Error(`Failed to build Docker image: ${error.message}`); + } + }); + + after(async () => { + // Clean up: remove container and image + try { + execSync(`docker rm -f ${containerName}`, { stdio: "ignore" }); + } catch { + // Container might not exist, ignore + } + + try { + execSync(`docker rmi ${imageName}`, { stdio: "ignore" }); + } catch { + // Image might be in use, ignore + } + }); + + it("should successfully run safe-chain setup and create aliases", async () => { + // Run the container and capture output + const result = await runDockerTest([ + "node", "bin/safe-chain.js", "setup" + ]); + + // Verify setup completed successfully + assert.ok( + result.stdout.includes("Setup successful"), + "Setup should report success" + ); + + assert.strictEqual( + result.exitCode, + 0, + `Setup should exit with code 0, got ${result.exitCode}` + ); + }); + + it("should create correct aliases in .bashrc", async () => { + // Run setup and then check .bashrc contents + const result = await runDockerTest([ + "bash", "-c", ` + node bin/safe-chain.js setup && + echo "=== BASHRC CONTENTS ===" && + cat /home/testuser/.bashrc + ` + ]); + + assert.strictEqual(result.exitCode, 0, "Commands should succeed"); + + const bashrcContent = result.stdout; + + // Check for all expected aliases + const expectedAliases = [ + 'alias npm="aikido-npm" # Safe-chain alias for npm', + 'alias npx="aikido-npx" # Safe-chain alias for npx', + 'alias yarn="aikido-yarn" # Safe-chain alias for yarn', + 'alias pnpm="aikido-pnpm" # Safe-chain alias for pnpm', + 'alias pnpx="aikido-pnpx" # Safe-chain alias for pnpx' + ]; + + for (const expectedAlias of expectedAliases) { + assert.ok( + bashrcContent.includes(expectedAlias), + `Should contain alias: ${expectedAlias}` + ); + } + }); + + it("should be idempotent (not create duplicate aliases)", async () => { + // Run setup twice and check for duplicates + const result = await runDockerTest([ + "bash", "-c", ` + node bin/safe-chain.js setup && + node bin/safe-chain.js setup && + echo "=== ALIAS COUNT ===" && + grep -c 'alias npm="aikido-npm"' /home/testuser/.bashrc || echo 0 + ` + ]); + + assert.strictEqual(result.exitCode, 0, "Commands should succeed"); + + // Extract the count from output + const lines = result.stdout.split('\n'); + const countLine = lines.find(line => line.match(/^\d+$/)); + const aliasCount = parseInt(countLine || '0'); + + assert.strictEqual( + aliasCount, + 1, + `Should have exactly 1 npm alias, found ${aliasCount}` + ); + }); + + it("should work with fresh .bashrc file", async () => { + // Ensure no .bashrc exists initially + const result = await runDockerTest([ + "bash", "-c", ` + rm -f /home/testuser/.bashrc && + node bin/safe-chain.js setup && + test -f /home/testuser/.bashrc && echo "BASHRC_CREATED" || + echo "BASHRC_NOT_CREATED" + ` + ]); + + assert.strictEqual(result.exitCode, 0, "Commands should succeed"); + assert.ok( + result.stdout.includes("BASHRC_CREATED"), + ".bashrc should be created if it doesn't exist" + ); + }); + + it("should detect bash shell correctly", async () => { + const result = await runDockerTest([ + "node", "bin/safe-chain.js", "setup" + ]); + + assert.strictEqual(result.exitCode, 0, "Setup should succeed"); + assert.ok( + result.stdout.includes("Detected") && result.stdout.includes("Bash"), + "Should detect Bash shell" + ); + }); + + /** + * Helper function to run a command in Docker container and return result + */ + async function runDockerTest(command) { + return new Promise((resolve, reject) => { + const dockerArgs = [ + "run", "--rm", + "--name", containerName, + imageName, + ...command + ]; + + const child = spawn("docker", dockerArgs, { + cwd: projectRoot, + stdio: ["pipe", "pipe", "pipe"] + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + resolve({ + exitCode: code, + stdout, + stderr + }); + }); + + child.on("error", (error) => { + reject(new Error(`Docker command failed: ${error.message}`)); + }); + + // Set timeout to prevent hanging tests + setTimeout(() => { + child.kill(); + reject(new Error("Test timed out after 60 seconds")); + }, 60000); + }); + } +}); \ No newline at end of file