import { afterAll, beforeAll, describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import * as path from "@std/path";
import { cors } from "@hono/hono/cors";
import env from "@/env.ts";
import createApp, { createRouter } from "@/lib/create-app.ts";
import router from "@/routes/shell/shell.index.ts";
import {
createShellStaticRouter,
StaticResponse,
} from "@/routes/shell/shell-static.ts";
if (env.ENV !== "test") {
throw new Error("ENV must be 'test'");
}
const app = createApp().route("/", router);
const INDEX_HTML = "
shellindex";
const APP_JS = "globalThis.__shell = true;\n";
const APP_CSS = "body { color: rebeccapurple; }\n";
const SENTINEL = "TOP_SECRET_OUTSIDE_ROOT";
let tempDir: string;
let sentinelPath: string;
// A static router mounted directly, used for the serving-behavior assertions.
let staticApp: ReturnType;
// The static router behind the same CORS middleware the shell wires up, mounted
// on a fully composed app, used to assert middleware applies to a 200 document.
let composedApp: ReturnType;
// Global hooks must be registered before any global describe() below, so the
// fixture setup for the static-router suites lives here at the top of the file.
beforeAll(async () => {
tempDir = await Deno.makeTempDir();
await Deno.writeTextFile(path.join(tempDir, "index.html"), INDEX_HTML);
await Deno.writeTextFile(path.join(tempDir, "app.js"), APP_JS);
await Deno.writeTextFile(path.join(tempDir, "app.css"), APP_CSS);
// Sentinel lives outside the static root, in the temp dir's parent, so a
// traversal request that escaped the root would expose it.
sentinelPath = path.join(path.dirname(tempDir), "shell-sentinel.txt");
await Deno.writeTextFile(sentinelPath, SENTINEL);
staticApp = createApp().route("/", createShellStaticRouter(tempDir));
const corsRouter = createRouter();
corsRouter.use(
"/*",
cors({ origin: "*", allowMethods: ["GET", "OPTIONS"] }),
);
corsRouter.route("/", createShellStaticRouter(tempDir));
composedApp = createApp().route("/", corsRouter);
});
afterAll(async () => {
await Deno.remove(tempDir, { recursive: true });
await Deno.remove(sentinelPath);
});
// The shell document must stay NON-cross-origin-isolated so that untrusted
// patterns are never handed SharedArrayBuffer / Atomics or a high-resolution
// clock. A page is cross-origin isolated only when it is served with BOTH
// `Cross-Origin-Opener-Policy: same-origin` AND a require-corp/credentialless
// `Cross-Origin-Embedder-Policy`. These tests fail loudly if a future change
// flips the served document to that isolating combination.
//
// See docs/specs/sandboxing/cross-origin-isolation.md.
describe("Shell cross-origin isolation posture", () => {
it("does not serve the isolating COOP+COEP header combination", async () => {
const response = await app.request("/");
// Drain the body so the response does not leak into the test runner.
await response.text();
const coop = response.headers.get("Cross-Origin-Opener-Policy");
const coep = response.headers.get("Cross-Origin-Embedder-Policy");
const isolatingCoop = coop === "same-origin";
const isolatingCoep = coep === "require-corp" || coep === "credentialless";
// Isolation requires BOTH headers; assert we never emit both together.
expect(isolatingCoop && isolatingCoep).toBe(false);
});
it("pins COOP to a non-isolating value", async () => {
const response = await app.request("/");
await response.text();
const coop = response.headers.get("Cross-Origin-Opener-Policy");
expect(coop).not.toBe("same-origin");
expect(coop).toBe("same-origin-allow-popups");
});
it("pins COEP to a non-isolating value", async () => {
const response = await app.request("/");
await response.text();
const coep = response.headers.get("Cross-Origin-Embedder-Policy");
expect(coep).not.toBe("require-corp");
expect(coep).not.toBe("credentialless");
expect(coep).toBe("unsafe-none");
});
it("applies the non-isolating headers to nested paths too", async () => {
// The posture must hold for every served path, not just the document root,
// because any same-origin response can establish or reuse the page's agent
// cluster.
const response = await app.request("/assets/app.js");
await response.text();
expect(response.headers.get("Cross-Origin-Opener-Policy")).toBe(
"same-origin-allow-popups",
);
expect(response.headers.get("Cross-Origin-Embedder-Policy")).toBe(
"unsafe-none",
);
});
});
// The shell routes serve read-only content to any origin. These pin that
// permissive CORS keeps working alongside the isolation headers, so a future
// change to one does not silently disturb the other.
describe("Shell route CORS", () => {
it("allows a cross-origin GET with a wildcard origin", async () => {
const response = await app.request("/", {
headers: { Origin: "https://example.com" },
});
await response.text();
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
});
it("answers an OPTIONS preflight", async () => {
const response = await app.request("/", {
method: "OPTIONS",
headers: {
Origin: "https://example.com",
"Access-Control-Request-Method": "GET",
},
});
await response.text();
expect(response.status).toBe(204);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
});
});
// With no compiled frontend and no SHELL_URL proxy target — the unit-test
// environment — the shell router answers with a 404 that tells an operator how
// to bring the shell up. This guards that operator hint and its port.
describe("Shell dev fallback without a compiled build or proxy", () => {
it("returns 404 with a hint naming SHELL_URL and the shell port", async () => {
const response = await app.request("/anything");
const body = await response.text();
expect(response.status).toBe(404);
expect(/Shell app not available/.test(body)).toBe(true);
expect(/SHELL_URL=http:\/\/localhost:\d+/.test(body)).toBe(true);
});
});
describe("createShellStaticRouter", () => {
it("serves index.html at the root with status 200, text/html, and an ETag", async () => {
const response = await staticApp.request("/");
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/html");
expect(response.headers.get("ETag")).toBeTruthy();
expect(await response.text()).toBe(INDEX_HTML);
});
it("serves an asset with a JS MIME type and its own ETag", async () => {
const response = await staticApp.request("/app.js");
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/javascript");
expect(response.headers.get("ETag")).toBeTruthy();
expect(await response.text()).toBe(APP_JS);
// The asset's ETag differs from index.html's (different content).
const indexResponse = await staticApp.request("/");
expect(response.headers.get("ETag")).not.toBe(
indexResponse.headers.get("ETag"),
);
});
it("serves a CSS asset with the text/css MIME type", async () => {
const response = await staticApp.request("/app.css");
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css");
expect(await response.text()).toBe(APP_CSS);
});
it("returns 304 with empty body and the same ETag for If-None-Match", async () => {
const first = await staticApp.request("/app.js");
const etag = first.headers.get("ETag");
expect(etag).toBeTruthy();
const second = await staticApp.request("/app.js", {
headers: { "If-None-Match": etag! },
});
expect(second.status).toBe(304);
expect(await second.text()).toBe("");
expect(second.headers.get("ETag")).toBe(etag);
});
it("falls back to index.html for a path with no matching file", async () => {
const response = await staticApp.request("/notes/42");
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/html");
expect(await response.text()).toBe(INDEX_HTML);
});
it("does not serve files outside the static root via traversal", async () => {
// The request resolves outside the static root; the traversal guard (and
// URL normalization) keep it from reaching the sentinel, so the client-side
// routing fallback serves index.html instead.
const response = await staticApp.request("/../shell-sentinel.txt");
expect(response.status).toBe(200);
const body = await response.text();
expect(body).toBe(INDEX_HTML);
expect(body).not.toContain(SENTINEL);
});
it("returns a stable ETag across repeated requests for the same file", async () => {
const first = await staticApp.request("/app.js");
const second = await staticApp.request("/app.js");
expect(first.headers.get("ETag")).toBe(second.headers.get("ETag"));
});
});
describe("createShellStaticRouter behind composed app middleware", () => {
it("applies the cross-origin middleware to a served 200 document", async () => {
// Exercises middleware ordering on a real 200 document rather than only on
// the dev 404 fallback: the served index.html must still carry the
// cross-origin header the shell wires up ahead of the static router.
const response = await composedApp.request("/");
expect(response.status).toBe(200);
expect(await response.text()).toBe(INDEX_HTML);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
});
});
describe("StaticResponse", () => {
const encoder = new TextEncoder();
// In-memory file set so StaticResponse can be exercised without touching disk.
const files: Record = {
"/root/index.html": encoder.encode(INDEX_HTML),
"/root/app.js": encoder.encode(APP_JS),
};
const deps = {
readFile: (filePath: string) => {
const content = files[filePath];
if (!content) return Promise.reject(new Deno.errors.NotFound(filePath));
return Promise.resolve(content);
},
generateETag: (content: Uint8Array) =>
Promise.resolve(`"len-${content.byteLength}"`),
};
it("derives MIME type and ETag from the file via injected deps", async () => {
const res = await StaticResponse.fromFile("/root/app.js", deps);
expect(res.mimeType).toBe("text/javascript");
expect(res.etag).toBe(`"len-${files["/root/app.js"].byteLength}"`);
const response = res.response();
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/javascript");
expect(response.headers.get("ETag")).toBe(res.etag);
expect(await response.text()).toBe(APP_JS);
});
it("returns 304 with no body when the ETag matches If-None-Match", async () => {
const res = await StaticResponse.fromFile("/root/index.html", deps);
const response = res.response(res.etag);
expect(response.status).toBe(304);
expect(response.headers.get("ETag")).toBe(res.etag);
expect(await response.text()).toBe("");
});
it("returns 200 when the If-None-Match ETag does not match", async () => {
const res = await StaticResponse.fromFile("/root/index.html", deps);
const response = res.response('"some-other-etag"');
expect(response.status).toBe(200);
expect(await response.text()).toBe(INDEX_HTML);
});
});