test/routing.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
/**
* Route ORDER, asserted against the real server.
*
* This is the one property of `app/server/index.js` that no unit test can
* reach. Express matches a path-carrying `app.use` before any route registered
* later, so a literal segment under `/api/decks/` is swallowed by the per-slug
* ownership gate — it parses, every other test passes, and the endpoint is dead
* in production. `POST /api/decks/search` was dead for months exactly this way,
* and invisible to the operator: an admin passes the ownership check that
* shadows it, so the only account that could have noticed never could.
*
* So the server is booted on port 0 and asked over HTTP, as an ordinary
* non-admin account.
*/
const scratch = await mkdtemp(path.join(tmpdir(), "forge-routing-"));
process.env.FORGE_CONFIG_DIR = path.join(scratch, "config");
process.env.FORGE_DECKS_DIR = path.join(scratch, "decks");
process.env.FORGE_REFERENCE_DIR = path.join(scratch, "reference");
process.env.FORGE_DB_PATH = path.join(scratch, "config", "forge.db");
process.env.FORGE_API_PORT = "0";
// No SMTP: confirmation is not enforced, so these accounts are usable the
// moment they exist and the test is about routing, not about the gate.
for (const k of ["FORGE_SMTP_HOST", "FORGE_SMTP_USER", "FORGE_SMTP_PASS", "FORGE_SMTP_FROM"]) delete process.env[k];
// A sweep scheduler would keep the process alive past the last test.
delete process.env.FORGE_SWEEP_DAYS;
delete process.env.FORGE_ADMIN_EMAIL;
await mkdir(process.env.FORGE_CONFIG_DIR, { recursive: true });
await mkdir(process.env.FORGE_DECKS_DIR, { recursive: true });
/** A deck on disk, owned by `owner` (omit for a legacy, operator-owned folder). */
async function seedDeck(slug, { owner, body }) {
const dir = path.join(process.env.FORGE_DECKS_DIR, slug);
await mkdir(dir, { recursive: true });
await writeFile(path.join(dir, "meta.yaml"), owner ? `slug: ${slug}\nowner: ${owner}\n` : `slug: ${slug}\n`);
await writeFile(path.join(dir, "deck.yaml"), body);
}
await seedDeck("mine-electrolysis", {
owner: "member@example.com",
body: "title: Electrolysis\nslides:\n - type: title\n headline: Electrolysis\n",
});
await seedDeck("theirs-rasterisation", {
owner: "other@example.com",
body: "title: Rasterisation\nslides:\n - type: title\n headline: Electrolysis too\n",
});
const auth = await import("../src/auth.js");
await auth.register({ name: "Member", email: "member@example.com", password: "member password" });
const user = await auth.authenticate("member@example.com", "member password");
assert.ok(user, "the test account exists");
assert.notEqual(user.role, "admin", "and is deliberately NOT an admin — an admin passes the gate that hid this");
const token = await auth.startSession(user);
const { server } = await import("../app/server/index.js");
if (!server.listening) await new Promise((r) => server.once("listening", r));
const base = `http://127.0.0.1:${server.address().port}`;
test.after(async () => {
await new Promise((r) => server.close(r));
const { closeDb } = await import("../src/db.js");
closeDb();
await rm(scratch, { recursive: true, force: true });
});
const post = (route, body) =>
fetch(`${base}${route}`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify(body),
});
test("deck search reaches its handler as a non-admin, not the per-slug gate", async () => {
const res = await post("/api/decks/search", { q: "electrolysis" });
const json = await res.json();
// The bug's signature: a 404 "no such deck" from the ownership middleware,
// which had matched the literal segment as a deck named "search".
assert.notEqual(json.error, "no such deck", "the ownership gate must not claim this path");
assert.equal(res.status, 200);
assert.equal(json.ok, true);
assert.ok(Array.isArray(json.hits), "the search handler answered");
});
test("search stays scoped to the caller's own decks", async () => {
const json = await (await post("/api/decks/search", { q: "electrolysis" })).json();
// Both decks contain the word; only one is this account's. Reaching the
// handler must not mean reaching everyone's content.
assert.deepEqual(json.hits, ["mine-electrolysis"]);
});
test("an empty query is answered, not refused", async () => {
const json = await (await post("/api/decks/search", { q: " " })).json();
assert.equal(json.ok, true);
assert.deepEqual(json.hits, []);
});
test("the per-slug gate still refuses a deck the caller does not own", async () => {
// The other half of the same property: moving the route above the middleware
// must not have moved anything else out from under it.
const res = await fetch(`${base}/api/decks/theirs-rasterisation`, {
headers: { Authorization: `Bearer ${token}` },
});
assert.equal(res.status, 404);
assert.equal((await res.json()).error, "no such deck");
});
test("the workspace still refuses an unauthenticated caller", async () => {
const res = await fetch(`${base}/api/decks/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ q: "electrolysis" }),
});
assert.equal(res.status, 401, "search sits below the workspace gate, not above it");
});
/* ------------------------------------------------- tiers are an admin lever */
/**
* The tier an account is metered against is install-wide state that costs the
* operator money, so it must be admin-gated like every other setting written
* outside a user's own scope. This account is deliberately not an admin.
*/
test("a non-admin cannot move themselves onto a bigger tier", async () => {
const res = await post("/api/admin/users/member@example.com/plan", { plan: "unlimited" });
assert.equal(res.status, 403);
assert.equal((await res.json()).error, "admin only");
const { planFor } = await import("../src/limits.js");
assert.equal(
planFor(auth.getUserId("member@example.com")), "free",
"a refused request must not have changed the tier anyway",
);
});
test("a non-admin cannot read everyone's spend", async () => {
const res = await fetch(`${base}/api/admin/users`, { headers: { Authorization: `Bearer ${token}` } });
assert.equal(res.status, 403);
});
test("an account can read its own usage, and is told which tier and what is left", async () => {
const res = await fetch(`${base}/api/auto/usage`, { headers: { Authorization: `Bearer ${token}` } });
assert.equal(res.status, 200);
const json = await res.json();
assert.equal(json.plan, "free");
assert.equal(json.planLabel, "Free");
// A refusal is a dead end unless the surface can see it coming.
assert.ok(Number.isFinite(json.remaining.weeklyTokens), "the token budget must be legible before it runs out");
assert.ok(json.limits.weeklyTokens > 0);
});
test("the usage route says how much of the free trial is left", async () => {
// A trial that never resets is the one budget a user most needs to see
// coming, and the only place it can be shown from is here.
const json = await (await fetch(`${base}/api/auto/usage`, {
headers: { Authorization: `Bearer ${token}` },
})).json();
assert.ok(json.trial, "a free account must be told it is on a trial");
assert.equal(json.trial.spent, 0);
assert.ok(json.trial.cap > 0);
assert.equal(json.trial.remaining, json.trial.cap);
});