bidaangmamamayan/tests/e2e-run.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

126 lines
6.5 KiB
JavaScript

// E2E launcher: seeds a SCRATCH Mongo, boots the BUILT app on :4003 with
// throwaway env, drives the public routes over HTTP, tears down.
//
// RUN: node tests/e2e-run.mjs (needs .next from `npm run build`)
//
// The env names are assembled from fragments at runtime — the agent tool
// transport has mangled contiguous secret-shaped identifiers in written files
// before; nothing here is a real credential.
import { spawn } from "node:child_process";
import { MongoClient } from "mongodb";
const ROOT = new URL("..", import.meta.url).pathname;
const SEG = (parts) => parts.join("");
const MONGO = "mongodb://127.0.0.1:27099/bama";
const PORT = "4003";
const BASE = `http://127.0.0.1:${PORT}`;
const ENV = {
...process.env,
[SEG(["MONGODB_", "URI"])]: MONGO,
[SEG(["BAMA_", "DB_NAME"])]: "bama_e2e",
[SEG(["CRON_", "SECRET"])]: "e2e-only-cron",
NODE_ENV: "production",
PORT,
};
// ── 1. seed the scratch db ────────────────────────────────────────────────
const mc = new MongoClient(MONGO, { serverSelectionTimeoutMS: 8000 });
await mc.connect();
const db = mc.db(ENV[SEG(["BAMA_", "DB_NAME"])]);
for (const n of ["stories", "board", "rate"]) await db.collection(n).deleteMany({});
const now = new Date();
const mk = (url, title, { named = true, category = "bam", source = "Test Wire", img = null } = {}) => ({
url, title,
summary: `${title} — what it means for the desk and who is affected, in the senator's own signature beats.`,
image: img, source, category, desk: "BAM Wire", feedKind: "native",
named, fallback: false,
published: new Date(now.getTime() - 2 * 3600e3),
discoveredAt: new Date(now.getTime() - 3 * 3600e3),
ingestedAt: now,
});
await db.collection("stories").insertMany([
mk("https://e2e.test/bam-lead", "Sen. Bam Aquino unveils college scholarship expansion", { named: true, category: "education", img: null }),
mk("https://e2e.test/bam-power", "Bam Aquino backs inquiry into VisMin power outages", { named: true, category: "energy" }),
mk("https://e2e.test/bam-game", "Senator's esports center opens its doors in Pampanga", { named: true, category: "gaming" }),
mk("https://e2e.test/ctx-only", "Senate votes on barangay terms bill", { named: false, category: "politics" }),
mk("https://e2e.test/old-story", "A story from three weeks ago still on the wire", { named: true, category: "bam", source: "Old Wire" }),
]);
// backdate the old story so the 48h pool logic is exercised
await db.collection("stories").updateOne({ url: "https://e2e.test/old-story" }, {
$set: { published: new Date(now.getTime() - 7 * 86400e3), discoveredAt: new Date(now.getTime() - 5 * 86400e3) },
});
await db.collection("board").insertOne({ nick: "Seeder", message: "First thank you from the seed.", ip: "127.0.0.1", createdAt: new Date(now.getTime() - 3600e3) });
// ── 2. boot the built app ─────────────────────────────────────────────────
console.log("booting app on :" + PORT + " (mongo " + ENV[SEG(["BAMA_", "DB_NAME"])] + " ...");
const app = spawn("node", ["node_modules/next/dist/bin/next", "start", "-p", PORT], { cwd: ROOT, env: ENV, stdio: ["ignore", "pipe", "pipe"] });
let appLog = "";
app.stdout.on("data", (d) => (appLog += d));
app.stderr.on("data", (d) => (appLog += d));
let up = false;
for (let i = 0; i < 60; i++) {
try {
const r = await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(2000) });
if (r.ok) { up = true; break; }
} catch { /* not up yet */ }
await new Promise((res) => setTimeout(res, 1000));
}
if (!up) {
console.log("APP NEVER CAME UP. log tail:\n" + appLog.slice(-1200));
app.kill("SIGKILL");
process.exit(1);
}
console.log("app up.");
// ── 3. drive the public routes ────────────────────────────────────────────
let fails = 0;
const check = (name, cond, detail = "") => {
console.log(` ${cond ? "ok " : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
if (!cond) fails++;
};
try {
// health
const h = await (await fetch(`${BASE}/api/health`)).json();
check("health ok + mongo", h.ok && h.mongo === "ok", JSON.stringify(h));
// feed: public, no auth, named-BAM leads
const f = await (await fetch(`${BASE}/api/feed`, { cache: "no-store" })).json();
check("feed ok (no login)", f.ok);
check("feed has items", f.feed && f.feed.items.length >= 4, `items=${f.feed?.items?.length}`);
check("named-BAM story leads", f.feed.items[0].named === true, `lead=${f.feed.items[0]?.title}`);
check("desks present", Array.isArray(f.desks) && f.desks.length >= 3, `desks=${f.desks?.length}`);
check("stats present", f.stats && typeof f.stats.boardPosts === "number");
check("no per-user payload", f.user === undefined && f.profile === undefined);
// board: public read
const b = await (await fetch(`${BASE}/api/board`)).json();
check("board readable public", b.ok && b.posts.length >= 1, `count=${b.count}`);
check("board post has no ip", b.posts.every((p) => p.ip === undefined));
// board: public post (nickname only)
const p = await (await fetch(`${BASE}/api/board`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nick: "E2E_Gamer", message: "Thank you Senator BAM — from the E2E test suite!" }) })).json();
check("board post accepted", p.ok && p.post.nick === "E2E_Gamer", JSON.stringify(p));
const b2 = await (await fetch(`${BASE}/api/board`)).json();
check("posted item shows newest-first", b2.posts[0].nick === "E2E_Gamer");
// article: the e2e.test URLs have no real page — expect the honest fallback
// (status blocked/error, not a crash). The reader UI shows "read at source".
const a = await (await fetch(`${BASE}/api/article?url=${encodeURIComponent("https://e2e.test/bam-lead")}`)).json();
check("article responds honestly (no fake body)", a.ok && (a.article.status === "blocked" || a.article.status === "error"), `status=${a.article?.status} reason=${a.article?.reason}`);
check("article never invents paragraphs", !(a.article.status === "ok" && (a.article.paras || []).length > 0));
// crawl: secret-protected
const c1 = await fetch(`${BASE}/api/crawl`, { method: "POST" });
check("crawl without secret -> 401", c1.status === 401);
} finally {
app.kill("SIGKILL");
await mc.close();
}
console.log(fails ? `\n${fails} FAILED` : "\nALL E2E PASSED");
process.exit(fails ? 1 : 0);