import test from "node:test"; import assert from "node:assert/strict"; import { callPost, holdPublishQueue, setupOpsCorpus } from "../_testCorpus"; // Run with: // pnpm -C editor exec tsx --test "app/api/ops/deploy-site/route.test.ts" // // The deploy routes' refusals — preview names, a site with no Pages project, a // site never built, the body's spelling — all decided BEFORE a job exists, // which is the whole of what they are. Moved from e2e/ops-api.spec.ts // (release 19, A2b). The publish queue is held, and wrangler and next are the // e2e fakes, so a regressed refusal could only queue a stage. const corpus = await setupOpsCorpus("title-filter-channel"); await corpus.writeSite("previewsite", { cloudflareProject: "proj" }); // NO Cloudflare project, on purpose (see the valid-preview test). await corpus.writeSite("noproj", {}); const deploySite = (await import("./route")).POST; const buildDeploy = (await import("../build-deploy/route")).POST; await holdPublishQueue(); test.after(() => corpus.cleanup()); const BOTH = [ ["deploy-site", deploySite], ["build-deploy", buildDeploy], ] as const; test("deploy-site and build-deploy refuse a preview name that is not one, before any job", async () => { for (const [action, handler] of BOTH) { // "main" is the production branch: deploying there is not a preview, it is // the live site, and the refusal says so rather than shipping it. const main = await callPost(handler, { siteId: "previewsite", preview: "main" }); assert.equal(main.status, 400, action); assert.match(String(main.body.error), /production branch/, action); // Uppercase is refused rather than lowercased FOR the caller: silently // rewriting "Main" into "main" would deploy to production. const upper = await callPost(handler, { siteId: "previewsite", preview: "Main" }); assert.equal(upper.status, 400, action); assert.match(String(upper.body.error), /not a valid preview branch name/, action); assert.doesNotMatch(String(upper.body.error), /production branch/, action); const spaced = await callPost(handler, { siteId: "previewsite", preview: "bad name" }); assert.equal(spaced.status, 400, action); assert.match(String(spaced.body.error), /not a valid preview branch name/, action); const tooLong = await callPost(handler, { siteId: "previewsite", preview: "a".repeat(29) }); assert.equal(tooLong.status, 400, action); assert.match(String(tooLong.body.error), /at most 28 characters/, action); } assert.deepEqual(await corpus.listJobIds(), []); }); test("a valid preview is accepted and reaches the action, on both deploy routes", async () => { // The action's own refusal is the last gate before wrangler, so a 400 saying // THAT — rather than "unknown key: preview" or a branch complaint — proves // the name passed validation and the route got all the way to the action. for (const [action, handler] of BOTH) { const { status, body } = await callPost(handler, { siteId: "noproj", preview: "tags-exclude" }); assert.equal(status, 400, action); assert.match(String(body.error), /no Cloudflare Pages project/, action); assert.doesNotMatch(String(body.error), /unknown key/, action); } assert.deepEqual(await corpus.listJobIds(), []); }); test("deploy-site refuses a site that was never built, in the deploy stage's words, before any job", async () => { // A deploy ships the site's OWN bundle (export/.export-builds//out, // stamped built.json) — never "whatever export/out holds". const { status, body } = await callPost(deploySite, { siteId: "previewsite" }); assert.equal(status, 400); assert.match( String(body.error), /^no build of previewsite in .*\.export-builds\/previewsite — archilyzer publish build previewsite$/, ); // A preview is refused for the same reason and just as early. const preview = await callPost(deploySite, { siteId: "previewsite", preview: "tags-exclude" }); assert.equal(preview.status, 400); assert.match(String(preview.body.error), /no build of previewsite/); assert.deepEqual(await corpus.listJobIds(), []); }); test("deploy-site takes siteId or siteIds, and has no all", async () => { const both = await callPost(deploySite, { siteId: "a", siteIds: ["a"] }); assert.equal(both.status, 400); assert.match(String(both.body.error), /not both/); const neither = await callPost(deploySite, {}); assert.equal(neither.status, 400); assert.match(String(neither.body.error), /siteId/); const bad = await callPost(deploySite, { siteIds: ["deploysite", "BAD ID"] }); assert.equal(bad.status, 400); assert.match(String(bad.body.error), /not a valid site id/); // Deploy-only has no all-sites form — build-deploy owns that — so `all` is // an unknown key here rather than a second spelling of it. const all = await callPost(deploySite, { all: true }); assert.equal(all.status, 400); assert.match(String(all.body.error), /unknown key/); assert.deepEqual(await corpus.listJobIds(), []); });