mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
First setup and teardown tests
This commit is contained in:
parent
73b209a5f6
commit
05ebb3f19e
6 changed files with 292 additions and 178 deletions
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"@inquirer/prompts": "^7.4.1",
|
"@inquirer/prompts": "^7.4.1",
|
||||||
"abbrev": "^3.0.1",
|
"abbrev": "^3.0.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
"node-pty": "^1.0.0",
|
||||||
"npm-registry-fetch": "^18.0.2",
|
"npm-registry-fetch": "^18.0.2",
|
||||||
"ora": "^8.2.0",
|
"ora": "^8.2.0",
|
||||||
"semver": "^7.7.2"
|
"semver": "^7.7.2"
|
||||||
|
|
@ -3905,6 +3906,12 @@
|
||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||||
|
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
|
|
@ -3921,6 +3928,16 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-pty": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nan": "^2.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/npm-package-arg": {
|
"node_modules/npm-package-arg": {
|
||||||
"version": "12.0.2",
|
"version": "12.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"@inquirer/prompts": "^7.4.1",
|
"@inquirer/prompts": "^7.4.1",
|
||||||
"abbrev": "^3.0.1",
|
"abbrev": "^3.0.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
"node-pty": "^1.0.0",
|
||||||
"npm-registry-fetch": "^18.0.2",
|
"npm-registry-fetch": "^18.0.2",
|
||||||
"ora": "^8.2.0",
|
"ora": "^8.2.0",
|
||||||
"semver": "^7.7.2"
|
"semver": "^7.7.2"
|
||||||
|
|
|
||||||
113
test/e2e/DockerTestContainer.js
Normal file
113
test/e2e/DockerTestContainer.js
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import * as pty from "node-pty";
|
||||||
|
import { parseShellOutput } from "./parseShellOutput.js";
|
||||||
|
|
||||||
|
export class DockerTestContainer {
|
||||||
|
constructor(imageName, containerName) {
|
||||||
|
this.imageName = imageName;
|
||||||
|
this.containerName = containerName;
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
throw new Error("Container is already running");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start a long-running container that we can exec commands into
|
||||||
|
execSync(
|
||||||
|
`docker run -d --name ${this.containerName} ${this.imageName} sleep infinity`,
|
||||||
|
{ stdio: "ignore" }
|
||||||
|
);
|
||||||
|
this.isRunning = true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to start container: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openShell(shell) {
|
||||||
|
let ptyProcess = pty.spawn(
|
||||||
|
"docker",
|
||||||
|
["exec", "-it", this.containerName, shell],
|
||||||
|
{
|
||||||
|
name: "xterm-color",
|
||||||
|
cols: 80,
|
||||||
|
rows: 30,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ptyProcess.on("data", (data) => {
|
||||||
|
if (data.includes("\u001b[?2004h")) {
|
||||||
|
// This indicates that the shell is ready
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ptyProcess.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function runCommand(command) {
|
||||||
|
if (!ptyProcess) {
|
||||||
|
throw new Error("Shell is not running");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let allData = [];
|
||||||
|
|
||||||
|
ptyProcess.on("data", handleInput);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
// Fallback in case the command doesn't finish in a reasonable time
|
||||||
|
resolve({ allData, output: parseShellOutput(allData), command });
|
||||||
|
ptyProcess.removeListener("data", handleInput);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
function handleInput(data) {
|
||||||
|
allData.push(data);
|
||||||
|
|
||||||
|
if (data.includes("\u001b[?2004h")) {
|
||||||
|
// This indicates that the command has finished executing
|
||||||
|
resolve({ allData, output: parseShellOutput(allData), command });
|
||||||
|
ptyProcess.removeListener("data", handleInput);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ptyProcess.write(`${command}\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { runCommand };
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return; // Already stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force stop and remove the container
|
||||||
|
execSync(`docker kill ${this.containerName}`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Container might already be stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`docker rm -f ${this.containerName}`, {
|
||||||
|
stdio: "ignore",
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Container might already be removed
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
FROM node:18-alpine
|
FROM node:24 as builder
|
||||||
|
|
||||||
# Install bash and basic utilities (Alpine uses apk, not apt-get)
|
ENV CI=true
|
||||||
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
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
@ -18,15 +14,20 @@ RUN npm install
|
||||||
# Copy the rest of the application
|
# Copy the rest of the application
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Switch to test user
|
# Build the application
|
||||||
USER testuser
|
RUN npm --no-git-tag-version version 1.0.0 --allow-same-version
|
||||||
|
RUN npm pack
|
||||||
|
|
||||||
# Create home directory structure that bash expects
|
FROM mcr.microsoft.com/devcontainers/javascript-node as runner
|
||||||
RUN mkdir -p /home/testuser
|
|
||||||
|
|
||||||
# Set environment variables for testing
|
WORKDIR /app
|
||||||
ENV HOME=/home/testuser
|
|
||||||
ENV SHELL=/bin/bash
|
|
||||||
|
|
||||||
# Default command runs our test
|
COPY --from=builder /app/*.tgz /app/
|
||||||
CMD ["bash", "test/e2e/test-setup.sh"]
|
|
||||||
|
# # Install the application package globally
|
||||||
|
RUN npm install -g /app/*.tgz
|
||||||
|
|
||||||
|
RUN mkdir /testapp
|
||||||
|
RUN cd /testapp && npm init -y
|
||||||
|
|
||||||
|
# ENV SHELL=/bin/bash
|
||||||
|
|
|
||||||
100
test/e2e/parseShellOutput.js
Normal file
100
test/e2e/parseShellOutput.js
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
const escapeChar = "\u001b";
|
||||||
|
const startMarker = `${escapeChar}[?2004l`;
|
||||||
|
const endMarker = `${escapeChar}[?2004h`;
|
||||||
|
|
||||||
|
export function parseShellOutput(rawData) {
|
||||||
|
const stringData = rawData.join("");
|
||||||
|
|
||||||
|
let output = getDataBetweenStartAndEndMarkers(stringData);
|
||||||
|
output = processBackspaces(output);
|
||||||
|
output = processEraseCommands(output);
|
||||||
|
output = removeOscSequences(output);
|
||||||
|
output = removeAnsiSgrSequences(output);
|
||||||
|
output = removeRemainingEscapeSequences(output);
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataBetweenStartAndEndMarkers(data) {
|
||||||
|
if (!data.includes(startMarker) || !data.includes(endMarker)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = data.indexOf(startMarker);
|
||||||
|
const endIndex = data.indexOf(endMarker, startIndex + startMarker.length);
|
||||||
|
|
||||||
|
if (startIndex === -1 || endIndex === -1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.slice(startIndex + startMarker.length, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processBackspaces(data) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const char = data[i];
|
||||||
|
|
||||||
|
if (char === "\b") {
|
||||||
|
// Backspace: remove the previous character if it exists
|
||||||
|
if (result.length > 0) {
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOscSequences(data) {
|
||||||
|
return data.replace(/\u001b\][0-9]*;[^\u0007\u001b]*(\u0007|\u001b\\)/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAnsiSgrSequences(data) {
|
||||||
|
return data.replace(/\u001b\[[0-9;]*m/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function processEraseCommands(data) {
|
||||||
|
const lines = data.split("\n");
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
// Process erase in line commands
|
||||||
|
line = line.replace(/\u001b\[K/g, ""); // Erase to end of line
|
||||||
|
line = line.replace(/\u001b\[0K/g, ""); // Erase to end of line
|
||||||
|
line = line.replace(/\u001b\[1K/g, ""); // Erase from start of line to cursor
|
||||||
|
line = line.replace(/\u001b\[2K/g, ""); // Erase entire line - remove the whole line
|
||||||
|
|
||||||
|
// Skip lines that were completely erased
|
||||||
|
if (line.includes("\u001b[2K")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process erase in display commands
|
||||||
|
let output = result.join("\n");
|
||||||
|
output = output.replace(/\u001b\[J/g, ""); // Erase to end of display
|
||||||
|
output = output.replace(/\u001b\[0J/g, ""); // Erase to end of display
|
||||||
|
output = output.replace(/\u001b\[1J/g, ""); // Erase from start to cursor
|
||||||
|
output = output.replace(/\u001b\[2J/g, ""); // Erase entire display
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRemainingEscapeSequences(data) {
|
||||||
|
// Remove mode setting sequences like \u001b[?1h, \u001b[?1l
|
||||||
|
data = data.replace(/\u001b\[\?[0-9]+[hl]/g, "");
|
||||||
|
|
||||||
|
// Remove any other CSI sequences we haven't handled
|
||||||
|
data = data.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "");
|
||||||
|
|
||||||
|
// Remove incomplete or malformed escape sequences
|
||||||
|
data = data.replace(/\u001b[^\u001b]*/g, "");
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { describe, it, before, after } from "node:test";
|
import { describe, it, before, beforeEach, afterEach } from "node:test";
|
||||||
import assert from "node:assert";
|
import { execSync } from "node:child_process";
|
||||||
import { execSync, spawn } from "node:child_process";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { DockerTestContainer } from "./DockerTestContainer.js";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
@ -11,185 +12,66 @@ const projectRoot = path.resolve(__dirname, "../..");
|
||||||
describe("E2E: safe-chain setup command", () => {
|
describe("E2E: safe-chain setup command", () => {
|
||||||
const imageName = "safe-chain-e2e-test";
|
const imageName = "safe-chain-e2e-test";
|
||||||
const containerName = "safe-chain-e2e-test-container";
|
const containerName = "safe-chain-e2e-test-container";
|
||||||
|
let container;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
console.log("Building Docker image for e2e tests...");
|
// Build the Docker image for the test environment
|
||||||
try {
|
try {
|
||||||
execSync(`docker build -t ${imageName} -f test/e2e/Dockerfile .`, {
|
execSync(`docker build -t ${imageName} -f test/e2e/Dockerfile .`, {
|
||||||
cwd: projectRoot,
|
cwd: projectRoot,
|
||||||
stdio: "inherit",
|
stdio: "ignore",
|
||||||
});
|
});
|
||||||
console.log("Docker image built successfully");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to build Docker image: ${error.message}`);
|
throw new Error(`Failed to setup test environment: ${error.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
beforeEach(async () => {
|
||||||
// Clean up: remove container and image
|
// Run a new Docker container for each test
|
||||||
try {
|
container = new DockerTestContainer(imageName, containerName);
|
||||||
execSync(`docker rm -f ${containerName}`, { stdio: "ignore" });
|
|
||||||
} catch {
|
|
||||||
// Container might not exist, ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
await container.start();
|
||||||
execSync(`docker rmi ${imageName}`, { stdio: "ignore" });
|
});
|
||||||
} catch {
|
|
||||||
// Image might be in use, ignore
|
afterEach(async () => {
|
||||||
|
// Stop and clean up the container after each test
|
||||||
|
if (container) {
|
||||||
|
await container.stop();
|
||||||
|
container = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should successfully run safe-chain setup and create aliases", async () => {
|
for (let shell of ["bash", "zsh"]) {
|
||||||
// Run the container and capture output
|
it(`safe-chain setup wraps npm command after installation for ${shell}`, async () => {
|
||||||
const result = await runDockerTest([
|
// setting up the container
|
||||||
"node", "bin/safe-chain.js", "setup"
|
const installationShell = await container.openShell(shell);
|
||||||
]);
|
await installationShell.runCommand("safe-chain setup");
|
||||||
|
|
||||||
// Verify setup completed successfully
|
const projectShell = await container.openShell(shell);
|
||||||
assert.ok(
|
await projectShell.runCommand("cd /testapp");
|
||||||
result.stdout.includes("Setup successful"),
|
const result = await projectShell.runCommand("npm i axios");
|
||||||
"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(
|
assert.ok(
|
||||||
bashrcContent.includes(expectedAlias),
|
result.output.includes("Scanning for malicious packages..."),
|
||||||
`Should contain alias: ${expectedAlias}`
|
"Expected npm command to be wrapped by safe-chain"
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should be idempotent (not create duplicate aliases)", async () => {
|
it(`safe-chain teardown unwraps npm command after uninstallation for ${shell}`, async () => {
|
||||||
// Run setup twice and check for duplicates
|
// setting up the container
|
||||||
const result = await runDockerTest([
|
const installationShell = await container.openShell(shell);
|
||||||
"bash", "-c", `
|
await installationShell.runCommand("safe-chain setup");
|
||||||
node bin/safe-chain.js setup &&
|
await installationShell.runCommand("safe-chain teardown");
|
||||||
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");
|
const projectShell = await container.openShell(shell);
|
||||||
|
await projectShell.runCommand("cd /testapp");
|
||||||
|
await projectShell.runCommand("npm i axios");
|
||||||
|
const result = await projectShell.runCommand("npm i axios");
|
||||||
|
|
||||||
// Extract the count from output
|
assert.ok(
|
||||||
const lines = result.stdout.split('\n');
|
!result.output.includes("Scanning for malicious packages..."),
|
||||||
const countLine = lines.find(line => line.match(/^\d+$/));
|
"Expected npm command to not be wrapped by safe-chain after teardown"
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue