484 lines · 21.3 KB
#!/usr/bin/env bun
/**
 * End-to-end check for notification coverage against a running dev server whose email goes to the dev
 * mail sink (EMAIL_DEV_SINK=kv, or any localhost PUBLIC_URL without a verified sender). Run it from the
 * worktree whose `vite dev` is under test, since it seeds users and sessions in that server's local D1/KV.
 *
 *   BASE_URL=http://localhost:3000 bun scripts/e2e-notify.ts
 *   BASE_URL=http://localhost:3017 bun scripts/e2e-notify.ts ci security   # only some cases
 *
 * The "digest" case needs NOTIFY_DIGEST_DELAY_MS=0 (dev forges only) so the cron flushes immediately.
 *
 * Each case triggers a real event over HTTP and asserts both the web notification (through the
 * listNotifications server function) and the captured email (through /__dev/mail), plus the
 * unsubscribe headers, redaction of secret values and send-time access checks.
 */
import { createHash, randomBytes } from "node:crypto";
import { toJSONAsync } from "seroval";

const BASE = (process.env.BASE_URL ?? "http://localhost:3000").replace(/\/$/, "");
const ROOT = new URL("..", import.meta.url).pathname;
const SESSION_COOKIE = "__Host-gitfor_session";
const POLL_MS = 1000;
const TIMEOUT_MS = Number(process.env.NOTIFY_TIMEOUT_MS ?? 90_000);
const LFS_LIMIT = 10 * 1024 * 1024 * 1024;
const LFS_NEAR_LIMIT = Math.floor(LFS_LIMIT * 0.9);
const STALE_RUNNER_MS = 15 * 60 * 1000;
const HTTP_OK = 200;
const HTTP_CREATED = 201;
const HTTP_NOT_FOUND = 404;
const FAKE_AWS_KEY = `AKIA${"QX7Z".repeat(4)}`;

type User = { id: string; name: string; email: string; token: string; cookie: string };
type Mail = { id: string; to: string; subject: string; text: string; html: string; headers?: Record<string, string> };
type WebNotification = { reason: string; title: string; url: string };

function sh(cmd: string[]): string {
  const result = Bun.spawnSync(cmd, { cwd: ROOT, stdout: "pipe", stderr: "pipe" });
  if (result.exitCode !== 0) throw new Error(`${cmd.join(" ")} failed: ${result.stderr.toString()}`);
  return result.stdout.toString();
}