// Community board: caps, sanitisation, URL-only rejection, rate limiting.
// Runs against a scratch Mongo.
// RUN: MONGODB_URI=mongodb://127.0.0.1:27099/bama BAMA_DB_NAME=bama_board node tests/board-test.mjs
import { MongoClient } from "mongodb";
import { postBoard, getBoard, boardCount } from "../lib/board.js";
const uri = process.env.MONGODB_URI || "mongodb://127.0.0.1:27099/bama";
const mc = new MongoClient(uri, { serverSelectionTimeoutMS: 8000 });
await mc.connect();
const db = mc.db(process.env.BAMA_DB_NAME || "bama");
await db.collection("board").deleteMany({});
await db.collection("rate").deleteMany({});
let fails = 0;
const check = (name, cond, detail = "") => {
console.log(` ${cond ? "ok " : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
if (!cond) fails++;
};
const fakeReq = (ip = "10.0.0.1") => ({
headers: new Headers({ "x-forwarded-for": ip }),
get: (k) => ({ "x-forwarded-for": ip, "x-real-ip": ip }[k.toLowerCase()] || null),
});
console.log("posting:");
let r = await postBoard(fakeReq("10.0.0.1"), { nick: "PampangaProud", message: "Thank you, Senator BAM, for the scholarship bill!" });
check("basic post ok", r.ok, JSON.stringify(r));
check("nick clean", r.post && r.post.nick === "PampangaProud");
r = await postBoard(fakeReq("10.0.0.1"), { nick: "GamerBAM", message: "GMAmateur here — palaPINA 2025 runner-up and so proud. GG, Senator!" });
check("second post ok", r.ok);
console.log("sanitisation:");
r = await postBoard(fakeReq("10.0.0.2"), { nick: " TagaBAM ", message: " Bold thanks & link to the senator. " });
check("html stripped from nick", r.ok && r.post.nick === "alert(1) TagaBAM", JSON.stringify(r.post && r.post.nick));
check("html stripped from message", r.ok && r.post.message === "Bold thanks & link to the senator.", JSON.stringify(r.post && r.post.message));
r = await postBoard(fakeReq("10.0.0.3"), { nick: "SpamBot", message: "https://bad.example.com" });
check("pure-url rejected", !r.ok && r.status === 400, JSON.stringify(r));
r = await postBoard(fakeReq("10.0.0.3"), { nick: "RealFan", message: "Thank you Senator Bam Aquino for the scholarship expansion" });
check("real sentence kept (URL logic didn't false-positive)", r.ok, JSON.stringify(r));
console.log("caps:");
r = await postBoard(fakeReq("10.0.0.4"), { nick: "x".repeat(99), message: "thanks" });
check("nick capped to 24", r.ok && r.post.nick.length === 24, `len=${r.post && r.post.nick.length}`);
r = await postBoard(fakeReq("10.0.0.4"), { nick: "LongOne", message: "t".repeat(5000) });
check("message capped to 600", r.ok && r.post.message.length === 600, `len=${r.post && r.post.message.length}`);
console.log("validation:");
r = await postBoard(fakeReq("10.0.0.5"), { nick: "x", message: "thanks" });
check("1-char nick rejected", !r.ok && r.status === 400);
r = await postBoard(fakeReq("10.0.0.5"), { nick: "Fine", message: " " });
check("empty message rejected", !r.ok && r.status === 400);
console.log("rate limit:");
const ip6 = "10.0.0.6";
for (let i = 0; i < 3; i++) {
r = await postBoard(fakeReq(ip6), { nick: `Ratel${i}`, message: `message number ${i} from the same ip` });
check(`post ${i + 1} of 3 ok`, r.ok, JSON.stringify(r));
}
r = await postBoard(fakeReq(ip6), { nick: "RatelX", message: "a fourth message from the same ip in ten minutes" });
check("4th post in window 429", !r.ok && r.status === 429, JSON.stringify(r));
console.log("listing:");
const posts = await getBoard();
check("list newest-first, capped", posts.length >= 6 && posts.length <= 200);
check("list has no ip field", posts.every((p) => p.ip === undefined));
// 2 (10.0.0.1) + 1 (10.0.0.2) + 1 RealFan (10.0.0.3) + 2 (10.0.0.4) + 3 (10.0.0.6) = 9
check("boardCount is the admitted total", (await boardCount()) === 9, `count=${await boardCount()}`);
await mc.close();
console.log(fails ? `\n${fails} FAILED` : "\nall passed");
process.exit(fails ? 1 : 0);