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.
73 lines
3.9 KiB
JavaScript
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);
|