import { describe, expect, mock, test } from "bun:test";
// Fake app records keyed by ID
const compiledApp = {
id: "app-1",
name: "Compiled App",
htmlDefinition: "",
schemaJson: "{}",
createdAt: 0,
updatedAt: 0,
formatVersion: 2,
};
const legacyApp = {
id: "legacy-1",
name: "Legacy App",
htmlDefinition: "
Hello
",
schemaJson: "{}",
createdAt: 0,
updatedAt: 0,
};
const apps = new Map([
["app-1", compiledApp],
["legacy-1", legacyApp],
]);
const UNSUPPORTED_HTML = `This app uses the retired single-file format and can no longer be opened.
`;
mock.module("../apps/app-store.js", () => ({
resolveAppSource: (id: string) => {
const app = apps.get(id);
if (!app) {
return null;
}
return {
id,
name: app.name,
dirName: id,
sourceDir: `/fake/apps/${id}`,
origin: { kind: "workspace" as const },
};
},
readAppFileBytesFromDir: () => Buffer.from(""),
// Only the legacy app's directory has a root index.html and no src/.
isLegacySingleFileDir: (sourceDir: string) =>
sourceDir === "/fake/apps/legacy-1",
UNSUPPORTED_LEGACY_APP_HTML: UNSUPPORTED_HTML,
}));
// Mock shared-app-links-store (imported by app-routes but unused here)
mock.module("../apps/shared-app-links-store.js", () => ({
createSharedAppLink: () => ({ shareToken: "tok" }),
getSharedAppLink: () => null,
incrementDownloadCount: () => {},
deleteSharedAppLinkByToken: () => false,
}));
// Stub fs so the serve path finds a fake dist/index.html for the compiled app
mock.module("node:fs", () => ({
existsSync: (p: string) => p === "/fake/apps/app-1/dist/index.html",
readFileSync: (p: string, _enc?: string) => {
if (p === "/fake/apps/app-1/dist/index.html") {
return '';
}
return "";
},
}));
import { ROUTES } from "../runtime/routes/app-routes.js";
import { BadRequestError, NotFoundError } from "../runtime/routes/errors.js";
import type { ResponseHeaderArgs } from "../runtime/routes/types.js";
/** Find a route by operationId. */
function getRoute(operationId: string) {
const route = ROUTES.find((r) => r.operationId === operationId);
if (!route) {
throw new Error(`Route not found: ${operationId}`);
}
return route;
}
/** Resolve responseHeaders from a route definition for the given args. */
function getResponseHeaders(
operationId: string,
args: ResponseHeaderArgs,
): Record {
const route = getRoute(operationId);
if (!route.responseHeaders) {
return {};
}
if (typeof route.responseHeaders === "function") {
return route.responseHeaders(args);
}
return route.responseHeaders;
}
/** Parse CSP header into a directive map. */
function parseCsp(header: string): Record {
const directives: Record = {};
for (const part of header.split(";")) {
const trimmed = part.trim();
const spaceIdx = trimmed.indexOf(" ");
if (spaceIdx === -1) {
continue;
}
directives[trimmed.slice(0, spaceIdx)] = trimmed.slice(spaceIdx + 1);
}
return directives;
}
describe("app-routes CSP headers", () => {
test("does NOT include 'unsafe-inline' in script-src", () => {
const headers = getResponseHeaders("pages_serve", {
pathParams: { appId: "app-1" },
});
const directives = parseCsp(headers["Content-Security-Policy"]);
expect(directives["script-src"]).not.toContain("'unsafe-inline'");
});
test("includes 'self' in script-src for external main.js", () => {
const headers = getResponseHeaders("pages_serve", {
pathParams: { appId: "app-1" },
});
const directives = parseCsp(headers["Content-Security-Policy"]);
expect(directives["script-src"]).toContain("'self'");
});
test("includes 'unsafe-inline' in style-src", () => {
const headers = getResponseHeaders("pages_serve", {
pathParams: { appId: "app-1" },
});
const directives = parseCsp(headers["Content-Security-Policy"]);
expect(directives["style-src"]).toContain("'unsafe-inline'");
});
test("has img-src with self, data, and https", () => {
const headers = getResponseHeaders("pages_serve", {
pathParams: { appId: "app-1" },
});
const directives = parseCsp(headers["Content-Security-Policy"]);
expect(directives["img-src"]).toContain("'self'");
expect(directives["img-src"]).toContain("data:");
expect(directives["img-src"]).toContain("https:");
});
});
describe("handleServePage", () => {
const pageHandler = getRoute("pages_serve").handler;
test("serves compiled dist/index.html for compiled apps", () => {
const html = pageHandler({ pathParams: { appId: "app-1" } }) as string;
expect(html).toContain("/v1/apps/app-1/dist/main.js");
});
test("serves the unsupported-format message for legacy single-file apps", () => {
const html = pageHandler({ pathParams: { appId: "legacy-1" } }) as string;
expect(html).toContain(UNSUPPORTED_HTML);
expect(html).not.toContain("Hello
");
});
test("throws NotFoundError for unknown apps", () => {
expect(() => pageHandler({ pathParams: { appId: "nope" } })).toThrow(
NotFoundError,
);
});
});
describe("handleServeDistFile appId validation", () => {
const distHandler = getRoute("apps_dist_file").handler;
test("rejects appId with encoded path traversal (..)", () => {
expect(() =>
distHandler({ pathParams: { appId: "..", filename: "main.js" } }),
).toThrow(BadRequestError);
});
test("rejects appId with forward slash", () => {
expect(() =>
distHandler({
pathParams: { appId: "../../etc", filename: "main.js" },
}),
).toThrow(BadRequestError);
});
test("rejects appId with backslash", () => {
expect(() =>
distHandler({
pathParams: { appId: "foo\\bar", filename: "main.js" },
}),
).toThrow(BadRequestError);
});
test("rejects empty appId", () => {
expect(() =>
distHandler({ pathParams: { appId: "", filename: "main.js" } }),
).toThrow(BadRequestError);
});
test("rejects appId with leading whitespace", () => {
expect(() =>
distHandler({
pathParams: { appId: " app-1", filename: "main.js" },
}),
).toThrow(BadRequestError);
});
test("rejects appId with trailing whitespace", () => {
expect(() =>
distHandler({
pathParams: { appId: "app-1 ", filename: "main.js" },
}),
).toThrow(BadRequestError);
});
test("rejects appId containing .. in the middle", () => {
expect(() =>
distHandler({
pathParams: { appId: "foo..bar", filename: "main.js" },
}),
).toThrow(BadRequestError);
});
test("allows valid appId and filename (file not found throws NotFoundError)", () => {
expect(() =>
distHandler({
pathParams: { appId: "app-1", filename: "main.js" },
}),
).toThrow(NotFoundError);
});
});