Fix tests and add command support

This commit is contained in:
Reinier Criel 2025-12-18 10:33:31 +01:00
parent b9de94f0f1
commit d2fc531c81
14 changed files with 198 additions and 462 deletions

View file

@ -10,6 +10,7 @@ import {
} from "./pnpm/createPackageManager.js";
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
import { createPipPackageManager } from "./pip/createPackageManager.js";
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
@ -61,6 +62,8 @@ export function initializePackageManager(packageManagerName, context) {
state.packageManagerName = createUvPackageManager();
} else if (packageManagerName === "poetry") {
state.packageManagerName = createPoetryPackageManager();
} else if (packageManagerName === "pipx") {
state.packageManagerName = createPipXPackageManager();
} else {
throw new Error("Unsupported package manager: " + packageManagerName);
}

View file

@ -11,7 +11,7 @@ export function createPipXPackageManager() {
runCommand: (args) => {
return runPipX("pipx", args);
},
// For uv, rely solely on MITM
// MITM only
isSupportedCommand: () => false,
getDependencyUpdatesForCommand: () => [],
};

View file

@ -5,7 +5,7 @@ import { createPipXPackageManager } from "./createPipXPackageManager.js";
test("createPipXPackageManager", async (t) => {
await t.test("should create package manager with required interface", () => {
const pm = createPipXPackageManager();
assert.ok(pm);
assert.strictEqual(typeof pm.runCommand, "function");
assert.strictEqual(typeof pm.isSupportedCommand, "function");

View file

@ -16,7 +16,7 @@ function setPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
}
env.SSL_CERT_FILE = combinedCaPath;
// REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
// REQUESTS_CA_BUNDLE: Used by the requests library (may be used by tooling under pipx)
if (env.REQUESTS_CA_BUNDLE) {
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
}
@ -30,18 +30,11 @@ function setPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
}
/**
* Runs a uv command with safe-chain's certificate bundle and proxy configuration.
* Runs a pipx command with safe-chain's certificate bundle and proxy configuration.
*
* uv respects standard environment variables for proxy and TLS configuration:
* - HTTP_PROXY / HTTPS_PROXY: Proxy settings
* - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
*
* Unlike pip (which requires a temporary config file for cert configuration), uv directly
* honors environment variables, so no config/ini file is needed.
*
* @param {string} command - The pipx command to execute
* @param {string[]} args - Command line arguments to pass to pipx
* @returns {Promise<{status: number}>} Exit status of the pipx command
* @param {string} command - The command to execute
* @param {string[]} args - Command line arguments
* @returns {Promise<{status: number}>} Exit status of the command
*/
export async function runPipX(command, args) {
try {

View file

@ -0,0 +1,100 @@
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
describe("runPipXCommand", () => {
let runPipX;
let safeSpawnMock;
let warnMock;
let errorMock;
let mergeCalls;
let mergedEnvReturn;
beforeEach(async () => {
mergeCalls = [];
mergedEnvReturn = {
HTTPS_PROXY: "http://localhost:8080",
HTTP_PROXY: "",
};
safeSpawnMock = mock.fn(async () => ({ status: 0 }));
warnMock = mock.fn();
errorMock = mock.fn();
mock.module("../../environment/userInteraction.js", {
namedExports: {
ui: {
writeWarning: warnMock,
writeError: errorMock,
writeInfo: () => {},
writeVerbose: () => {},
writeSuccess: () => {},
},
},
});
mock.module("../../registryProxy/registryProxy.js", {
namedExports: {
mergeSafeChainProxyEnvironmentVariables: (env) => {
mergeCalls.push(env);
return { ...env, ...mergedEnvReturn };
},
},
});
mock.module("../../registryProxy/certBundle.js", {
namedExports: {
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
},
});
mock.module("../../utils/safeSpawn.js", {
namedExports: {
safeSpawn: safeSpawnMock,
},
});
const mod = await import("./runPipXCommand.js");
runPipX = mod.runPipX;
});
afterEach(() => {
mock.reset();
});
it("sets CA env vars and proxies before spawning", async () => {
const res = await runPipX("pipx", ["install", "ruff"]);
assert.strictEqual(res.status, 0);
assert.strictEqual(safeSpawnMock.mock.calls.length, 1, "safeSpawn should be called once");
const [, , options] = safeSpawnMock.mock.calls[0].arguments;
const env = options.env;
assert.strictEqual(env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem");
assert.strictEqual(env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem");
assert.strictEqual(env.PIP_CERT, "/tmp/test-combined-ca.pem");
assert.strictEqual(env.HTTPS_PROXY, "http://localhost:8080");
assert.strictEqual(env.HTTP_PROXY, "");
assert.ok(mergeCalls.length >= 1, "proxy merge should be invoked");
});
it("overwrites user CA env vars and warns", async () => {
mergedEnvReturn = {
HTTPS_PROXY: "http://localhost:8080",
HTTP_PROXY: "",
SSL_CERT_FILE: "user-ssl",
REQUESTS_CA_BUNDLE: "user-requests",
PIP_CERT: "user-pip",
};
await runPipX("pipx", ["install", "ruff"]);
const [, , options] = safeSpawnMock.mock.calls[0].arguments;
const env = options.env;
assert.strictEqual(env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem", "SSL cert should be overwritten");
assert.strictEqual(env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem", "requests bundle should be overwritten");
assert.strictEqual(env.PIP_CERT, "/tmp/test-combined-ca.pem", "pip cert should be overwritten");
assert.strictEqual(warnMock.mock.calls.length, 3, "should warn for each overwritten var");
});
});