diff --git a/packages/producer/scripts/no-network.mjs b/packages/producer/scripts/no-network.mjs new file mode 100644 index 0000000000..16abfc05f2 --- /dev/null +++ b/packages/producer/scripts/no-network.mjs @@ -0,0 +1,16 @@ +import dns from "node:dns"; + +// Unit tests run offline: fetch throws, and a lookup of any host but this machine fails, which also covers a test +// environment's own fetch (happy-dom's) and node:http. A test that needs a response stubs fetch itself. +const LOCAL = new Set(["localhost"]); +const refusal = (target) => new TypeError(`No network in unit tests: ${target}`); + +globalThis.fetch = async (input) => { + throw refusal(input?.url ?? input); +}; + +const { lookup } = dns; +const guarded = (host, ...rest) => + LOCAL.has(host) ? lookup(host, ...rest) : process.nextTick(rest.at(-1), refusal(host)); +// Keeps the original's own properties, among them what util.promisify reads to resolve { address, family }. +dns.lookup = Object.defineProperties(guarded, Object.getOwnPropertyDescriptors(lookup)); diff --git a/packages/producer/scripts/run-test-lane.mjs b/packages/producer/scripts/run-test-lane.mjs index 8c22ca09ec..cf954b6121 100644 --- a/packages/producer/scripts/run-test-lane.mjs +++ b/packages/producer/scripts/run-test-lane.mjs @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { discoverProducerTests, PRODUCER_ROOT } from "./test-classification.mjs"; const lane = process.argv[2]; @@ -14,10 +15,13 @@ const tests = discoverProducerTests().filter( (test) => test.lane === lane && (!requestedRunner || test.runner === requestedRunner), ); -function run(args) { +const noNetwork = new URL("./no-network.mjs", import.meta.url); +const unit = lane === "unit"; + +function run(args, env = {}) { const result = spawnSync("bun", args, { cwd: PRODUCER_ROOT, - env: { ...process.env, HYPERFRAMES_TEST_LANE: lane }, + env: { ...process.env, HYPERFRAMES_TEST_LANE: lane, ...env }, stdio: "inherit", }); if (result.error) throw result.error; @@ -25,10 +29,13 @@ function run(args) { } const vitestFiles = tests.filter((test) => test.runner === "vitest").map((test) => test.file); -if (vitestFiles.length > 0) run(["x", "vitest", "run", ...vitestFiles]); +const vitestEnv = unit + ? { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --import ${noNetwork.href}`.trim() } + : {}; +if (vitestFiles.length > 0) run(["x", "vitest", "run", ...vitestFiles], vitestEnv); // Bun's mock.module registry is process-global. Run each file in a fresh // process so mocks from one source test cannot mutate another test's imports. for (const test of tests.filter((entry) => entry.runner === "bun")) { - run(["test", test.file]); + run(["test", ...(unit ? ["--preload", fileURLToPath(noNetwork)] : []), test.file]); } diff --git a/packages/producer/src/noNetwork.test.ts b/packages/producer/src/noNetwork.test.ts new file mode 100644 index 0000000000..7188e79bcf --- /dev/null +++ b/packages/producer/src/noNetwork.test.ts @@ -0,0 +1,6 @@ +import { expect, it } from "bun:test"; + +// scripts/run-test-lane.mjs preloads scripts/no-network.mjs for the unit lane. +it("reaches no network in the unit lane", async () => { + await expect(fetch("https://example.com")).rejects.toThrow("No network in unit tests"); +}); diff --git a/packages/producer/src/noNetworkNode.test.ts b/packages/producer/src/noNetworkNode.test.ts new file mode 100644 index 0000000000..44b3718bb9 --- /dev/null +++ b/packages/producer/src/noNetworkNode.test.ts @@ -0,0 +1,15 @@ +// @vitest-environment happy-dom +import { get } from "node:http"; +import { expect, it } from "vitest"; + +// scripts/run-test-lane.mjs imports scripts/no-network.mjs into the unit lane's vitest workers. +it("refuses happy-dom's own fetch in the unit lane", async () => { + await expect(fetch("https://example.com")).rejects.toThrow("No network in unit tests"); +}); + +it("refuses a node:http request to another host in the unit lane", async () => { + const request = new Promise((resolve, reject) => { + get("http://example.com", resolve).on("error", reject); + }); + await expect(request).rejects.toThrow("No network in unit tests"); +}); diff --git a/packages/producer/src/services/htmlCompiler.test.ts b/packages/producer/src/services/htmlCompiler.test.ts index 36243531b4..4a2ab4c937 100644 --- a/packages/producer/src/services/htmlCompiler.test.ts +++ b/packages/producer/src/services/htmlCompiler.test.ts @@ -1686,7 +1686,7 @@ describe("crossorigin attribute stripping", () => { // // Tests run on localizeRemoteMediaSources directly (exported for testing) to // avoid invoking ffprobe / the full compileForRender pipeline. fetch is patched -// in-process for success cases; real 404s from example.com cover fallback. +// in-process for success cases; the unit lane's refused network covers fallback. describe("localizeRemoteMediaSources", () => { it("rewrites remote