// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // Tests for dep-review.js — run with: node --test .github/scripts/dep-review.test.js "use strict"; const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const { parseExternalDeps, fmtSource, diffDeps, buildSummary, parseCratePathMap, parseInternalDepGraph, checkContainment, diffContainmentViolations, buildPolicySummary, run, } = require("./dep-review.js"); // -- fixtures -- const LOCK_MINIMAL = ` # This file is automatically @generated by Cargo. version = 4 [[package]] name = "my_crate" version = "0.0.0" dependencies = [ "serde", "helper", ] [[package]] name = "helper" version = "0.0.0" [[package]] name = "serde" version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abc" [[package]] name = "serde_derive" version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def" `; const MANIFEST_MINIMAL = ` [workspace] members = ["my_crate", "helper", "lib_a", "lib_b"] [workspace.dependencies] my_crate = { path = "app/my_crate" } helper = { path = "support/helper" } lib_a = { path = "support/lib_a" } lib_b = { path = "vm/lib_b" } serde = "1.0" `; const POLICY = { containment: [ { prefix: "support/", description: "support/ crates must not depend on crates outside support/", }, ], }; // -- parseExternalDeps -- describe("parseExternalDeps", () => { it("extracts external packages (those with source)", () => { const deps = parseExternalDeps(LOCK_MINIMAL); assert.equal(deps.size, 2); assert.ok( deps.has("serde\t1.0.200\tregistry+https://github.com/rust-lang/crates.io-index") ); assert.ok( deps.has("serde_derive\t1.0.200\tregistry+https://github.com/rust-lang/crates.io-index") ); }); it("ignores internal packages (no source)", () => { const deps = parseExternalDeps(LOCK_MINIMAL); for (const key of deps) { assert.ok(!key.startsWith("my_crate\t")); assert.ok(!key.startsWith("helper\t")); } }); it("handles empty lockfile", () => { const deps = parseExternalDeps("version = 4\n"); assert.equal(deps.size, 0); }); it("handles git sources", () => { const lock = ` [[package]] name = "foo" version = "0.1.0" source = "git+https://github.com/org/foo?branch=main#abc123" `; const deps = parseExternalDeps(lock); assert.equal(deps.size, 1); const key = [...deps][0]; assert.ok(key.startsWith("foo\t0.1.0\tgit+")); }); it("tracks multiple versions of the same crate", () => { const lock = ` [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaa" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb" `; const deps = parseExternalDeps(lock); assert.equal(deps.size, 2); assert.ok( deps.has("windows-sys\t0.48.0\tregistry+https://github.com/rust-lang/crates.io-index") ); assert.ok( deps.has("windows-sys\t0.52.0\tregistry+https://github.com/rust-lang/crates.io-index") ); }); }); // -- fmtSource -- describe("fmtSource", () => { it("returns empty for registry source", () => { assert.equal(fmtSource("registry+https://github.com/rust-lang/crates.io-index"), ""); }); it("formats git source with URL", () => { const result = fmtSource("git+https://github.com/org/repo?branch=main#abc123"); assert.equal(result, " (https://github.com/org/repo?branch=main)"); }); it("passes through unknown sources", () => { assert.equal(fmtSource("something-else"), " (something-else)"); }); }); // -- diffDeps -- describe("diffDeps", () => { it("detects added crates", () => { const base = new Set(); const pr = new Set(["foo\t1.0.0\tsrc"]); const { added } = diffDeps(base, pr); assert.equal(added.length, 1); assert.equal(added[0].name, "foo"); }); it("detects version changes as additions", () => { const base = new Set(["foo\t1.0.0\tsrc"]); const pr = new Set(["foo\t2.0.0\tsrc"]); const { added } = diffDeps(base, pr); assert.equal(added.length, 1); assert.equal(added[0].name, "foo"); assert.equal(added[0].version, "2.0.0"); }); it("does not report removals", () => { const base = new Set(["foo\t1.0.0\tsrc"]); const pr = new Set(); const { added } = diffDeps(base, pr); assert.equal(added.length, 0); }); it("reports nothing when identical", () => { const base = new Set(["foo\t1.0.0\tsrc"]); const pr = new Set(["foo\t1.0.0\tsrc"]); const { added } = diffDeps(base, pr); assert.equal(added.length, 0); }); it("detects new version alongside existing version", () => { const base = new Set(["foo\t1.0.0\tsrc"]); const pr = new Set(["foo\t1.0.0\tsrc", "foo\t2.0.0\tsrc"]); const { added } = diffDeps(base, pr); assert.equal(added.length, 1); assert.equal(added[0].version, "2.0.0"); }); }); // -- buildSummary -- describe("buildSummary", () => { it("includes added crates", () => { const summary = buildSummary({ added: [{ name: "foo", version: "1.0.0", source: "registry+x" }], }); assert.ok(summary.includes("**New external crate versions:**")); assert.ok(summary.includes("`foo` 1.0.0")); }); }); // -- parseCratePathMap -- describe("parseCratePathMap", () => { it("extracts path-based dependencies", () => { const map = parseCratePathMap(MANIFEST_MINIMAL); assert.equal(map.get("my_crate"), "app/my_crate"); assert.equal(map.get("helper"), "support/helper"); assert.equal(map.get("lib_a"), "support/lib_a"); assert.equal(map.get("lib_b"), "vm/lib_b"); }); it("ignores non-path dependencies", () => { const map = parseCratePathMap(MANIFEST_MINIMAL); assert.ok(!map.has("serde")); }); it("handles complex inline tables", () => { const toml = `foo = { path = "a/b", features = ["x"], default-features = false }`; const map = parseCratePathMap(toml); assert.equal(map.get("foo"), "a/b"); }); }); // -- parseInternalDepGraph -- describe("parseInternalDepGraph", () => { it("extracts internal crates and their dependencies", () => { const { graph, internalCrates } = parseInternalDepGraph(LOCK_MINIMAL); assert.ok(internalCrates.has("my_crate")); assert.ok(internalCrates.has("helper")); assert.ok(!internalCrates.has("serde")); assert.deepEqual(graph.get("my_crate"), ["serde", "helper"]); assert.deepEqual(graph.get("helper"), []); }); }); // -- checkContainment -- describe("checkContainment", () => { it("flags violations when support/ crate depends on non-support/ crate", () => { // helper (support/helper) depends on lib_b (vm/lib_b) const lock = ` [[package]] name = "helper" version = "0.0.0" dependencies = [ "lib_b", ] [[package]] name = "lib_b" version = "0.0.0" `; const { graph, internalCrates } = parseInternalDepGraph(lock); const pathMap = parseCratePathMap(MANIFEST_MINIMAL); const violations = checkContainment(graph, internalCrates, pathMap, POLICY); assert.equal(violations.length, 1); assert.equal(violations[0].crate, "helper"); assert.equal(violations[0].dep, "lib_b"); }); it("allows support/ crate depending on other support/ crate", () => { const lock = ` [[package]] name = "helper" version = "0.0.0" dependencies = [ "lib_a", ] [[package]] name = "lib_a" version = "0.0.0" `; const { graph, internalCrates } = parseInternalDepGraph(lock); const pathMap = parseCratePathMap(MANIFEST_MINIMAL); const violations = checkContainment(graph, internalCrates, pathMap, POLICY); assert.equal(violations.length, 0); }); it("ignores non-support/ crates depending on anything", () => { const lock = ` [[package]] name = "my_crate" version = "0.0.0" dependencies = [ "helper", "lib_b", ] [[package]] name = "helper" version = "0.0.0" [[package]] name = "lib_b" version = "0.0.0" `; const { graph, internalCrates } = parseInternalDepGraph(lock); const pathMap = parseCratePathMap(MANIFEST_MINIMAL); const violations = checkContainment(graph, internalCrates, pathMap, POLICY); assert.equal(violations.length, 0); }); }); // -- diffContainmentViolations -- describe("diffContainmentViolations", () => { const v1 = { crate: "a", dep: "b", cratePath: "support/a", depPath: "vm/b", rule: "test" }; const v2 = { crate: "c", dep: "d", cratePath: "support/c", depPath: "vm/d", rule: "test" }; it("returns only new violations", () => { const result = diffContainmentViolations([v1], [v1, v2]); assert.equal(result.length, 1); assert.equal(result[0].crate, "c"); }); it("returns empty when no new violations", () => { const result = diffContainmentViolations([v1, v2], [v1]); assert.equal(result.length, 0); }); it("returns empty when identical", () => { const result = diffContainmentViolations([v1], [v1]); assert.equal(result.length, 0); }); }); // -- buildPolicySummary -- describe("buildPolicySummary", () => { it("returns empty for no violations", () => { assert.equal(buildPolicySummary([]), ""); }); it("formats violations", () => { const summary = buildPolicySummary([ { crate: "a", cratePath: "support/a", dep: "b", depPath: "vm/b", rule: "no cross" }, ]); assert.ok(summary.includes("`a` (support/a) → `b` (vm/b)")); assert.ok(summary.includes("no cross")); }); }); // -- integration: full lockfile round-trip -- describe("integration", () => { it("no issues when base and PR are identical", () => { const baseDeps = parseExternalDeps(LOCK_MINIMAL); const prDeps = parseExternalDeps(LOCK_MINIMAL); const diff = diffDeps(baseDeps, prDeps); assert.equal(diff.added.length, 0); const pathMap = parseCratePathMap(MANIFEST_MINIMAL); const baseGraph = parseInternalDepGraph(LOCK_MINIMAL); const prGraph = parseInternalDepGraph(LOCK_MINIMAL); const baseV = checkContainment(baseGraph.graph, baseGraph.internalCrates, pathMap, POLICY); const prV = checkContainment(prGraph.graph, prGraph.internalCrates, pathMap, POLICY); const newV = diffContainmentViolations(baseV, prV); assert.equal(newV.length, 0); }); it("detects added external dep + new containment violation together", () => { const prLock = LOCK_MINIMAL + ` [[package]] name = "new_ext" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "xyz" [[package]] name = "lib_a" version = "0.0.0" dependencies = [ "lib_b", ] [[package]] name = "lib_b" version = "0.0.0" `; // External diff const baseDeps = parseExternalDeps(LOCK_MINIMAL); const prDeps = parseExternalDeps(prLock); const diff = diffDeps(baseDeps, prDeps); assert.equal(diff.added.length, 1); assert.equal(diff.added[0].name, "new_ext"); // Containment const pathMap = parseCratePathMap(MANIFEST_MINIMAL); const baseGraph = parseInternalDepGraph(LOCK_MINIMAL); const prGraph = parseInternalDepGraph(prLock); const baseV = checkContainment(baseGraph.graph, baseGraph.internalCrates, pathMap, POLICY); const prV = checkContainment(prGraph.graph, prGraph.internalCrates, pathMap, POLICY); const newV = diffContainmentViolations(baseV, prV); assert.equal(newV.length, 1); assert.equal(newV[0].crate, "lib_a"); assert.equal(newV[0].dep, "lib_b"); }); }); describe("run", () => { it("uses merge-base sha for baseline file fetches", async () => { const lock = ` [[package]] name = "serde" version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" `; const manifest = ` [workspace] members = [] [workspace.dependencies] `; const calls = { getContent: [], compare: [], requestReviewers: 0, removeRequestedReviewers: 0, }; const github = { request: async (route, params) => { calls.compare.push({ route, params }); return { data: { merge_base_commit: { sha: "mergebase123" } } }; }, rest: { pulls: { listFiles: async () => ({ data: [{ filename: "Cargo.lock" }] }), requestReviewers: async () => { calls.requestReviewers += 1; return {}; }, removeRequestedReviewers: async () => { calls.removeRequestedReviewers += 1; return {}; }, }, repos: { getContent: async ({ path, ref }) => { calls.getContent.push({ path, ref }); const content = path === "Cargo.lock" ? lock : manifest; return { data: { type: "file", content: Buffer.from(content, "utf8").toString("base64"), }, }; }, }, }, }; const context = { repo: { owner: "microsoft", repo: "openvmm" }, payload: { pull_request: { number: 42, base: { sha: "basetip999" }, head: { sha: "headabc999" }, }, }, }; const core = { warning: () => {}, setFailed: () => {}, }; await run(github, context, core); assert.equal(calls.compare.length, 1); assert.equal( calls.compare[0].params.basehead, "basetip999...headabc999" ); const lockFetches = calls.getContent.filter((c) => c.path === "Cargo.lock"); assert.equal(lockFetches.length, 2); assert.ok(lockFetches.some((c) => c.ref === "mergebase123")); assert.ok(lockFetches.some((c) => c.ref === "refs/pull/42/head")); }); });