91 lines
5.8 KiB
TypeScript
91 lines
5.8 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const serverSource = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "server.ts"), "utf8");
|
|
const rootPackage = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
|
|
const webPackage = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "apps", "web", "package.json"), "utf8"));
|
|
|
|
describe("server runtime routing integration", () => {
|
|
test("delegates host routing decisions before API or React Router dispatch", () => {
|
|
expect(serverSource).toContain("getHostRoutingRuntimeDecision(request)");
|
|
expect(serverSource).toContain('routingDecision.type === "redirect"');
|
|
expect(serverSource).toContain('routingDecision.target === "api"');
|
|
});
|
|
|
|
test("uses required web config for fail-fast production startup and listen host/port", () => {
|
|
expect(serverSource).toContain('import { loadRequiredWebConfig } from "@toolctrl/config"');
|
|
expect(serverSource).toContain("const webConfig = loadRequiredWebConfig()");
|
|
expect(serverSource).toContain("const PORT = webConfig.listen.port");
|
|
expect(serverSource).toContain("const HOST = webConfig.listen.host");
|
|
expect(serverSource).not.toContain("loadWebConfig().config");
|
|
expect(serverSource).not.toContain("parsePort(process.env.PORT)");
|
|
expect(serverSource).not.toContain("process.env.HOST");
|
|
});
|
|
|
|
test("delegates root start to the web runtime owner", () => {
|
|
expect(rootPackage.scripts.start).toBe("pnpm --filter @toolctrl/web start");
|
|
expect(webPackage.scripts.start).toContain("node ../../build/server-runtime/server.js");
|
|
expect(webPackage.scripts.start).not.toContain("tsx");
|
|
expect(rootPackage.scripts.start).not.toContain("tsx");
|
|
});
|
|
|
|
test("resolves web build artifacts independently from caller cwd", () => {
|
|
expect(serverSource).toContain("const RUNTIME_DIRECTORY = path.dirname(fileURLToPath(import.meta.url))");
|
|
expect(serverSource).toContain('RUNTIME_DIRECTORY.endsWith(path.join("build", "server-runtime"))');
|
|
expect(serverSource).toContain('path.join(REPO_ROOT_DIRECTORY, "apps", "web", "build")');
|
|
expect(serverSource).toContain('const BUILD_PATH = path.join(WEB_BUILD_DIRECTORY, "server", "index.js")');
|
|
expect(serverSource).toContain('const CLIENT_BUILD_DIRECTORY = path.join(WEB_BUILD_DIRECTORY, "client")');
|
|
expect(serverSource).not.toContain('path.resolve("build/server/index.js")');
|
|
expect(serverSource).not.toContain('path.resolve("build/client")');
|
|
});
|
|
|
|
test("defines idempotent web security headers", () => {
|
|
expect(serverSource).toContain("function appendWebSecurityHeaders");
|
|
expect(serverSource).toContain("\"Content-Security-Policy\": \"base-uri 'self'; object-src 'none'; frame-ancestors 'self'\"");
|
|
expect(serverSource).toContain('"Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"');
|
|
expect(serverSource).toContain('"X-Content-Type-Options": "nosniff"');
|
|
expect(serverSource).toContain('"Referrer-Policy": "strict-origin-when-cross-origin"');
|
|
expect(serverSource).toContain('"X-Frame-Options": "SAMEORIGIN"');
|
|
expect(serverSource).toContain('"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()"');
|
|
expect(serverSource).toContain('"Cross-Origin-Opener-Policy": "same-origin"');
|
|
expect(serverSource).toContain('"Cross-Origin-Resource-Policy": "same-origin"');
|
|
expect(serverSource).toContain('"Cross-Origin-Embedder-Policy": "unsafe-none"');
|
|
expect(serverSource).toContain('key === "Content-Security-Policy" && hasHeader(headers, key)');
|
|
expect(serverSource).toContain("headers.set(key, value)");
|
|
expect(serverSource).toContain("headers.setHeader(key, value)");
|
|
});
|
|
|
|
test("applies web security headers to forwarded responses", () => {
|
|
expect(serverSource).toMatch(/for \(const \[key, value\] of response\.headers\)[\s\S]*res\.appendHeader\(key, value\);[\s\S]*appendWebSecurityHeaders\(res\);/);
|
|
});
|
|
|
|
test("applies web security headers to static assets without changing cache control", () => {
|
|
expect(serverSource).toContain('headers["Cache-Control"] = "public, max-age=31536000, immutable"');
|
|
expect(serverSource).toMatch(/function getStaticHeaders[\s\S]*return appendWebSecurityHeaders\(headers\);/);
|
|
});
|
|
|
|
test("sets a 1-day cache for /fonts and /images static assets", () => {
|
|
// Hash-named bundles get `immutable` because their URL contains the hash,
|
|
// but unhashed brand assets need a shorter TTL or stale-content lingers.
|
|
// See docs/improvement-plan/03-scaling-infrastructure/asset-cdn.md.
|
|
expect(serverSource).toContain('headers["Cache-Control"] = "public, max-age=86400"');
|
|
expect(serverSource).toMatch(/pathname === "\/fonts"[\s\S]*pathname\.startsWith\("\/fonts\/"\)/);
|
|
expect(serverSource).toMatch(/pathname === "\/images"[\s\S]*pathname\.startsWith\("\/images\/"\)/);
|
|
});
|
|
|
|
test("wires server diagnostics through the structured logger", () => {
|
|
expect(serverSource).toContain('import { createChildLogger, defaultLogger } from "@toolctrl/logger"');
|
|
expect(serverSource).toContain('const serverLogger = createChildLogger(defaultLogger, { component: "server" })');
|
|
expect(serverSource).toContain('serverLogger.error({ error }, "server request failed")');
|
|
expect(serverSource).toContain('serverLogger.info({ host: address, port: PORT }, "server listening")');
|
|
expect(serverSource).toContain('serverLogger.error({ error }, "server shutdown failed")');
|
|
expect(serverSource).toContain('serverLogger.error({ error }, "static asset stream failed")');
|
|
});
|
|
|
|
test("does not leave active console diagnostics in the server runtime", () => {
|
|
expect(serverSource).not.toMatch(/console\.(?:log|error|warn|info)\s*\(/);
|
|
});
|
|
});
|