From ea4831207765514bb3ec9d22474a02abc8cf1779 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:41:59 +0000 Subject: [PATCH] add e2e tests --- packages/wrangler/e2e/provisioning.test.ts | 173 ++++++++++++++++++ packages/wrangler/src/d1/list.ts | 6 +- packages/wrangler/src/deploy/deploy.ts | 8 +- packages/wrangler/src/deploy/index.ts | 8 + .../src/deployment-bundle/bindings.ts | 59 +++--- packages/wrangler/src/kv/helpers.ts | 6 +- 6 files changed, 230 insertions(+), 30 deletions(-) create mode 100644 packages/wrangler/e2e/provisioning.test.ts diff --git a/packages/wrangler/e2e/provisioning.test.ts b/packages/wrangler/e2e/provisioning.test.ts new file mode 100644 index 000000000000..2b5063717039 --- /dev/null +++ b/packages/wrangler/e2e/provisioning.test.ts @@ -0,0 +1,173 @@ +import assert from "node:assert"; +import dedent from "ts-dedent"; +import { fetch } from "undici"; +import { beforeAll, describe, expect, it } from "vitest"; +import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; +import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; +import { fetchText } from "./helpers/fetch-text"; +import { generateResourceName } from "./helpers/generate-resource-name"; +import { normalizeOutput } from "./helpers/normalize"; +import { retry } from "./helpers/retry"; + +const TIMEOUT = 500_000; +const normalize = (str: string) => { + return normalizeOutput(str, { + [CLOUDFLARE_ACCOUNT_ID]: "CLOUDFLARE_ACCOUNT_ID", + }).replaceAll( + /- KV: ([0-9a-f]{32})/gm, + "- KV: 00000000000000000000000000000000" + ); +}; +const workerName = generateResourceName(); + +describe("provisioning", { timeout: TIMEOUT }, () => { + let deployedUrl: string; + let kvId: string; + let d1Id: string; + const helper = new WranglerE2ETestHelper(); + + it.skip("can run dev without resource ids", async () => { + const worker = helper.runLongLived("wrangler dev --x-provision", { + debug: true, + }); + + const { url } = await worker.waitForReady(); + await fetch(url); + + const text = await fetchText(url); + + expect(text).toMatchInlineSnapshot(`"Hello World!"`); + }); + + beforeAll(async () => { + await helper.seed({ + "wrangler.toml": dedent` + name = "${workerName}" + main = "src/index.ts" + compatibility_date = "2023-01-01" + + [[kv_namespaces]] + binding = "KV" + + [[r2_buckets]] + binding = "R2" + + [[d1_databases]] + binding = "D1" + `, + "src/index.ts": dedent` + export default { + fetch(request) { + return new Response("Hello World!") + } + }`, + "package.json": dedent` + { + "name": "${workerName}", + "version": "0.0.0", + "private": true + } + `, + }); + }); + + it("can provision resources and deploy worker", async () => { + const worker = helper.runLongLived( + `wrangler deploy --x-provision --x-auto-create` + ); + await worker.exitCode; + const output = await worker.output; + expect(normalize(output)).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + The following bindings need to be provisioned: + - KV Namespaces: + - KV + - D1 Databases: + - D1 + - R2 Buckets: + - R2 + Provisioning KV (KV Namespace)... + 🌀 Creating new KV Namespace "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-kv"... + ✨ KV provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-kv + -------------------------------------- + Provisioning D1 (D1 Database)... + 🌀 Creating new D1 Database "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-d1"... + ✨ D1 provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-d1 + -------------------------------------- + Provisioning R2 (R2 Bucket)... + 🌀 Creating new R2 Bucket "tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2"... + ✨ R2 provisioned with tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2 + -------------------------------------- + 🎉 All resources provisioned, continuing with deployment... + Your worker has access to the following bindings: + - KV Namespaces: + - KV: 00000000000000000000000000000000 + - D1 Databases: + - D1: 00000000-0000-0000-0000-000000000000 + - R2 Buckets: + - R2: tmp-e2e-worker-00000000-0000-0000-0000-000000000000-r2 + Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) + Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) + https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev + Current Version ID: 00000000-0000-0000-0000-000000000000" + `); + const urlMatch = output.match( + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ + ); + assert(urlMatch?.groups); + deployedUrl = urlMatch.groups.url; + + const kvMatch = output.match(/- KV: (?[0-9a-f]{32})/); + assert(kvMatch?.groups); + kvId = kvMatch.groups.kv; + + const d1Match = output.match(/- D1: (?\w{8}-\w{4}-\w{4}-\w{4}-\w{12})/); + assert(d1Match?.groups); + d1Id = d1Match.groups.d1; + + const { text } = await retry( + (s) => s.status !== 200, + async () => { + const r = await fetch(deployedUrl); + return { text: await r.text(), status: r.status }; + } + ); + expect(text).toMatchInlineSnapshot('"Hello World!"'); + }); + + it("delete worker and resources", async () => { + // we need to add d1 back into the config because otherwise wrangler will + // call the api for all 5000 or so db's the e2e test account has + // :( + await helper.seed({ + "wrangler.toml": dedent` + name = "${workerName}" + main = "src/index.ts" + compatibility_date = "2023-01-01" + + [[d1_databases]] + binding = "D1" + database_name = "${workerName}-d1" + database_id = "${d1Id}" + `, + }); + let output = await helper.run(`wrangler r2 bucket delete ${workerName}-r2`); + expect(output.stdout).toContain(`Deleted bucket`); + output = await helper.run(`wrangler d1 delete ${workerName}-d1 -y`, { + debug: true, + }); + expect(output.stdout).toContain(`Deleted '${workerName}-d1' successfully.`); + output = await helper.run(`wrangler delete`); + expect(output.stdout).toContain("Successfully deleted"); + const status = await retry( + (s) => s === 200 || s === 500, + () => fetch(deployedUrl).then((r) => r.status) + ); + expect(status).toBe(404); + + output = await helper.run( + `wrangler kv namespace delete --namespace-id ${kvId}` + ); + expect(output.stdout).toContain(`Deleted KV namespace`); + }); +}); diff --git a/packages/wrangler/src/d1/list.ts b/packages/wrangler/src/d1/list.ts index 4ede458d3a70..2ab70a35eee9 100644 --- a/packages/wrangler/src/d1/list.ts +++ b/packages/wrangler/src/d1/list.ts @@ -39,7 +39,8 @@ export const Handler = withConfig( ); export const listDatabases = async ( - accountId: string + accountId: string, + limitCalls: boolean = false ): Promise> => { const pageSize = 10; let page = 1; @@ -55,6 +56,9 @@ export const listDatabases = async ( ); page++; results.push(...json); + if (limitCalls) { + break; + } if (json.length < pageSize) { break; } diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index af81f0393327..25f3661d6727 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -107,6 +107,7 @@ type Props = { projectRoot: string | undefined; dispatchNamespace: string | undefined; experimentalVersions: boolean | undefined; + experimentalAutoCreate: boolean; }; export type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute; @@ -787,7 +788,12 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m } else { assert(accountId, "Missing accountId"); - await provisionBindings(bindings, accountId, scriptName); + await provisionBindings( + bindings, + accountId, + scriptName, + props.experimentalAutoCreate + ); await ensureQueuesExistByConfig(config); let bindingsPrinted = false; diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index 22627f2e9166..3c0fd6bd9c79 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -230,6 +230,13 @@ export function deployOptions(yargs: CommonYargsArgv) { "Name of a dispatch namespace to deploy the Worker to (Workers for Platforms)", type: "string", }) + .option("experimental-auto-create", { + describe: "Automatically provision draft bindings with new resources", + type: "boolean", + default: false, + hidden: true, + alias: "x-auto-create", + }) ); } @@ -379,6 +386,7 @@ async function deployWorker(args: DeployArgs) { projectRoot, dispatchNamespace: args.dispatchNamespace, experimentalVersions: args.experimentalVersions, + experimentalAutoCreate: args.experimentalAutoCreate, }); writeOutput({ diff --git a/packages/wrangler/src/deployment-bundle/bindings.ts b/packages/wrangler/src/deployment-bundle/bindings.ts index 1628dd2dee77..ce04ad84efae 100644 --- a/packages/wrangler/src/deployment-bundle/bindings.ts +++ b/packages/wrangler/src/deployment-bundle/bindings.ts @@ -86,7 +86,8 @@ type PendingResources = { export async function provisionBindings( bindings: CfWorkerInit["bindings"], accountId: string, - scriptName: string + scriptName: string, + autoCreate: boolean ): Promise { const pendingResources: PendingResources = { d1_databases: [], @@ -166,34 +167,37 @@ export async function provisionBindings( printBindings(pendingResources, { provisioning: true }); logger.log(); if (pendingResources.kv_namespaces?.length) { - const preExistingKV = await listKVNamespaces(accountId); + const preExistingKV = await listKVNamespaces(accountId, true); await runProvisioningFlow( pendingResources.kv_namespaces, - "KV Namespace", preExistingKV.map((ns) => ({ name: ns.title, id: ns.id })), + "KV Namespace", "title or id", - scriptName + scriptName, + autoCreate ); } if (pendingResources.d1_databases?.length) { - const preExisting = await listDatabases(accountId); + const preExisting = await listDatabases(accountId, true); await runProvisioningFlow( pendingResources.d1_databases, - "D1 Database", preExisting.map((db) => ({ name: db.name, id: db.uuid })), + "D1 Database", "name or id", - scriptName + scriptName, + autoCreate ); } if (pendingResources.r2_buckets?.length) { const preExisting = await listR2Buckets(accountId); await runProvisioningFlow( pendingResources.r2_buckets, - "R2 Bucket", preExisting.map((bucket) => ({ name: bucket.name, id: bucket.name })), + "R2 Bucket", "name", - scriptName + scriptName, + autoCreate ); } logger.log(`🎉 All resources provisioned, continuing with deployment...\n`); @@ -231,10 +235,11 @@ type NormalisedResourceInfo = { type ResourceType = "d1_databases" | "r2_buckets" | "kv_namespaces"; async function runProvisioningFlow( pending: PendingResources[ResourceType], - friendlyBindingName: string, preExisting: NormalisedResourceInfo[], + friendlyBindingName: string, resourceKeyDescriptor: string, - scriptName: string + scriptName: string, + autoCreate: boolean ) { const MAX_OPTIONS = 4; if (pending.length) { @@ -254,25 +259,25 @@ async function runProvisioningFlow( for (const item of pending) { logger.log("Provisioning", item.binding, `(${friendlyBindingName})...`); let name: string = ""; - const selected = - options.length === 0 - ? "new" - : await select( - `Would you like to connect an existing ${friendlyBindingName} or create a new one?`, - { - choices: options.concat([ - { title: "Create new", value: "new" }, - ]), - defaultOption: options.length, - } - ); - if (selected === "new") { - name = await prompt( - `Enter a name for your new ${friendlyBindingName}`, + let selected: string; + if (options.length === 0 || autoCreate) { + selected = "new"; + } else { + selected = await select( + `Would you like to connect an existing ${friendlyBindingName} or create a new one?`, { - defaultValue: `${scriptName}-${item.binding.toLowerCase().replace("_", "-")}`, + choices: options.concat([{ title: "Create new", value: "new" }]), + defaultOption: options.length, } ); + } + if (selected === "new") { + const defaultValue = `${scriptName}-${item.binding.toLowerCase().replace("_", "-")}`; + name = autoCreate + ? defaultValue + : await prompt(`Enter a name for your new ${friendlyBindingName}`, { + defaultValue, + }); logger.log(`🌀 Creating new ${friendlyBindingName} "${name}"...`); // creates new resource and mutates `bindings` to update id await item.create(name); diff --git a/packages/wrangler/src/kv/helpers.ts b/packages/wrangler/src/kv/helpers.ts index e5d8b0ab005d..61d271dd4374 100644 --- a/packages/wrangler/src/kv/helpers.ts +++ b/packages/wrangler/src/kv/helpers.ts @@ -61,7 +61,8 @@ export interface KVNamespaceInfo { * Fetch a list of all the namespaces under the given `accountId`. */ export async function listKVNamespaces( - accountId: string + accountId: string, + limitCalls: boolean = false ): Promise { const pageSize = 100; let page = 1; @@ -79,6 +80,9 @@ export async function listKVNamespaces( ); page++; results.push(...json); + if (limitCalls) { + break; + } if (json.length < pageSize) { break; }