bidaangmamamayan/tests/board-test.mjs
Ryan 6b06e08c08 BidaAngMamayan: public supporter tribute news desk for Senator BAM Aquino
Forked from Forthena (PERSONAL/forthena) and retargeted:
- NO login, NO LLM: the edition is public and the ranking is deterministic
  (stories that name Sen. BAM lead; freshest first) — Daniel: no
  like/dislike or store profile for visitors.
- Crawler scoped to BAM: Bing news keyword queries + Inquirer/Rappler
  fallback wires, with a two-tier relevance gate (named-BAM always;
  Aquino+legislative-context only from fallback) so op-eds and non-Bam
  senators are dropped.
- Desks: BAM (lead), Gaming, Education, Politics, Energy, PH, Business.
- Community board /board: 'Thank you Senator BAM' — nickname-only posts,
  HTML-stripped, URL-only rejected, IP rate-limited (3/10min, 20/day).
- Yellow-gamer theme (Orbitron/Rajdhani/JetBrains Mono, dark + gold,
  HUD corners), supporter disclaimer in header and footer.
- Tests: category/date/relevance/text suites + Mongo board suite +
  full HTTP e2e (all passing).

Infra: 66.181.46.132 (bidaangmamamayan.com), local Mongo 7.0,
nginx + certbot TLS, PM2 bama (:3000) + 15-min crawl cron.
2026-09-15 08:37:03 +00:00

73 lines
3.9 KiB
JavaScript

// 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: " <script>alert(1)</script>TagaBAM ", message: " <b>Bold</b> thanks & <a href='x'>link</a> 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);