Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions src/routing/api/module-loader/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @veryfront-test runtime-guarded-deno
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals, assertMatch, assertRejects } from "#veryfront/testing/assert.ts";
import { afterAll, describe, it } from "#veryfront/testing/bdd.ts";
Expand Down Expand Up @@ -30,9 +31,11 @@ import { __resetPoolForTests } from "#veryfront/security/sandbox/worker-pool.ts"
import { runWithExactSourceIntegrationPolicy } from "#veryfront/integrations/source-policy-context.ts";
import { normalizeSourceIntegrationPolicy } from "#veryfront/integrations/source-policy.ts";
import type { APIRoute, AppRouteContext, AppRouteHandler } from "./types.ts";
import { isDeno } from "#veryfront/platform/compat/runtime.ts";

const fs = createFileSystem();
const appRouteContext: AppRouteContext = { params: {}, env: {} };
const denoIt = isDeno ? it : it.skip;

async function getText(route: APIRoute | null): Promise<string | undefined> {
const handler = route?.GET as AppRouteHandler | undefined;
Expand Down Expand Up @@ -156,7 +159,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
);
});

it("picks up an edited route module instead of serving the cached one", async () => {
denoIt("picks up an edited route module instead of serving the cached one", async () => {
// The counterpart to reuse: editing a route must still take effect without
// restarting the dev server.
const tmpDir = await makeTempDir();
Expand Down Expand Up @@ -188,7 +191,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
);
});

it("picks up same-size edits when the route mtime does not change", async () => {
denoIt("picks up same-size edits when the route mtime does not change", async () => {
const tmpDir = await makeTempDir();
const modulePath = join(tmpDir, "same-mtime-handler.ts");
const observableTime = new Date(1_700_000_000_000);
Expand Down Expand Up @@ -404,7 +407,9 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
}
});

it("executes prepared bundled source only inside the project worker", async () => {
// Execution crosses the Deno Worker boundary. The portable preparation and
// source-hashing path is covered by the preceding test on every runtime.
denoIt("executes prepared bundled source only inside the project worker", async () => {
const tmpDir = await makeTempDir();
const modulePath = join(tmpDir, "isolated-handler.ts");
const marker = "__vf_prepared_route_worker_marker__";
Expand Down Expand Up @@ -527,7 +532,9 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
assertEquals(typeof route?.GET, "function");
});

it("resolves npm dependencies declared by adapter-backed virtual projects", async () => {
// Virtual projects rely on Deno's npm: resolution without a physical
// node_modules tree. Node project resolution is covered by the local cases.
denoIt("resolves npm dependencies declared by adapter-backed virtual projects", async () => {
const realDir = await makeTempDir();
await fs.mkdir(join(realDir, "lib"), { recursive: true });
await fs.mkdir(join(realDir, "pages", "api"), { recursive: true });
Expand Down Expand Up @@ -677,7 +684,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
// Bundling reads the route through the adapter; a direct import does not. A
// module that threw while evaluating must surface its own error rather than
// be evaluated a second time under bundling semantics.
it("does not retry a module whose own error quotes a resolver phrase", async () => {
denoIt("does not retry a module whose own error quotes a resolver phrase", async () => {
const tmpDir = await makeTempDir();
const modulePath = join(tmpDir, "handler.ts");

Expand Down Expand Up @@ -1459,7 +1466,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
assertMatch(prepared.source, /project-dependency/);
});

it("rejects project symlink escapes before the adapter reads the target", async () => {
denoIt("rejects project symlink escapes before the adapter reads the target", async () => {
const projectDir = await makeTempDir();
const outsideDir = await makeTempDir();
const projectLibDir = join(projectDir, "lib");
Expand Down Expand Up @@ -1509,7 +1516,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
assertEquals(linkedModuleRead, false);
});

it("rejects a project package manifest symlink before reading outside the project", async () => {
denoIt("rejects an out-of-project package manifest symlink", async () => {
const projectDir = await makeTempDir();
const outsideDir = await makeTempDir();
const outsideManifest = join(outsideDir, "package.json");
Expand Down Expand Up @@ -1556,7 +1563,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
assertEquals(outsideManifestRead, false);
});

it("rejects a symlinked dependency manifest outside the project", async () => {
denoIt("rejects a symlinked dependency manifest outside the project", async () => {
const projectDir = await makeTempDir();
const outsideDir = await makeTempDir();
const packageName = "outside-dependency";
Expand Down Expand Up @@ -1616,7 +1623,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
assertEquals(outsideManifestRead, false);
});

it("reads the authorized canonical path when a project symlink is swapped", async () => {
denoIt("reads the authorized canonical path when a project symlink is swapped", async () => {
const projectDir = await makeTempDir();
const outsideDir = await makeTempDir();
const projectLibDir = join(projectDir, "lib");
Expand Down Expand Up @@ -1708,7 +1715,7 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
);

await withMockFetch(serveModule, async () => {
await assertRejects(
const error = await assertRejects(
async () => {
await loadHandlerModule({
projectDir: virtualBase,
Expand All @@ -1718,7 +1725,10 @@ describe("loadHandlerModule", { sanitizeResources: false, sanitizeOps: false },
});
},
Error,
"No such file or directory",
);
assertMatch(
String((error as Error).message),
/No such file or directory|ENOENT/i,
);
});
});
Expand Down Expand Up @@ -1815,7 +1825,7 @@ describe("generateCompiledBinaryRequireShim - symlink resistance (VULN-FS-5)", {
sanitizeResources: false,
sanitizeOps: false,
}, () => {
it("re-canonicalisation via realPathSync catches a node_modules symlink escape", async () => {
denoIt("re-canonicalisation via realPathSync catches a node_modules symlink escape", async () => {
// Create a project root, a decoy "evil" package whose entry file is a
// symlink pointing at a file outside the project root. If the shim only
// checked the pre-symlink path, the containment test would pass but the
Expand Down Expand Up @@ -1871,7 +1881,7 @@ describe("generateCompiledBinaryRequireShim - symlink resistance (VULN-FS-5)", {
} catch (_) { /* best effort */ }
});

it("accepts legitimate deps when the project root itself is opened through a symlink", async () => {
denoIt("accepts dependencies through a symlinked project root", async () => {
// Regression for Codex review on #1120: if __vf_projectRoot is not
// canonicalised at shim init, a legitimate dep inside a symlinked project
// fails the post-realPathSync containment check (because realPathSync on
Expand Down
9 changes: 2 additions & 7 deletions tests/bun/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
import os from "node:os";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { filterTestFiles, listTestFiles } from "../test-file-utils.mjs";
import { filterTestFiles, isDenoDependentTestSource, listTestFiles } from "../test-file-utils.mjs";
import { ensureNpmNodeModulesLinks } from "../ensure-npm-links.mjs";
import { DENO_ONLY_TESTS } from "../deno-only-tests.mjs";
import { buildIsolatedBunTestRuns, registerBunWorkspaceCleanup } from "./runner-args.mjs";
Expand Down Expand Up @@ -77,12 +77,7 @@ const hasFilters = includePatterns.length > 0 || excludePatterns.length > 0;
function isDenoDependentTest(file) {
try {
const source = readFileSync(file, "utf-8");
return (
/\bDeno\./.test(source) ||
/\bDeno\.test\s*\(/.test(source) ||
/tests\/_helpers\/utils\.ts/.test(source) ||
/\bcreateMockServer\s*\(/.test(source)
);
return isDenoDependentTestSource(source);
} catch {
return false;
}
Expand Down
12 changes: 7 additions & 5 deletions tests/node/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { spawn } from "node:child_process";
import os from "node:os";
import { readFileSync } from "node:fs";
import { filterTestFiles, listTestFiles, splitIntoShards } from "../test-file-utils.mjs";
import {
filterTestFiles,
isDenoDependentTestSource,
listTestFiles,
splitIntoShards,
} from "../test-file-utils.mjs";
import { ensureNpmNodeModulesLinks } from "../ensure-npm-links.mjs";
import { DENO_ONLY_TESTS } from "../deno-only-tests.mjs";

Expand Down Expand Up @@ -68,10 +73,7 @@ const hasFilters = includePatterns.length > 0 || envExcludePatterns.length > 0;
function isDenoDependentTest(file) {
try {
const source = readFileSync(file, "utf-8");
return /\bDeno\./.test(source) ||
/\bDeno\.test\s*\(/.test(source) ||
/tests\/_helpers\/utils\.ts/.test(source) ||
/\bcreateMockServer\s*\(/.test(source);
return isDenoDependentTestSource(source);
} catch {
return false;
}
Expand Down
26 changes: 25 additions & 1 deletion tests/runtime-test-filters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
import { describe, it } from "#veryfront/testing/bdd.ts";
import { assert, assertEquals } from "#veryfront/testing/assert.ts";
import { DENO_ONLY_TESTS } from "./deno-only-tests.mjs";
import { filterTestFiles } from "./test-file-utils.mjs";
import {
filterTestFiles,
hasRuntimeGuardedDenoHeader,
isDenoDependentTestSource,
} from "./test-file-utils.mjs";

/** The files the shared list exists to exclude. */
const DENO_ONLY_FILES = [
Expand Down Expand Up @@ -66,6 +70,26 @@ describe("runtime test filters", () => {
);
});

it("keeps runtime-guarded API module-loader coverage eligible for Node", async () => {
const source = await Deno.readTextFile(
new URL(
"../src/routing/api/module-loader/loader.test.ts",
import.meta.url,
),
);

assertEquals(
hasRuntimeGuardedDenoHeader(source),
true,
"the broad loader suite must declare that its Deno-only cases are guarded",
);
assertEquals(
isDenoDependentTestSource(source),
false,
"the Node runner must not silently discard portable module-loader coverage",
);
});

it("matches files that actually exist", async () => {
// A renamed or moved file leaves a pattern matching nothing, and the
// exclusion silently stops working. Cheaper to catch here than in a failing
Expand Down
26 changes: 26 additions & 0 deletions tests/test-file-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ import { dirname, relative, resolve, sep } from "node:path";
const TEST_FILE_RE = /\.test\.[cm]?[jt]sx?$/i;
const GLOB_CHARS_RE = /[\*\?\[{]/;

/** First-line metadata for files whose Deno references are runtime-guarded. */
export const RUNTIME_GUARDED_DENO_HEADER = "// @veryfront-test runtime-guarded-deno";

/**
* Whether a source opens with the runtime-guarded header line. A Windows
* checkout with `core.autocrlf=true` ends that line in CRLF, and treating it as
* unguarded would silently drop the file from every non-Deno suite.
*/
export function hasRuntimeGuardedDenoHeader(source) {
const lineEnd = source.indexOf("\n");
if (lineEnd === -1) return false;
const firstLine = source.slice(0, lineEnd).replace(/\r$/, "");
return firstLine === RUNTIME_GUARDED_DENO_HEADER;
}

/** Whether a test source must be omitted from non-Deno runtime suites. */
export function isDenoDependentTestSource(source) {
if (hasRuntimeGuardedDenoHeader(source)) return false;
return (
/\bDeno\./.test(source) ||
/\bDeno\.test\s*\(/.test(source) ||
/tests\/_helpers\/utils\.ts/.test(source) ||
/\bcreateMockServer\s*\(/.test(source)
);
}

function toPosixPath(path) {
return path.split(sep).join("/");
}
Expand Down
38 changes: 38 additions & 0 deletions tests/test-file-utils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
import { tmpdir } from "node:os";
import { dirname, join, resolve, sep } from "node:path";
import { describe, it } from "node:test";
import {
hasRuntimeGuardedDenoHeader,
isDenoDependentTestSource,
RUNTIME_GUARDED_DENO_HEADER,
} from "./test-file-utils.mjs";

const utilsUrl = new URL("./test-file-utils.mjs", import.meta.url).href;

Expand Down Expand Up @@ -48,6 +53,39 @@ const GITIGNORED_TREE = [
/** The single path the stub `rg` prints; it exists in no fixture. */
const STUB_RIPGREP_MATCH = "fabricated-by-stub-ripgrep.test.ts";

describe("non-Deno runtime source filtering", () => {
it("preserves the existing Deno source heuristics", () => {
deepStrictEqual(isDenoDependentTestSource("Deno.test('x', () => {});"), true);
deepStrictEqual(isDenoDependentTestSource("await Deno.readTextFile('x');"), true);
deepStrictEqual(isDenoDependentTestSource("createMockServer();"), true);
deepStrictEqual(isDenoDependentTestSource("import './tests/_helpers/utils.ts';"), true);
deepStrictEqual(isDenoDependentTestSource("describe('portable', () => {});"), false);
});

it("accepts only an exact first-line runtime-guarded header", () => {
const guarded = `${RUNTIME_GUARDED_DENO_HEADER}\nawait Deno.readTextFile('x');`;
deepStrictEqual(isDenoDependentTestSource(guarded), false);
deepStrictEqual(isDenoDependentTestSource(`\n${guarded}`), true);
deepStrictEqual(
isDenoDependentTestSource(`// explanation\n${RUNTIME_GUARDED_DENO_HEADER}\nDeno.test('x')`),
true,
);
deepStrictEqual(isDenoDependentTestSource(RUNTIME_GUARDED_DENO_HEADER), false);
deepStrictEqual(
isDenoDependentTestSource(`${RUNTIME_GUARDED_DENO_HEADER} extra\nDeno.test('x')`),
true,
);
});

it("keeps the header guard working on a CRLF checkout", () => {
// A Windows clone with core.autocrlf=true rewrites the header line ending.
// Reading that as unguarded drops the whole file from the Node suite.
const crlf = `${RUNTIME_GUARDED_DENO_HEADER}\r\nawait Deno.readTextFile('x');\r\n`;
deepStrictEqual(hasRuntimeGuardedDenoHeader(crlf), true);
deepStrictEqual(isDenoDependentTestSource(crlf), false);
});
});

/**
* Build a throwaway tree, hand it to `run`, then remove it.
*
Expand Down