Skip to content

Commit

Permalink
add e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
emily-shen committed Dec 10, 2024
1 parent 5f2a3a2 commit 906b16e
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 30 deletions.
173 changes: 173 additions & 0 deletions packages/wrangler/e2e/provision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import assert from "node:assert";
import dedent from "ts-dedent";
import { fetch } from "undici";
import { afterAll, 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(
/(?<url>https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/
);
assert(urlMatch?.groups);
deployedUrl = urlMatch.groups.url;

const kvMatch = output.match(/- KV: (?<kv>[0-9a-f]{32})/);
assert(kvMatch?.groups);
kvId = kvMatch.groups.kv;

const d1Match = output.match(/- D1: (?<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!"');
});

afterAll(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`);
});
});
6 changes: 5 additions & 1 deletion packages/wrangler/src/d1/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export const Handler = withConfig<HandlerOptions>(
);

export const listDatabases = async (
accountId: string
accountId: string,
limitCalls: boolean = false
): Promise<Array<Database>> => {
const pageSize = 10;
let page = 1;
Expand All @@ -55,6 +56,9 @@ export const listDatabases = async (
);
page++;
results.push(...json);
if (limitCalls) {
break;
}
if (json.length < pageSize) {
break;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/wrangler/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ type Props = {
projectRoot: string | undefined;
dispatchNamespace: string | undefined;
experimentalVersions: boolean | undefined;
experimentalAutoCreate: boolean;
};

export type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
Expand Down Expand Up @@ -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;

Expand Down
8 changes: 8 additions & 0 deletions packages/wrangler/src/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
);
}

Expand Down Expand Up @@ -379,6 +386,7 @@ async function deployWorker(args: DeployArgs) {
projectRoot,
dispatchNamespace: args.dispatchNamespace,
experimentalVersions: args.experimentalVersions,
experimentalAutoCreate: args.experimentalAutoCreate,
});

writeOutput({
Expand Down
59 changes: 32 additions & 27 deletions packages/wrangler/src/deployment-bundle/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ type PendingResources = {
export async function provisionBindings(
bindings: CfWorkerInit["bindings"],
accountId: string,
scriptName: string
scriptName: string,
autoCreate: boolean
): Promise<void> {
const pendingResources: PendingResources = {
d1_databases: [],
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion packages/wrangler/src/kv/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<KVNamespaceInfo[]> {
const pageSize = 100;
let page = 1;
Expand All @@ -79,6 +80,9 @@ export async function listKVNamespaces(
);
page++;
results.push(...json);
if (limitCalls) {
break;
}
if (json.length < pageSize) {
break;
}
Expand Down

0 comments on commit 906b16e

Please sign in to comment.