Files
Thesisly/tests/fixtures/session-app.mjs
T

134 lines
7.5 KiB
JavaScript

// Synthetic Core only. The application and its production proxy run unchanged
// in a disposable Next dev copy; no real accounts, .env files or OAuth services.
import { createServer } from "node:http";
import { createHash, randomBytes } from "node:crypto";
import { cp, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
import { createRequire } from "node:module";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
const root = fileURLToPath(new URL("../../", import.meta.url));
const origin = "http://localhost:17323";
const snapshot = JSON.parse(await readFile(join(root, "src/data/papers.json"), "utf8"));
const sessions = new Set(), grants = new Map();
let meFailure = false, logoutFailure = false, verified = false;
let counts = {};
let paperStatus = 0, paperMalformed = false;
function reset() {
sessions.clear(); grants.clear(); meFailure = false; logoutFailure = false; verified = false;
paperStatus = 0; paperMalformed = false;
counts = { exchange: 0, verified: 0, logout: 0, google: 0, slack: 0 };
}
reset();
const token = () => randomBytes(32).toString("hex");
function reply(res, status, data) {
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
res.end(status === 204 ? undefined : JSON.stringify(data));
}
function session(res) {
const value = token(); sessions.add(value);
reply(res, 200, { access_token: value, expires_in: 2592000 });
}
async function body(req) {
const parts = [];
for await (const part of req) parts.push(part);
return JSON.parse(Buffer.concat(parts).toString() || "{}");
}
const server = createServer(async (req, res) => {
try {
const url = new URL(req.url, "http://localhost:17324");
if (url.pathname === "/__test/reset" && req.method === "POST") { reset(); return reply(res, 200, {}); }
if (url.pathname === "/__test/state") return reply(res, 200, { ...counts, sessions: sessions.size });
if (url.pathname === "/__test/control" && req.method === "POST") {
const data = await body(req);
if (data.expire) sessions.clear();
if (typeof data.meFailure === "boolean") meFailure = data.meFailure;
if (typeof data.logoutFailure === "boolean") logoutFailure = data.logoutFailure;
if ([0, 400, 429, 503, 504].includes(data.paperStatus)) paperStatus = data.paperStatus;
if (typeof data.paperMalformed === "boolean") paperMalformed = data.paperMalformed;
return reply(res, 200, {});
}
if (url.pathname.startsWith("/projects/thesisly/research/papers/")) {
const paper = snapshot.papers.find(p => p.id === url.pathname.split("/").at(-1));
return reply(res, paper ? 200 : 404, paper || { error: "not_found" });
}
if (url.pathname === "/projects/thesisly/research/papers") {
if (paperStatus) return reply(res, paperStatus, { error: "synthetic research failure" });
if (paperMalformed) return reply(res, 200, { unexpected: true });
const page = Number(url.searchParams.get("page") || 1), pageSize = Number(url.searchParams.get("pageSize") || 8);
// Fixture transport data, not an implementation of Core search semantics.
return reply(res, 200, { items: snapshot.papers.slice((page - 1) * pageSize, page * pageSize), total: snapshot.papers.length, page, pageSize, topics: [...new Set(snapshot.papers.flatMap(p => p.topics))] });
}
const match = /^\/projects\/thesisly\/auth\/([a-z-]+)$/.exec(url.pathname);
if (!match) return reply(res, 404, { error: "not_found" });
const action = match[1];
if (["google", "slack"].includes(action) && req.method === "GET") {
counts[action]++;
const challenge = url.searchParams.get("challenge");
if (!/^[-_a-zA-Z0-9]{43}$/.test(challenge || "")) return reply(res, 400, { error: "invalid_request" });
const code = token(); grants.set(code, challenge);
res.writeHead(302, { Location: `${origin}/auth/callback#code=${code}` }); return res.end();
}
if (action === "exchange" && req.method === "POST") {
counts.exchange++;
const code = req.headers["x-foundry-code"], verifier = req.headers["x-foundry-verifier"];
const expected = grants.get(code);
if (!expected || typeof verifier !== "string" || createHash("sha256").update(verifier).digest("base64url") !== expected) return reply(res, 400, { error: "invalid_verification" });
grants.delete(code); return session(res);
}
if (action === "register" && req.method === "POST") return reply(res, 202, { status: "verification_required" });
if (action === "verify-email" && req.method === "POST") {
counts.verified++;
const data = await body(req);
if (verified || data.token !== "fixture-email-verification") return reply(res, 400, { error: "invalid_verification" });
verified = true; return session(res);
}
if (action === "login" && req.method === "POST") {
const data = await body(req);
if (data.email !== "reader@example.test" || data.password !== "fixture-password-123") return reply(res, 401, { error: "invalid_credentials" });
return session(res);
}
const bearer = (req.headers.authorization || "").replace(/^Bearer /, "");
if (!sessions.has(bearer)) return reply(res, 401, { error: "invalid_session" });
if (action === "me" && req.method === "GET") {
if (meFailure) return reply(res, 503, { error: "temporarily_unavailable" });
res.setHeader("X-Foundry-Session-Expires-In", "2592000");
return reply(res, 200, { id: "fixture-account", email: "reader@example.test", project: "thesisly" });
}
if (action === "logout" && req.method === "POST") {
if (logoutFailure) return reply(res, 503, { error: "temporarily_unavailable" });
sessions.delete(bearer); counts.logout++; return reply(res, 204);
}
return reply(res, 405, { error: "method_not_allowed" });
} catch { reply(res, 500, { error: "fixture_error" }); }
});
const directory = await mkdtemp(join(root, ".session-test-"));
let child;
let closing = false;
async function close(code = 0) {
if (closing) return;
closing = true;
if (child && child.exitCode === null) {
child.kill("SIGTERM");
await new Promise(resolve => { child.once("exit", resolve); setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5000).unref(); });
}
server.closeAllConnections();
await new Promise(resolve => server.close(resolve));
await rm(directory, { recursive: true, force: true });
process.exit(code);
}
process.on("SIGTERM", () => void close());
process.on("SIGINT", () => void close());
try {
for (const name of ["src", "package.json", "tsconfig.json", "postcss.config.mjs", "next.config.ts", "next-env.d.ts"]) await cp(join(root, name), join(directory, name), { recursive: true });
await symlink(join(root, "node_modules"), join(directory, "node_modules"), "dir");
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(17324, "localhost", resolve); });
const require = createRequire(import.meta.url);
child = spawn(process.execPath, [require.resolve("next/dist/bin/next"), "dev", "--hostname", "localhost", "--port", "17323"], {
cwd: directory, stdio: "inherit", env: { ...process.env, FOUNDRY_PROJECT: "thesisly", FOUNDRY_API_ORIGIN: "http://localhost:17324", APP_ORIGIN: origin, PAPER_API_ORIGIN: process.env.THESISLY_PAPER_MODE === "sample" ? "" : "http://localhost:17324/projects/thesisly/research/", NEXT_TELEMETRY_DISABLED: "1" },
});
child.once("exit", code => { if (!closing) void close(code || 1); });
} catch (error) { console.error(error.message); await close(1); }