import { beforeEach, describe, expect, mock, test } from "bun:test"; import { Database } from "bun:sqlite"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; const published: string[] = []; mock.module("cloudflare:workers", () => ({ env: { PUBLIC_URL: "https://forge.example.com", SITES_DOMAIN: "sites.example.net", SECRETS_KEY: "test-secret", ABUSE_FORWARD_TO: "", KV: { put: async (key: string) => void published.push(key) }, }, })); const db = new Database(":memory:"); const migrations = join(import.meta.dir, "../../migrations"); for (const file of readdirSync(migrations).sort()) db.exec(readFileSync(join(migrations, file), "utf8")); mock.module("./db", () => ({ first: async (sql: string, ...params: unknown[]) => db.query(sql).get(...(params as never[])) ?? null, all: async (sql: string, ...params: unknown[]) => db.query(sql).all(...(params as never[])), run: async (sql: string, ...params: unknown[]) => ({ meta: { changes: db.query(sql).run(...(params as never[])).changes }, }), })); mock.module("./sites/deploy", () => ({ loadSiteById: async (id: string) => db.query("SELECT * FROM sites WHERE id = ?").get(id) ?? null, })); const { actOnAbuseReport, guessAbuseCategory, recordAbuseReport, restoreSite, siteIdForUrl } = await import("./abuse"); function site(id: string, label: string, scratch: number, disabled: string | null = null) { db.query( `INSERT INTO sites (id, owner_id, label, source, scratch, disabled_reason, created_at, updated_at) VALUES (?, 'owner', ?, 'upload', ?, ?, 0, 0)`, ).run(id, label, scratch, disabled); } function state(id: string) { return db.query("SELECT disabled_reason, paused_at FROM sites WHERE id = ?").get(id) as { disabled_reason: string | null; paused_at: number | null; }; } async function report(url: string, reporter: string, category: "phishing" | "spam" = "spam") { return recordAbuseReport({ source: "web", targetUrl: url, category, details: "", reporterEmail: null, reporterKey: reporter, }); } beforeEach(() => { db.exec("DELETE FROM abuse_reports; DELETE FROM site_deployments; DELETE FROM sites; DELETE FROM accounts;"); db.exec( "INSERT INTO accounts (id, name, kind, created_at) VALUES ('owner', 'owner', 'user', 0), ('admin', 'admin', 'user', 0)", ); published.length = 0; }); describe("siteIdForUrl", () => { test("resolves site labels, preview labels and custom domains", async () => { site("s1", "blog--alice", 0); db.exec("UPDATE sites SET custom_domain = 'www.alice.dev' WHERE id = 's1'"); db.exec( `INSERT INTO site_deployments (id, site_id, environment, label, source, ref, sha, kind, status, created_at) VALUES ('d1', 's1', 'preview', 'feat--blog--alice', 'upload', 'r', 'sha', 'static', 'active', 0)`, ); expect(await siteIdForUrl("https://blog--alice.sites.example.net/login")).toBe("s1"); expect(await siteIdForUrl("https://feat--blog--alice.sites.example.net/")).toBe("s1"); expect(await siteIdForUrl("https://WWW.alice.dev/x")).toBe("s1"); expect(await siteIdForUrl("https://forge.example.com/alice/blog")).toBeNull(); expect(await siteIdForUrl("not a url")).toBeNull(); }); }); describe("auto-pause", () => { test("owned sites pause after five distinct reporters, duplicates count once", async () => { site("s1", "blog--alice", 0); const url = "https://blog--alice.sites.example.net/"; for (const reporter of ["ip:1", "ip:1", "ip:2", "ip:3", "ip:4"]) await report(url, reporter); expect(state("s1").paused_at).toBeNull(); expect(db.query("SELECT COUNT(*) AS n FROM abuse_reports").get()).toEqual({ n: 4 }); const result = await report(url, "ip:5"); expect(result.paused).toBe(true); expect(state("s1").disabled_reason).toBe("Paused pending abuse review"); expect(published).toContain("sites:meta:s1"); }); test("phishing on a scratch site pauses after two reporters", async () => { site("s2", "abc--scratch", 1); const url = "https://abc--scratch.sites.example.net/"; expect((await report(url, "ip:1", "phishing")).paused).toBe(false); expect((await report(url, "ip:2", "phishing")).paused).toBe(true); }); test("never overrides a different disable and restore only lifts abuse pauses", async () => { site("s3", "old--alice", 0, "Repository was deleted"); const url = "https://old--alice.sites.example.net/"; for (const reporter of ["a", "b", "c", "d", "e"]) await report(url, reporter, "phishing"); expect(state("s3")).toEqual({ disabled_reason: "Repository was deleted", paused_at: null }); expect(await restoreSite("s3")).toBe(false); expect(state("s3").disabled_reason).toBe("Repository was deleted"); }); }); describe("admin actions", () => { test("restoring dismisses every open report for the site", async () => { site("s4", "shop--alice", 1); const url = "https://shop--alice.sites.example.net/"; await report(url, "x", "phishing"); await report(url, "y", "phishing"); const { id } = db.query("SELECT id FROM abuse_reports LIMIT 1").get() as { id: string }; await actOnAbuseReport(id, "restore", "admin"); expect(state("s4")).toEqual({ disabled_reason: null, paused_at: null }); expect(db.query("SELECT DISTINCT status, resolved_by FROM abuse_reports").all()).toEqual([ { status: "dismissed", resolved_by: "admin" }, ]); }); test("pause is rejected for reports that are not about a hosted site", async () => { await report("https://forge.example.com/alice/repo", "z"); const { id } = db.query("SELECT id FROM abuse_reports LIMIT 1").get() as { id: string }; await expect(actOnAbuseReport(id, "pause", "admin")).rejects.toThrow("not linked to a hosted site"); }); }); test("guessAbuseCategory", () => { expect(guessAbuseCategory("This page is a PayPal phishing login")).toBe("phishing"); expect(guessAbuseCategory("DMCA takedown notice")).toBe("copyright"); expect(guessAbuseCategory("hello")).toBe("other"); });