// 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);