Skip to content
Closed
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
69 changes: 68 additions & 1 deletion src/server/handlers/preview/markdown-preview.handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertNotEquals } from "#veryfront/testing/assert.ts";
import type { HandlerContext } from "../types.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { MarkdownPreviewHandler } from "./markdown-preview.handler.ts";
Expand Down Expand Up @@ -99,6 +99,73 @@ Deno.test("MarkdownPreviewHandler fails closed before shared source reads", asyn
assertEquals(reads, 0);
});

Deno.test("MarkdownPreviewHandler renders shared markdown once the host grants execution", async () => {
const originalFetch = globalThis.fetch;
let contentReads = 0;
globalThis.fetch = (input) => {
const url = String(input);
if (url.includes("/git/trees/")) {
return Promise.resolve(Response.json({
sha: "tree",
tree: [{ path: "README.md", mode: "100644", type: "blob", sha: "readme", size: 7 }],
truncated: false,
}));
}
if (url.includes("/contents/README.md")) {
contentReads += 1;
const content = "# Hello";
return Promise.resolve(Response.json({
type: "file",
name: "README.md",
path: "README.md",
sha: "readme",
size: content.length,
content: btoa(content),
encoding: "base64",
download_url: null,
}));
}
return Promise.resolve(new Response("Not found", { status: 404 }));
};

const github = new GitHubFSAdapter({
type: "github",
projectDir: "/project",
github: { token: "token", owner: "owner", repo: "repo" },
});
const fs = new FSAdapterWrapper(github);
try {
const result = await new MarkdownPreviewHandler().handle(
new Request("https://tenant.example/README.md"),
makeCtx({
isLocalProject: false,
allowHostProjectCodeExecution: true,
requestContext: { mode: "preview" } as HandlerContext["requestContext"],
prepareHostedConfigContext: (() =>
Promise.resolve(
undefined,
)) as unknown as HandlerContext["prepareHostedConfigContext"],
securityConfig: null,
adapter: { fs } as unknown as HandlerContext["adapter"],
}),
);

assertNotEquals(
result.response?.status,
503,
"a granted shared executor must not return project-execution-unavailable",
);
assertEquals(
contentReads,
1,
"the request must reach the project source read instead of failing at the guard",
);
} finally {
await fs.shutdown();
globalThis.fetch = originalFetch;
}
});
Comment on lines +102 to +167

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the required BDD test API.

Replace Deno.test() with describe() and it() from #veryfront/testing/bdd.ts.

  • src/server/handlers/preview/markdown-preview.handler.test.ts#L102-L167: wrap the markdown test in describe() and it().
  • src/server/handlers/request/snippet.handler.test.ts#L111-L160: wrap the snippet test in describe() and it().

As per coding guidelines, **/*.{test,spec}.ts must use describe() and it() from #veryfront/testing/bdd.ts.

📍 Affects 2 files
  • src/server/handlers/preview/markdown-preview.handler.test.ts#L102-L167 (this comment)
  • src/server/handlers/request/snippet.handler.test.ts#L111-L160
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/handlers/preview/markdown-preview.handler.test.ts` around lines
102 - 167, Replace Deno.test usage with describe and it imported from
`#veryfront/testing/bdd.ts`. In
src/server/handlers/preview/markdown-preview.handler.test.ts:102-167, wrap the
markdown test in describe and it; apply the same wrapping in
src/server/handlers/request/snippet.handler.test.ts:111-160 for the snippet
test.

Source: Coding guidelines


Deno.test("MarkdownPreviewHandler admits and reads through a real wrapped GitHub adapter", async () => {
const originalFetch = globalThis.fetch;
let contentReads = 0;
Expand Down
4 changes: 2 additions & 2 deletions src/server/handlers/preview/markdown-preview.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { extract } from "#std/front-matter/yaml.ts";
import { tryNotFoundFallback } from "../request/ssr/not-found-fallback.ts";
import { generateMarkdownHtml } from "./markdown-html-generator.ts";
import { validateLexicalPath, validatePath, ValidationPresets } from "#veryfront/security";
import { isSharedProjectRuntime } from "#veryfront/security/project-locality.ts";
import { requiresIsolatedProjectRuntime } from "#veryfront/security/project-locality.ts";
import {
createErrorResponseFromDefinition,
PROJECT_EXECUTION_UNAVAILABLE,
Expand Down Expand Up @@ -48,7 +48,7 @@ export class MarkdownPreviewHandler extends BaseHandler {
return this.continue();
}

if (isSharedProjectRuntime(ctx)) {
if (requiresIsolatedProjectRuntime(ctx)) {
const problem = createErrorResponseFromDefinition(
PROJECT_EXECUTION_UNAVAILABLE,
{
Expand Down
50 changes: 49 additions & 1 deletion src/server/handlers/request/module/module.handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertNotEquals } from "#veryfront/testing/assert.ts";
import { afterEach, describe, it } from "#veryfront/testing/bdd.ts";
import { ModuleHandler } from "./module.handler.ts";
import { handleBatchModuleEndpoint } from "./batch-module-handler.ts";
Expand Down Expand Up @@ -273,6 +273,54 @@ describe("server/handlers/request/module/module.handler", () => {
assertEquals(result.response?.status, 503);
assertEquals(await result.response?.text(), "");
});

it("reaches the host renderer once the host grants execution", async () => {
let rendererCalls = 0;
const renderer = {
renderPage: () =>
Promise.resolve({ pageModule: { code: "export default 1;" } }) as ReturnType<
Renderer["renderPage"]
>,
} as unknown as Renderer;
setRendererInitializer({
initialize: () => {
rendererCalls++;
return Promise.resolve(renderer);
},
isInitialized: () => rendererCalls > 0,
get: () => renderer,
destroy: () => Promise.resolve(),
});

const result = await new ModuleHandler().handle(
new Request("https://tenant.example/_veryfront/pages/page.js"),
makeCtx({
isLocalProject: false,
allowHostProjectCodeExecution: true,
projectSlug: "tenant",
proxyToken: "token",
adapter: {
fs: {
isMultiProjectMode: () => true,
runWithContext: <R>(_s: string, _t: string, fn: () => Promise<R>) => fn(),
exists: () => Promise.resolve(true),
readFile: () => Promise.resolve(""),
},
} as unknown as HandlerContext["adapter"],
}),
);

assertNotEquals(
result.response?.status,
503,
"a granted shared executor must not return project-execution-unavailable",
);
assertEquals(
rendererCalls > 0,
true,
"the request must reach the host renderer instead of failing at the guard",
);
});
});

describe("handle - page modules", () => {
Expand Down
10 changes: 5 additions & 5 deletions src/server/handlers/request/module/module.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
createErrorResponseFromDefinition,
PROJECT_EXECUTION_UNAVAILABLE,
} from "#veryfront/errors";
import { isSharedProjectRuntime } from "#veryfront/security/project-locality.ts";
import { requiresIsolatedProjectRuntime } from "#veryfront/security/project-locality.ts";

const MODULE_ENDPOINT_PREFIXES = [
"/_vf_modules/",
Expand Down Expand Up @@ -69,11 +69,11 @@ export class ModuleHandler extends BaseHandler {
}

// These endpoints delegate to the legacy renderer, whose module loader
// imports page and layout code in the host process. Remote source must not
// reach that path until rendering has a generation-owned prepared module
// graph equivalent to isolated API routes.
// imports page and layout code in the host process. A shared runtime must
// not reach that path unless its host-owned entrypoint granted the
// host-execution capability.
if (
isSharedProjectRuntime(ctx) &&
requiresIsolatedProjectRuntime(ctx) &&
HOST_RENDERER_ENDPOINT_PREFIXES.some((prefix) => pathname.startsWith(prefix))
) {
const problem = createErrorResponseFromDefinition(
Expand Down
55 changes: 53 additions & 2 deletions src/server/handlers/request/snippet.handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertNotEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { validateLexicalPath } from "#veryfront/security";
import { SnippetHandler } from "./snippet.handler.ts";
Expand Down Expand Up @@ -94,7 +94,7 @@ Deno.test("SnippetHandler rejects shared rendering before proxy context or sourc
projectDir: "/project",
projectSlug: "project",
proxyToken: "token",
isLocalProject: true,
isLocalProject: false,
adapter: { fs },
} as unknown as HandlerContext;

Expand All @@ -108,6 +108,57 @@ Deno.test("SnippetHandler rejects shared rendering before proxy context or sourc
assertEquals(readPath, undefined);
});

Deno.test("SnippetHandler renders shared snippets once the host grants execution", async () => {
let contextCalls = 0;
let readPath: string | undefined;
const fs = {
symlinkSemantics: "none" as const,
isMultiProjectMode: () => true,
isContextualMode: () => true,
runWithContext: async (
_slug: string,
_token: string,
fn: () => Promise<unknown>,
) => {
contextCalls++;
return await fn();
},
exists: () => Promise.resolve(true),
stat: () =>
Promise.resolve({
isFile: true,
isDirectory: false,
isSymlink: false,
size: 0,
mtime: new Date(),
}),
readFile: (path: string) => {
readPath = path;
return Promise.resolve("");
},
};
const ctx = {
projectDir: "/project",
projectSlug: "project",
proxyToken: "token",
isLocalProject: false,
allowHostProjectCodeExecution: true,
adapter: { fs },
} as unknown as HandlerContext;

const result = await new SnippetHandler().handle(
new Request("http://localhost/@components/button"),
ctx,
);
assertNotEquals(
result.response?.status,
503,
"a granted shared executor must not return project-execution-unavailable",
);
assertEquals(contextCalls, 1);
assertEquals(readPath, "/project/components/button.snippet.mdx");
});

Deno.test("SnippetHandler preserves dedicated local rendering", async () => {
let readPath: string | undefined;
const fs = {
Expand Down
4 changes: 2 additions & 2 deletions src/server/handlers/request/snippet.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
VeryfrontError,
} from "#veryfront/errors";
import { validatePath, ValidationPresets } from "#veryfront/security";
import { isSharedProjectRuntime } from "#veryfront/security/project-locality.ts";
import { requiresIsolatedProjectRuntime } from "#veryfront/security/project-locality.ts";
import {
createHandlerDependencyPinningSource,
getHandlerDependencyPinningIdentity,
Expand All @@ -37,7 +37,7 @@ export class SnippetHandler extends BaseHandler {
return this.continue();
}

if (isSharedProjectRuntime(ctx)) {
if (requiresIsolatedProjectRuntime(ctx)) {
const problem = createErrorResponseFromDefinition(
PROJECT_EXECUTION_UNAVAILABLE,
{
Expand Down
50 changes: 50 additions & 0 deletions src/server/handlers/response/cors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,61 @@ describe("server/handlers/response/cors", () => {
prepareHostedConfigContext: (() => {
throw new Error("shared preflight prepared project config");
}) as HandlerContext["prepareHostedConfigContext"],
securityConfig: { cors: { origin: ["https://app.example"] } } as never,
}),
);

assertEquals(result.response instanceof Response, true);
assertEquals(routeResolutionCalls, 0);
assertEquals(
result.response?.headers.get("access-control-allow-methods"),
"GET, POST, PUT, PATCH, DELETE, OPTIONS",
);
});

it("resolves project route methods once the host grants execution", async () => {
const dir = await Deno.makeTempDir({ prefix: "vf-cors-granted-" });
const routeFile = `${dir}/route.ts`;
await Deno.writeTextFile(
routeFile,
"export function GET() {}\nexport function POST() {}\n",
);

let routeResolutionCalls = 0;
const handler = new CorsHandler({
resolveAppRouteFile: () => {
routeResolutionCalls++;
return Promise.resolve({ file: routeFile } as never);
},
});

try {
const result = await handler.handle(
new Request("https://tenant.example/api/private", {
method: "OPTIONS",
headers: {
Origin: "https://app.example",
"access-control-request-method": "POST",
},
}),
makeCtx({
allowHostProjectCodeExecution: true,
prepareHostedConfigContext: (() =>
Promise.resolve(
undefined,
)) as unknown as HandlerContext["prepareHostedConfigContext"],
securityConfig: { cors: { origin: ["https://app.example"] } } as never,
}),
);

assertEquals(routeResolutionCalls, 1);
assertEquals(
result.response?.headers.get("access-control-allow-methods"),
"HEAD, GET, POST, OPTIONS",
);
} finally {
await Deno.remove(dir, { recursive: true });
}
});

it("does not advertise infrastructure-only request headers", async () => {
Expand Down
8 changes: 4 additions & 4 deletions src/server/handlers/response/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ResponseBuilder } from "#veryfront/security/index.ts";
import { getConfig } from "#veryfront/config";
import { PRIORITY_VERY_HIGH } from "#veryfront/utils/constants/index.ts";
import { resolveAppRouteFile } from "../request/api/app-router-resolver.ts";
import { isSharedProjectRuntime } from "#veryfront/security/project-locality.ts";
import { requiresIsolatedProjectRuntime } from "#veryfront/security/project-locality.ts";
import { isInfrastructureOnlyRequestHeader } from "#veryfront/security/http/application-request.ts";

type AppRouteResolver = typeof resolveAppRouteFile;
Expand Down Expand Up @@ -50,13 +50,13 @@ export class CorsHandler extends BaseHandler {
if (req.method.toUpperCase() !== "OPTIONS") return this.continue();

const pathname = new URL(req.url).pathname;
const isSharedRuntime = isSharedProjectRuntime(ctx);
const allowMethods = isSharedRuntime
const mustDenyProjectExecution = requiresIsolatedProjectRuntime(ctx);
const allowMethods = mustDenyProjectExecution
? CorsHandler.DEFAULT_METHODS
: await this.resolveAllowedMethods(pathname, ctx);

let corsConfig = ctx.securityConfig?.cors;
if (!isSharedRuntime) {
if (!mustDenyProjectExecution) {
try {
const cfg = await getConfig(ctx.projectDir, ctx.adapter);
corsConfig = cfg?.security?.cors ?? corsConfig;
Expand Down