/** * Git-push scratch sites, sweep and metering against a running dev server. * * SITES_E2E_URL=http://localhost:3000 bun run e2e/sites-scratch-git.e2e.ts */ import { createScratchRepo, d1, DOMAIN, gitPush, HTTP_OK, log, runCron, send, waitForSite, WORKER } from "./helpers"; const HTTP_NOT_FOUND = 404; const HTTP_GONE = 410; let failures = 0; function check(name: string, ok: boolean, detail = ""): void { log(`${ok ? "ok " : "FAIL"} ${name}${ok || !detail ? "" : ` -- ${detail}`}`); if (!ok) failures += 1; } const scratch = await createScratchRepo(); const host = new URL(scratch.url).hostname; const siteId = host.split("--")[0]; check( "POST /api/v1/scratch returns a remote, push token and claim token", scratch.pushToken.startsWith("gitfor_scratch_") && scratch.claimToken.startsWith("gitfor_claim_") && scratch.remote.endsWith(".git"), ); const marker = `git-${Date.now()}`; const pushed = await gitPush(scratch, { "index.html": `

${marker}

`, "_worker.js": WORKER }); check("git push with the scratch token succeeds", pushed.code === 0, pushed.output); const live = await waitForSite(host, marker); check("the push deploys to the scratch host", live.body.includes(marker), `${live.status} ${live.body.slice(0, 200)}`); const previewMarker = `preview-${Date.now()}`; const branchPush = await gitPush(scratch, { "index.html": `

${previewMarker}

` }, "feature"); check("pushing another branch succeeds", branchPush.code === 0, branchPush.output); const preview = await waitForSite(`feature--${host.replace(`.${DOMAIN}`, "")}.${DOMAIN}`, previewMarker); check( "each branch push deploys a preview on its own host", preview.body.includes(previewMarker), `${preview.status} ${preview.body.slice(0, 200)}`, ); const wrong = await gitPush({ ...scratch, pushToken: "gitfor_scratch_wrong" }, { "index.html": "x" }); check("a wrong push token is refused", wrong.code !== 0); const tag = await gitPush(scratch, { "index.html": "tag" }, "main", "HEAD:refs/tags/v1"); check("scratch repos refuse tag pushes", tag.code !== 0 && tag.output.includes("branch pushes only"), tag.output); await send(host, "/"); await Bun.sleep(1500); const usage = await d1<{ requests: number; bytes_out: number }>( "SELECT requests, bytes_out FROM site_usage WHERE site_id = ?", [siteId], ); check( "site requests and bytes are metered", (usage[0]?.requests ?? 0) > 0 && (usage[0]?.bytes_out ?? 0) > 0, JSON.stringify(usage), ); const fetched = await send(host, "/echo"); check("dynamic scratch sites still run", fetched.status === HTTP_OK, `${fetched.status}`); const doomed = await createScratchRepo(); const doomedHost = new URL(doomed.url).hostname; const doomedId = doomedHost.split("--")[0]; await gitPush(doomed, { "index.html": "

doomed

" }); await waitForSite(doomedHost, "doomed"); await d1("UPDATE sites SET expires_at = 1 WHERE id = ?", [doomedId]); await d1("UPDATE repos SET created_at = 1 WHERE name = ? AND owner_id = 'sitesscratchaccount0'", [doomedId]); await runCron(); await Bun.sleep(3000); await runCron(); await Bun.sleep(5000); const remaining = await d1<{ id: string }>("SELECT key AS id FROM scope_locators WHERE kind = 'site' AND key = ?", [ doomedId, ]); const repos = await d1<{ id: string }>( "SELECT r.id FROM repos r JOIN accounts a ON a.id = r.owner_id WHERE a.name = 'scratch' AND r.name = ?", [doomedId], ); check("the sweep deletes an expired scratch site", remaining.length === 0, JSON.stringify(remaining)); check("the sweep deletes its scratch repository", repos.length === 0, JSON.stringify(repos)); const gone = await send(doomedHost, "/"); check( "an expired scratch host stops serving", gone.status === HTTP_NOT_FOUND || gone.status === HTTP_GONE, `${gone.status}`, ); log(`\nscratch repo: ${scratch.repo} site: ${scratch.url} claim: ${scratch.claimUrl}`); log(failures ? `\n${failures} check(s) failed` : "\nall checks passed"); process.exit(failures ? 1 : 0);