mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Fix e2e tests
This commit is contained in:
parent
c71320386e
commit
a7fc215354
4 changed files with 47 additions and 124 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { createRequire } from "module";
|
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
@ -81,7 +80,7 @@ function writeHelp() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
"safe-chain run"
|
"safe-chain run"
|
||||||
)}: Run the proxy as a standalone service. Sets system-wide proxy environment variables. Options: --verbose`
|
)}: Run the proxy as a standalone service. (Used by the background agent). Options: --verbose`
|
||||||
);
|
);
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
|
|
@ -99,7 +98,7 @@ function getVersion() {
|
||||||
const packageJsonPath = join(__dirname, '../package.json');
|
const packageJsonPath = join(__dirname, '../package.json');
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||||
return packageJson.version;
|
return packageJson.version;
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Fallback for bundled version
|
// Fallback for bundled version
|
||||||
return '1.0.0';
|
return '1.0.0';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import { generateCACertificate } from "../registryProxy/certUtils.js";
|
||||||
import { writeFileSync, mkdirSync } from "node:fs";
|
import { writeFileSync, mkdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate certificate command
|
* Generate certificate command
|
||||||
|
|
@ -34,7 +32,7 @@ export async function generateCertCommand(args) {
|
||||||
|
|
||||||
writeFileSync(certPath, cert);
|
writeFileSync(certPath, cert);
|
||||||
writeFileSync(keyPath, key);
|
writeFileSync(keyPath, key);
|
||||||
} catch (/** @type {any} */ error) {
|
} catch {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,34 @@ import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
|
|
||||||
const certFolder = process.env.SAFE_CHAIN_CERT_DIR || path.join(os.homedir(), ".safe-chain", "certs");
|
const certFolder = determineCertFolder();
|
||||||
const ca = loadCa();
|
const ca = loadCa();
|
||||||
|
|
||||||
const certCache = new Map();
|
const certCache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the certificate folder location
|
||||||
|
* Priority:
|
||||||
|
* 1. SAFE_CHAIN_CERT_DIR environment variable (set by installer/LaunchAgent)
|
||||||
|
* 2. System-wide installation directory (if certificates exist there)
|
||||||
|
* 3. User home directory (fallback for CLI wrapper mode and development)
|
||||||
|
*/
|
||||||
|
function determineCertFolder() {
|
||||||
|
// 1. Explicit env var takes precedence (set by LaunchAgent)
|
||||||
|
if (process.env.SAFE_CHAIN_CERT_DIR) {
|
||||||
|
return process.env.SAFE_CHAIN_CERT_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if system-wide installation exists (macOS/Linux standard location)
|
||||||
|
const systemCertDir = "/usr/local/share/safe-chain/certs";
|
||||||
|
if (fs.existsSync(path.join(systemCertDir, "ca-cert.pem"))) {
|
||||||
|
return systemCertDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to user home directory (CLI wrapper mode, development)
|
||||||
|
return path.join(os.homedir(), ".safe-chain", "certs");
|
||||||
|
}
|
||||||
|
|
||||||
export function getCaCertPath() {
|
export function getCaCertPath() {
|
||||||
return path.join(certFolder, "ca-cert.pem");
|
return path.join(certFolder, "ca-cert.pem");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { describe, it, before, after } from "node:test";
|
import { describe, it, before, after } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
||||||
import { join, dirname } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { parseShellOutput } from "./parseShellOutput.js";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const REPO_ROOT = join(__dirname, "../..");
|
const REPO_ROOT = join(__dirname, "../..");
|
||||||
|
|
@ -22,7 +20,6 @@ const AIKIDO_PIP_BIN = join(
|
||||||
REPO_ROOT,
|
REPO_ROOT,
|
||||||
"packages/safe-chain/bin/aikido-pip3.js"
|
"packages/safe-chain/bin/aikido-pip3.js"
|
||||||
);
|
);
|
||||||
const PROXY_STATE_FILE = join(homedir(), ".safe-chain/proxy-state.json");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to start safe-chain run in agent mode
|
* Helper to start safe-chain run in agent mode
|
||||||
|
|
@ -102,20 +99,26 @@ async function stopAgentMode(proc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.on("exit", () => {
|
let resolved = false;
|
||||||
|
const doResolve = () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
proc.on("exit", doResolve);
|
||||||
|
|
||||||
// Send SIGTERM
|
// Send SIGTERM
|
||||||
proc.kill("SIGTERM");
|
proc.kill("SIGTERM");
|
||||||
|
|
||||||
// Force kill after 2 seconds if still running
|
// Force kill after 1 second if still running
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!proc.killed) {
|
if (!proc.killed) {
|
||||||
proc.kill("SIGKILL");
|
proc.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
resolve();
|
doResolve();
|
||||||
}, 2000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,57 +188,9 @@ async function runAikidoPip(args) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and parse proxy state file
|
|
||||||
* @returns {{port: number, url: string, pid: number, ecosystem: string, certPath: string} | null}
|
|
||||||
*/
|
|
||||||
function readProxyState() {
|
|
||||||
try {
|
|
||||||
if (!existsSync(PROXY_STATE_FILE)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const content = readFileSync(PROXY_STATE_FILE, "utf-8");
|
|
||||||
const state = JSON.parse(content);
|
|
||||||
|
|
||||||
// Validate that process is still running (same as actual implementation)
|
|
||||||
try {
|
|
||||||
process.kill(state.pid, 0);
|
|
||||||
return state;
|
|
||||||
} catch {
|
|
||||||
// Process doesn't exist
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up proxy state file
|
|
||||||
*/
|
|
||||||
function cleanupProxyState() {
|
|
||||||
try {
|
|
||||||
if (existsSync(PROXY_STATE_FILE)) {
|
|
||||||
unlinkSync(PROXY_STATE_FILE);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
||||||
before(() => {
|
|
||||||
// Clean up any existing proxy state
|
|
||||||
cleanupProxyState();
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
// Clean up after tests
|
|
||||||
cleanupProxyState();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("safe-chain run", () => {
|
describe("safe-chain run", () => {
|
||||||
it("should start proxy and create state file", async () => {
|
it("should start proxy successfully", async () => {
|
||||||
let agent;
|
let agent;
|
||||||
try {
|
try {
|
||||||
// Start agent mode
|
// Start agent mode
|
||||||
|
|
@ -245,16 +200,6 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
||||||
assert.ok(agent.process);
|
assert.ok(agent.process);
|
||||||
assert.ok(agent.port > 0);
|
assert.ok(agent.port > 0);
|
||||||
assert.ok(agent.pid > 0);
|
assert.ok(agent.pid > 0);
|
||||||
|
|
||||||
// Verify state file was created
|
|
||||||
const state = readProxyState();
|
|
||||||
assert.ok(state, "State file should exist");
|
|
||||||
assert.strictEqual(state.port, agent.port);
|
|
||||||
assert.strictEqual(state.pid, agent.pid);
|
|
||||||
assert.strictEqual(state.ecosystem, "all");
|
|
||||||
assert.strictEqual(state.url, `http://localhost:${agent.port}`);
|
|
||||||
assert.ok(state.certPath);
|
|
||||||
assert.ok(state.certPath.includes(".safe-chain/certs/ca-cert.pem"));
|
|
||||||
} finally {
|
} finally {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
await stopAgentMode(agent.process);
|
await stopAgentMode(agent.process);
|
||||||
|
|
@ -268,10 +213,9 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
||||||
// Start agent mode with verbose flag
|
// Start agent mode with verbose flag
|
||||||
agent = await startAgentMode(["--verbose"]);
|
agent = await startAgentMode(["--verbose"]);
|
||||||
|
|
||||||
// Verify state file ecosystem is always 'all'
|
// Verify process started
|
||||||
const state = readProxyState();
|
assert.ok(agent.process);
|
||||||
assert.ok(state);
|
assert.ok(agent.port > 0);
|
||||||
assert.strictEqual(state.ecosystem, "all");
|
|
||||||
} finally {
|
} finally {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
await stopAgentMode(agent.process);
|
await stopAgentMode(agent.process);
|
||||||
|
|
@ -279,25 +223,21 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cleanup state file when proxy stops", async () => {
|
it("should stop cleanly", async () => {
|
||||||
let agent;
|
let agent;
|
||||||
try {
|
try {
|
||||||
// Start agent mode
|
// Start agent mode
|
||||||
agent = await startAgentMode();
|
agent = await startAgentMode();
|
||||||
|
|
||||||
// Verify state file exists
|
// Verify process is running
|
||||||
assert.ok(readProxyState());
|
assert.ok(agent.process);
|
||||||
|
|
||||||
// Stop agent
|
// Stop agent
|
||||||
await stopAgentMode(agent.process);
|
await stopAgentMode(agent.process);
|
||||||
agent = null;
|
agent = null;
|
||||||
|
|
||||||
// Wait a bit for cleanup
|
// Wait a bit for cleanup
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Verify state file was removed
|
|
||||||
const state = readProxyState();
|
|
||||||
assert.strictEqual(state, null, "State file should be removed");
|
|
||||||
} finally {
|
} finally {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
await stopAgentMode(agent.process);
|
await stopAgentMode(agent.process);
|
||||||
|
|
@ -393,15 +333,7 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inline mode (no agent)", () => {
|
describe("inline mode (no agent)", () => {
|
||||||
before(() => {
|
|
||||||
// Ensure no agent is running
|
|
||||||
cleanupProxyState();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should start inline proxy when no agent is running", async () => {
|
it("should start inline proxy when no agent is running", async () => {
|
||||||
// Verify no state file
|
|
||||||
assert.strictEqual(readProxyState(), null);
|
|
||||||
|
|
||||||
// Run aikido-npm without agent mode
|
// Run aikido-npm without agent mode
|
||||||
const result = await runAikidoNpm(["view", "lodash", "version"]);
|
const result = await runAikidoNpm(["view", "lodash", "version"]);
|
||||||
|
|
||||||
|
|
@ -410,35 +342,6 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => {
|
||||||
|
|
||||||
// Should have output
|
// Should have output
|
||||||
assert.ok(result.stdout.includes("4.17") || result.stdout.includes("lodash"));
|
assert.ok(result.stdout.includes("4.17") || result.stdout.includes("lodash"));
|
||||||
|
|
||||||
// State file should still not exist (inline mode doesn't create it)
|
|
||||||
assert.strictEqual(readProxyState(), null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("proxy state validation", () => {
|
|
||||||
it("should ignore stale state file with dead process", async () => {
|
|
||||||
// Create a fake state file with a non-existent PID
|
|
||||||
const fakeState = {
|
|
||||||
port: 12345,
|
|
||||||
url: "http://localhost:12345",
|
|
||||||
pid: 99999999, // Very unlikely to exist
|
|
||||||
ecosystem: "js",
|
|
||||||
certPath: join(homedir(), ".safe-chain/certs/ca-cert.pem"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write fake state file
|
|
||||||
const fs = await import("node:fs/promises");
|
|
||||||
const proxyStateDir = join(homedir(), ".safe-chain");
|
|
||||||
await fs.mkdir(proxyStateDir, { recursive: true });
|
|
||||||
await fs.writeFile(PROXY_STATE_FILE, JSON.stringify(fakeState, null, 2));
|
|
||||||
|
|
||||||
// Verify state file exists but process is dead
|
|
||||||
const state = readProxyState();
|
|
||||||
assert.strictEqual(state, null, "Should return null for dead process");
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
cleanupProxyState();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue