#!/usr/bin/env bun /** * End-to-end check for integrations: signs up, creates an org, adds a generic webhook integration that * points at a local sink, sends a test event and verifies the signed delivery arrives and is logged. * Also checks that a private-network URL is rejected (SSRF guard). * * The target server must run with INTEGRATIONS_ALLOW_LOOPBACK=true (in .dev.vars) so the loopback sink * is reachable; production never sets it. * * E2E_BASE_URL=http://localhost:3000 bun run test:e2e:integrations */ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { chromium } from "playwright-core"; const BASE_URL = process.env.E2E_BASE_URL ?? "http://localhost:3000"; const SINK_PORT = Number(process.env.E2E_SINK_PORT ?? 4545); const TIMEOUT_MS = 60_000; const DUPLICATE_WINDOW_MS = 20_000; type Received = { headers: Record; body: string }; const received: Received[] = []; const sink = Bun.serve({ port: SINK_PORT, hostname: "127.0.0.1", async fetch(request) { received.push({ headers: Object.fromEntries(request.headers), body: await request.text() }); return new Response("ok"); }, }); function fail(message: string): never { console.error(`FAIL: ${message}`); process.exit(1); } async function waitFor(what: string, check: () => T | undefined | Promise): Promise { const deadline = Date.now() + TIMEOUT_MS; while (Date.now() < deadline) { const value = await check(); if (value !== undefined) return value; await Bun.sleep(500); } return fail(`timed out waiting for ${what}`); } const suffix = randomBytes(4).toString("hex"); const username = `e2e${suffix}`; const org = `e2eorg${suffix}`; const browser = await chromium .launch({ headless: true, executablePath: process.env.E2E_CHROME || undefined }) .catch(() => chromium.launch({ headless: true, channel: "chrome" })); const page = await browser.newPage(); try { await page.goto(`${BASE_URL}/signup`, { waitUntil: "networkidle" }); await page.getByLabel("Username").fill(username); await page.getByLabel("Email").fill(`${username}@example.com`); await page.getByLabel("Password", { exact: true }).fill(`pw-${suffix}-${suffix}`); await page.getByRole("button", { name: /sign up|create account/i }).click(); await page.waitForURL((url) => !url.pathname.startsWith("/signup"), { timeout: TIMEOUT_MS }); await page.goto(`${BASE_URL}/orgs/new`, { waitUntil: "networkidle" }); await page.getByLabel("Organization name").fill(org); await page.getByRole("button", { name: /create/i }).click(); await page.waitForURL(`${BASE_URL}/${org}`, { timeout: TIMEOUT_MS }); await page.goto(`${BASE_URL}/orgs/${org}/integrations`, { waitUntil: "networkidle" }); await page.getByLabel("Type").selectOption("webhook"); const urlField = page.getByLabel("Webhook URL"); await urlField.fill("https://10.0.0.1/hook"); await page.getByRole("button", { name: "Add integration" }).click(); await page.getByText(/Only public https/).waitFor({ timeout: TIMEOUT_MS }); console.log("ok: private-network URL rejected"); await urlField.fill("https://127.0.0.1.nip.io/hook"); await page.getByRole("button", { name: "Add integration" }).click(); await page.getByText(/resolves to a non-public address/).waitFor({ timeout: TIMEOUT_MS }); console.log("ok: DNS-rebinding hostname rejected"); await urlField.fill(`http://127.0.0.1:${SINK_PORT}/hook`); await page.getByRole("button", { name: "Add integration" }).click(); const secret = (await page.getByTestId("signing-secret").textContent({ timeout: TIMEOUT_MS }))?.trim(); if (!secret) fail("no signing secret shown"); console.log("ok: integration created"); await page.getByRole("button", { name: "Send test event" }).click(); const delivery = await waitFor("a delivery at the sink", () => received.find((item) => item.headers["x-gitfor-event"] === "ping"), ); const expected = `sha256=${createHmac("sha256", secret).update(delivery.body).digest("hex")}`; const actual = delivery.headers["x-gitfor-signature-256"] ?? ""; if (expected.length !== actual.length || !timingSafeEqual(Buffer.from(expected), Buffer.from(actual))) { fail("signature mismatch"); } const body = JSON.parse(delivery.body) as { event: string; summary: { title: string } }; if (body.event !== "ping" || body.summary.title !== "Test event from gitfor") fail("unexpected payload"); console.log("ok: signed test event delivered"); await waitFor("the delivery log to show success", async () => { await page.reload(); const text = (await page.getByTestId("delivery-log").first().textContent()) ?? ""; return text.includes("success") ? true : undefined; }); console.log("ok: delivery logged"); await Bun.sleep(DUPLICATE_WINDOW_MS); const pings = received.filter((item) => item.headers["x-gitfor-event"] === "ping"); if (pings.length !== 1) fail(`expected exactly one test delivery, got ${pings.length}`); console.log("ok: test event delivered exactly once"); const before = received.length; await page.getByRole("button", { name: "Redeliver" }).first().click(); await waitFor("a redelivery", () => (received.length > before ? true : undefined)); await Bun.sleep(DUPLICATE_WINDOW_MS); if (received.length !== before + 1) fail(`expected one redelivery, got ${received.length - before}`); const ids = new Set(received.map((item) => item.headers["x-gitfor-delivery"])); if (ids.size !== received.length) fail("a delivery id was delivered twice"); console.log("ok: redelivery delivered exactly once with a new delivery id"); console.log("PASS"); } finally { await browser.close(); await sink.stop(true); }