201 lines
6.1 KiB
JavaScript
201 lines
6.1 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import request from "supertest";
|
|
import { newDb } from "pg-mem";
|
|
import { createApp } from "../server/app.js";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const password = "this is a long password";
|
|
|
|
async function setup() {
|
|
const mem = newDb();
|
|
mem.public.registerFunction({
|
|
name: "gen_random_uuid",
|
|
returns: "uuid",
|
|
implementation: () => crypto.randomUUID(),
|
|
impure: true,
|
|
});
|
|
const pg = mem.adapters.createPg();
|
|
const db = new pg.Pool();
|
|
await db.query(
|
|
`CREATE TABLE users(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),email text UNIQUE NOT NULL,password_hash text NOT NULL,role text NOT NULL DEFAULT 'user',created_at timestamptz DEFAULT now()); CREATE TABLE sessions(token_hash text PRIMARY KEY,user_id uuid NOT NULL REFERENCES users(id),csrf_hash text NOT NULL,expires_at timestamptz NOT NULL,created_at timestamptz DEFAULT now()); CREATE TABLE roast_plans(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),user_id uuid NOT NULL REFERENCES users(id),plan jsonb NOT NULL,created_at timestamptz DEFAULT now(),updated_at timestamptz DEFAULT now()); CREATE TABLE app_settings(key text PRIMARY KEY,value text NOT NULL); INSERT INTO app_settings VALUES('signup_enabled','true')`,
|
|
);
|
|
const app = createApp({
|
|
db,
|
|
root,
|
|
env: {
|
|
NODE_ENV: "test",
|
|
BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token",
|
|
},
|
|
});
|
|
return { db, app, agent: request.agent(app) };
|
|
}
|
|
|
|
async function signup(agent, email) {
|
|
const response = await agent
|
|
.post("/api/auth/signup")
|
|
.send({ email, password });
|
|
return { response, csrf: response.body.csrfToken };
|
|
}
|
|
|
|
test("strict CSP/static modules, no-store data, auth lifecycle, and ownership share one database", async () => {
|
|
const { db, app, agent: first } = await setup();
|
|
const second = request.agent(app);
|
|
const anonymous = request.agent(app);
|
|
|
|
const landing = await anonymous.get("/");
|
|
assert.equal(landing.status, 200);
|
|
assert.match(
|
|
landing.headers["content-security-policy"],
|
|
/default-src 'self'/,
|
|
);
|
|
assert.doesNotMatch(
|
|
landing.headers["content-security-policy"],
|
|
/(?:default-src|script-src)[^;]*unsafe-inline/,
|
|
);
|
|
assert.match(
|
|
landing.text,
|
|
/<script type="module" src="\/js\/landing\.js"><\/script>/,
|
|
);
|
|
assert.doesNotMatch(landing.text, /<script type="module">/);
|
|
const adminHtml = await anonymous.get("/admin");
|
|
assert.equal(adminHtml.status, 401);
|
|
assert.equal(adminHtml.headers["cache-control"], "no-store, private");
|
|
assert.equal((await anonymous.get("/api/plans")).status, 401);
|
|
assert.equal(
|
|
(await anonymous.get("/api/plans")).headers["cache-control"],
|
|
"no-store, private",
|
|
);
|
|
assert.match(
|
|
(await anonymous.get("/js/admin.js")).text,
|
|
/async function load/,
|
|
);
|
|
const mainScript = await anonymous.get("/js/main.js");
|
|
assert.match(mainScript.text, /roastPlannerPlan\.v2/);
|
|
assert.match(mainScript.text, /localStorage\.removeItem\(key\)/);
|
|
const serviceWorker = (await anonymous.get("/sw.js")).text;
|
|
assert.match(serviceWorker, /data-free authenticated shell/);
|
|
assert.match(
|
|
serviceWorker,
|
|
/url\.pathname === "\/app"[\s\S]*caches\.match\("\/app"\)/,
|
|
);
|
|
assert.match(serviceWorker, /logout clears that namespace/);
|
|
|
|
const one = await signup(first, "[email protected]");
|
|
const two = await signup(second, "[email protected]");
|
|
assert.equal(one.response.status, 201);
|
|
assert.equal(two.response.status, 201);
|
|
const plan = await first
|
|
.post("/api/plans")
|
|
.set("x-csrf-token", one.csrf)
|
|
.send({ plan: { fields: { 0.1: "Private" } } });
|
|
assert.equal(plan.status, 201);
|
|
assert.equal(
|
|
(await first.get("/api/plans")).headers["cache-control"],
|
|
"no-store, private",
|
|
);
|
|
assert.equal(
|
|
(
|
|
await second
|
|
.put(`/api/plans/${plan.body.plan.id}`)
|
|
.set("x-csrf-token", two.csrf)
|
|
.send({ plan: {} })
|
|
).status,
|
|
404,
|
|
);
|
|
assert.equal((await second.get("/api/plans")).body.plans.length, 0);
|
|
|
|
const login = request.agent(app);
|
|
assert.equal(
|
|
(
|
|
await login
|
|
.post("/api/auth/login")
|
|
.send({ email: "[email protected]", password })
|
|
).status,
|
|
200,
|
|
);
|
|
assert.equal((await login.get("/api/auth/me")).status, 200);
|
|
const loginCsrf = (
|
|
await login
|
|
.post("/api/auth/login")
|
|
.send({ email: "[email protected]", password })
|
|
).body.csrfToken;
|
|
assert.equal(
|
|
(await login.post("/api/auth/logout").set("x-csrf-token", loginCsrf))
|
|
.status,
|
|
200,
|
|
);
|
|
assert.equal((await login.get("/api/auth/me")).status, 401);
|
|
|
|
const admin = await first.post("/api/auth/bootstrap").send({
|
|
email: "[email protected]",
|
|
password,
|
|
setupToken: "a-secure-bootstrap-token",
|
|
});
|
|
assert.equal(admin.status, 201);
|
|
assert.equal((await first.get("/api/admin/users")).status, 200);
|
|
assert.equal((await second.get("/api/admin/users")).status, 403);
|
|
assert.equal(
|
|
(
|
|
await first
|
|
.put("/api/admin/signup-enabled")
|
|
.set("x-csrf-token", admin.body.csrfToken)
|
|
.send({ enabled: false })
|
|
).status,
|
|
200,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await anonymous
|
|
.post("/api/auth/signup")
|
|
.send({ email: "[email protected]", password })
|
|
).status,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
(await db.query("SELECT count(*)::int AS count FROM users")).rows[0].count,
|
|
3,
|
|
);
|
|
});
|
|
|
|
test("bootstrap token is optional after first setup and unavailable before setup without one", async () => {
|
|
const { app, db } = await setup();
|
|
const agent = request.agent(app);
|
|
assert.equal(
|
|
(
|
|
await agent.post("/api/auth/bootstrap").send({
|
|
email: "[email protected]",
|
|
password,
|
|
setupToken: "wrong",
|
|
})
|
|
).status,
|
|
403,
|
|
);
|
|
const noTokenApp = createApp({ db, root, env: { NODE_ENV: "test" } });
|
|
assert.equal(
|
|
(
|
|
await request(noTokenApp).post("/api/auth/bootstrap").send({
|
|
email: "[email protected]",
|
|
password,
|
|
})
|
|
).status,
|
|
503,
|
|
);
|
|
await db.query(
|
|
"INSERT INTO users(email,password_hash,role) VALUES($1,$2,'admin')",
|
|
["[email protected]", "not-used-in-this-test"],
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(noTokenApp).post("/api/auth/bootstrap").send({
|
|
email: "[email protected]",
|
|
password,
|
|
})
|
|
).status,
|
|
409,
|
|
);
|
|
});
|