Improve e2e tests: add npm install tests, add test matrix

This commit is contained in:
Sander Declerck 2025-09-16 10:53:19 +02:00
parent 45b43366d2
commit 753f3cd837
No known key found for this signature in database
5 changed files with 172 additions and 35 deletions

View file

@ -40,6 +40,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
include:
- node_version: "lts"
npm_version: "latest"
yarn_version: "latest"
pnpm_version: "latest"
- node_version: "22"
npm_version: "10.0.0"
yarn_version: "latest"
pnpm_version: "latest"
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -53,6 +65,11 @@ jobs:
run: npm ci run: npm ci
- name: Run E2E tests - name: Run E2E tests
env:
NODE_VERSION: ${{ matrix.node_version }}
NPM_VERSION: ${{ matrix.npm_version }}
YARN_VERSION: ${{ matrix.yarn_version }}
PNPM_VERSION: ${{ matrix.pnpm_version }}
run: npm run test:e2e run: npm run test:e2e
- name: Clean up Docker resources - name: Clean up Docker resources

View file

@ -1,14 +1,48 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import path from "node:path";
import * as pty from "node-pty"; import * as pty from "node-pty";
import { parseShellOutput } from "./parseShellOutput.js"; import { parseShellOutput } from "./parseShellOutput.js";
const imageName = "safe-chain-e2e-test";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dockerFile = path.join(__dirname, "Dockerfile");
const contextPath = path.join(__dirname, "../..");
const nodeVersion = process.env.NODE_VERSION || "lts";
const npmVersion = process.env.NPM_VERSION || "latest";
const yarnVersion = process.env.YARN_VERSION || "latest";
const pnpmVersion = process.env.PNPM_VERSION || "latest";
export class DockerTestContainer { export class DockerTestContainer {
constructor(imageName, containerName) { constructor() {
this.imageName = imageName; this.containerName = `safe-chain-test-${Math.random()
this.containerName = containerName; .toString(36)
.substring(2, 15)}`;
this.isRunning = false; this.isRunning = false;
} }
static buildImage() {
try {
const buildArgs = [
`--build-arg NODE_VERSION=${nodeVersion}`,
`--build-arg NPM_VERSION=${npmVersion}`,
`--build-arg YARN_VERSION=${yarnVersion}`,
`--build-arg PNPM_VERSION=${pnpmVersion}`,
].join(" ");
execSync(
`docker build -t ${imageName} -f ${dockerFile} ${contextPath} ${buildArgs}`,
{
stdio: "ignore",
}
);
} catch (error) {
throw new Error(`Failed to build Docker image: ${error.message}`);
}
}
async start() { async start() {
if (this.isRunning) { if (this.isRunning) {
throw new Error("Container is already running"); throw new Error("Container is already running");
@ -17,7 +51,7 @@ export class DockerTestContainer {
try { try {
// Start a long-running container that we can exec commands into // Start a long-running container that we can exec commands into
execSync( execSync(
`docker run -d --name ${this.containerName} ${this.imageName} sleep infinity`, `docker run -d --name ${this.containerName} ${imageName} sleep infinity`,
{ stdio: "ignore" } { stdio: "ignore" }
); );
this.isRunning = true; this.isRunning = true;

View file

@ -18,15 +18,36 @@ COPY packages/safe-chain ./
RUN npm --no-git-tag-version version 1.0.0 --allow-same-version RUN npm --no-git-tag-version version 1.0.0 --allow-same-version
RUN npm pack RUN npm pack
FROM mcr.microsoft.com/devcontainers/javascript-node:22-bookworm as runner FROM buildpack-deps:trixie
WORKDIR /app # Package manager version arguments with defaults
ARG NODE_VERSION=latest
ARG NPM_VERSION=latest
ARG YARN_VERSION=latest
ARG PNPM_VERSION=latest
COPY --from=builder /app/*.tgz /app/ SHELL ["/bin/bash", "-c"]
ENV BASH_ENV=~/.bashrc
# # Install the application package globally # Install zsh
RUN npm install -g /app/*.tgz RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.1/zsh-in-docker.sh)"
# Install fish
RUN apt-get install -y fish && \
mkdir -p /root/.config/fish/ && \
touch /root/.config/fish/config.fish
RUN mkdir /testapp # Install Volta and Node.js
RUN cd /testapp && npm init -y RUN curl https://get.volta.sh | bash
RUN volta install node@${NODE_VERSION}
RUN volta install npm@${NPM_VERSION}
RUN volta install yarn@${YARN_VERSION}
RUN volta install pnpm@${PNPM_VERSION}
# Copy and install Safe chain
COPY --from=builder /app/*.tgz /pkgs/
# RUN npm install -g /pkgs/*.tgz
RUN npm install -g @aikidosec/safe-chain@1.0.21
WORKDIR /testapp
RUN npm init -y

80
test/e2e/npm.e2e.spec.js Normal file
View file

@ -0,0 +1,80 @@
import { describe, it, before, beforeEach, afterEach } from "node:test";
import { DockerTestContainer } from "./DockerTestContainer.js";
import assert from "node:assert";
describe("E2E: npm coverage", () => {
let container;
before(async () => {
DockerTestContainer.buildImage();
});
beforeEach(async () => {
// Run a new Docker container for each test
container = new DockerTestContainer();
await container.start();
const installationShell = await container.openShell("zsh");
await installationShell.runCommand("safe-chain setup");
});
afterEach(async () => {
// Stop and clean up the container after each test
if (container) {
await container.stop();
container = null;
}
});
it(`safe-chain succesfully installs safe packages`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("npm i axios");
assert.ok(
result.output.includes("No malicious packages detected."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
it(`safe-chain blocks installation of malicious packages`, async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("npm i safe-chain-test");
assert.ok(
result.output.includes("Malicious changes detected:"),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("- safe-chain-test"),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("Exiting without installing malicious packages."),
`Output did not include expected text. Output was:\n${result.output}`
);
const listResult = await shell.runCommand("npm list");
assert.ok(
!listResult.output.includes("safe-chain-test"),
`Malicious package was installed despite safe-chain protection. Output of 'npm list' was:\n${listResult.output}`
);
});
it("safe-chain blocks npx from executing malicious packages", async () => {
const shell = await container.openShell("zsh");
const result = await shell.runCommand("npx safe-chain-test");
assert.ok(
result.output.includes("Malicious changes detected:"),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("- safe-chain-test"),
`Output did not include expected text. Output was:\n${result.output}`
);
assert.ok(
result.output.includes("Exiting without installing malicious packages."),
`Output did not include expected text. Output was:\n${result.output}`
);
});
});

View file

@ -1,40 +1,20 @@
import { describe, it, before, beforeEach, afterEach } from "node:test"; import { describe, it, before, beforeEach, afterEach } from "node:test";
import { execSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DockerTestContainer } from "./DockerTestContainer.js"; import { DockerTestContainer } from "./DockerTestContainer.js";
import assert from "node:assert"; import assert from "node:assert";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
describe("E2E: safe-chain setup command", () => { describe("E2E: safe-chain setup command", () => {
const imageName = "safe-chain-e2e-test";
const containerName = "safe-chain-e2e-test-container";
let container; let container;
before(async () => { before(async () => {
// Build the Docker image for the test environment DockerTestContainer.buildImage();
try {
const sourceDir = path.join(__dirname, "../..");
execSync(`docker build -t ${imageName} -f Dockerfile ${sourceDir}`, {
cwd: __dirname,
stdio: "ignore",
});
} catch (error) {
throw new Error(`Failed to setup test environment: ${error.message}`);
}
}); });
beforeEach(async () => { beforeEach(async () => {
// Run a new Docker container for each test container = new DockerTestContainer();
container = new DockerTestContainer(imageName, containerName);
await container.start(); await container.start();
}); });
afterEach(async () => { afterEach(async () => {
// Stop and clean up the container after each test
if (container) { if (container) {
await container.stop(); await container.stop();
container = null; container = null;
@ -51,9 +31,14 @@ describe("E2E: safe-chain setup command", () => {
await projectShell.runCommand("cd /testapp"); await projectShell.runCommand("cd /testapp");
const result = await projectShell.runCommand("npm i axios"); const result = await projectShell.runCommand("npm i axios");
const hasExpectedOutput = result.output.includes(
"Scanning for malicious packages..."
);
assert.ok( assert.ok(
result.output.includes("Scanning for malicious packages..."), hasExpectedOutput,
"Expected npm command to be wrapped by safe-chain" hasExpectedOutput
? "Expected npm command to be wrapped by safe-chain"
: `Output did not contain "Scanning for malicious packages...": \n${result.output}`
); );
}); });