70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
1051 lines
31 KiB
TypeScript
1051 lines
31 KiB
TypeScript
import ci from "ci-info";
|
|
import dedent from "ts-dedent";
|
|
import { fetch } from "undici";
|
|
import {
|
|
afterAll,
|
|
afterEach,
|
|
assert,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
type ExpectStatic,
|
|
it,
|
|
} from "vitest";
|
|
import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id";
|
|
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
|
|
import { generateResourceName } from "./helpers/generate-resource-name";
|
|
import { normalizeOutput, validateAssetUploadLogs } from "./helpers/normalize";
|
|
import { retry } from "./helpers/retry";
|
|
import { waitForLong } from "./helpers/wait-for";
|
|
|
|
const TIMEOUT = 50_000;
|
|
|
|
describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)(
|
|
"deployments",
|
|
{ timeout: TIMEOUT },
|
|
() => {
|
|
// Note that we are sharing the workerName and helper across all these tests,
|
|
// which means that these tests are not isolated from each other.
|
|
// Seeded files will leak between tests.
|
|
const workerName = generateResourceName();
|
|
const helper = new WranglerE2ETestHelper();
|
|
|
|
afterAll(async () => {
|
|
// clean up user Worker after all tests
|
|
await helper.bestEffortRun(`wrangler delete`);
|
|
});
|
|
|
|
it("deploys a Worker", async ({ expect }) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request) {
|
|
return new Response("Hello World!")
|
|
}
|
|
}`,
|
|
"package.json": dedent`
|
|
{
|
|
"name": "${workerName}",
|
|
"version": "0.0.0",
|
|
"private": true
|
|
}
|
|
`,
|
|
});
|
|
|
|
const output = await helper.run(`wrangler deploy`);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
await waitForLong(
|
|
async () => {
|
|
const response = await fetch(deployedUrl);
|
|
expect(await response.text()).toEqual("Hello World!");
|
|
},
|
|
{ timeout: 30_000 }
|
|
);
|
|
});
|
|
|
|
it("lists 1 deployment", async ({ expect }) => {
|
|
const output = await helper.run(`wrangler deployments list`);
|
|
|
|
expect(normalizeOutput(output.stdout)).toMatchInlineSnapshot(`
|
|
"Created: TIMESTAMP
|
|
Author: person@example.com
|
|
Source: Upload
|
|
Message: Automatic deployment on upload.
|
|
Version(s): (100%) 00000000-0000-0000-0000-000000000000
|
|
Created: TIMESTAMP
|
|
Tag: -
|
|
Message: -"
|
|
`);
|
|
});
|
|
|
|
it("modifies & deploys a Worker", async ({ expect }) => {
|
|
await helper.seed({
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
fetch(request) {
|
|
return new Response("Updated Worker!")
|
|
}
|
|
}`,
|
|
});
|
|
const output = await helper.run(`wrangler deploy`);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
await waitForLong(
|
|
async () => {
|
|
const response = await fetch(deployedUrl);
|
|
expect(await response.text()).toEqual("Updated Worker!");
|
|
},
|
|
{ timeout: 30_000 }
|
|
);
|
|
});
|
|
|
|
it("lists 2 deployments", async ({ expect }) => {
|
|
const dep = await helper.run(`wrangler deployments list`);
|
|
expect(normalizeOutput(dep.stdout)).toMatchInlineSnapshot(`
|
|
"Created: TIMESTAMP
|
|
Author: person@example.com
|
|
Source: Upload
|
|
Message: Automatic deployment on upload.
|
|
Version(s): (100%) 00000000-0000-0000-0000-000000000000
|
|
Created: TIMESTAMP
|
|
Tag: -
|
|
Message: -
|
|
Created: TIMESTAMP
|
|
Author: person@example.com
|
|
Source: Unknown (deployment)
|
|
Message: -
|
|
Version(s): (100%) 00000000-0000-0000-0000-000000000000
|
|
Created: TIMESTAMP
|
|
Tag: -
|
|
Message: -"
|
|
`);
|
|
});
|
|
|
|
it("rolls back", async ({ expect }) => {
|
|
const output = await helper.run(
|
|
`wrangler rollback --message "A test message"`
|
|
);
|
|
expect(normalizeOutput(output.stdout)).toMatchInlineSnapshot(`
|
|
"├ Fetching latest deployment
|
|
│
|
|
├ Your current deployment has 1 version(s):
|
|
│
|
|
│ (100%) 00000000-0000-0000-0000-000000000000
|
|
│ Created: TIMESTAMP
|
|
│ Tag: -
|
|
│ Message: -
|
|
│
|
|
├ Finding latest stable Worker Version to rollback to
|
|
│
|
|
│
|
|
? Please provide an optional message for this rollback (120 characters max)
|
|
🤖 Using default value in non-interactive context: A test message
|
|
│
|
|
├ WARNING You are about to rollback to Worker Version 00000000-0000-0000-0000-000000000000.
|
|
│ This will immediately replace the current deployment and become the active deployment across all your deployed triggers.
|
|
│ However, your local development environment will not be affected by this rollback.
|
|
│ Rolling back to a previous deployment will not rollback any of the bound resources (Durable Object, D1, R2, KV, etc).
|
|
│
|
|
│ (100%) 00000000-0000-0000-0000-000000000000
|
|
│ Created: TIMESTAMP
|
|
│ Tag: -
|
|
│ Message: -
|
|
│
|
|
? Are you sure you want to deploy this Worker Version to 100% of traffic?
|
|
🤖 Using fallback value in non-interactive context: yes
|
|
Performing rollback...
|
|
│
|
|
╰ SUCCESS Worker Version 00000000-0000-0000-0000-000000000000 has been deployed to 100% of traffic.
|
|
Current Version ID: 00000000-0000-0000-0000-000000000000"
|
|
`);
|
|
});
|
|
|
|
it("lists deployments", async ({ expect }) => {
|
|
const dep = await helper.run(`wrangler deployments list`);
|
|
expect(normalizeOutput(dep.stdout)).toMatchInlineSnapshot(`
|
|
"Created: TIMESTAMP
|
|
Author: person@example.com
|
|
Source: Upload
|
|
Message: Automatic deployment on upload.
|
|
Version(s): (100%) 00000000-0000-0000-0000-000000000000
|
|
Created: TIMESTAMP
|
|
Tag: -
|
|
Message: -
|
|
Created: TIMESTAMP
|
|
Author: person@example.com
|
|
Source: Unknown (deployment)
|
|
Message: -
|
|
Version(s): (100%) 00000000-0000-0000-0000-000000000000
|
|
Created: TIMESTAMP
|
|
Tag: -
|
|
Message: -
|
|
Created: TIMESTAMP
|
|
Author: person@example.com
|
|
Source: Unknown (deployment)
|
|
Message: A test message
|
|
Version(s): (100%) 00000000-0000-0000-0000-000000000000
|
|
Created: TIMESTAMP
|
|
Tag: -
|
|
Message: -"
|
|
`);
|
|
});
|
|
}
|
|
);
|
|
|
|
type AssetTestCase = {
|
|
path: string;
|
|
content?: string;
|
|
redirect?: string;
|
|
};
|
|
|
|
function generateInitialAssets(workerName: string) {
|
|
return {
|
|
"public/index.html": dedent`
|
|
<h1>index.html</h1>`,
|
|
"public/[boop].html": dedent`
|
|
<h1>[boop].html</h1>`,
|
|
"public/404.html": dedent`
|
|
<h1>404.html</h1>`,
|
|
"package.json": dedent`
|
|
{
|
|
"name": "${workerName}",
|
|
"version": "0.0.0",
|
|
"private": true
|
|
}`,
|
|
};
|
|
}
|
|
|
|
async function checkAssets(
|
|
expect: ExpectStatic,
|
|
testCases: AssetTestCase[],
|
|
deployedUrl: string
|
|
) {
|
|
for (const testCase of testCases) {
|
|
await waitForLong(
|
|
async () => {
|
|
const r = await fetch(new URL(testCase.path, deployedUrl));
|
|
const text = await r.text();
|
|
const url = r.url;
|
|
|
|
if (testCase.content) {
|
|
expect(
|
|
text,
|
|
`expected content for ${testCase.path} to be ${testCase.content}`
|
|
).toContain(testCase.content);
|
|
}
|
|
if (testCase.redirect) {
|
|
expect(
|
|
new URL(url).pathname,
|
|
`expected redirect for ${testCase.path} to be ${testCase.redirect}`
|
|
).toEqual(new URL(testCase.redirect, deployedUrl).pathname);
|
|
} else {
|
|
expect(
|
|
new URL(url).pathname,
|
|
`unexpected pathname for ${testCase.path}`
|
|
).toEqual(new URL(testCase.path, deployedUrl).pathname);
|
|
}
|
|
},
|
|
{ timeout: 40_000 }
|
|
);
|
|
}
|
|
}
|
|
|
|
describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => {
|
|
let helper: WranglerE2ETestHelper;
|
|
let workerName: string;
|
|
|
|
beforeEach(() => {
|
|
// We are recreating the helper on each test to ensure they are isolated from each other.
|
|
helper = new WranglerE2ETestHelper();
|
|
// Use a new user Worker in each test
|
|
workerName = generateResourceName();
|
|
});
|
|
|
|
describe("Workers", () => {
|
|
afterEach(async () => {
|
|
// clean up user Worker after each test
|
|
await helper.bestEffortRun(`wrangler delete`);
|
|
});
|
|
|
|
it("deploys a Workers + Assets project with assets only", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// deploy user Worker && verify output
|
|
const output = await helper.run(`wrangler deploy`);
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/index.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
// Tests html_handling = "auto_trailing_slash" (default):
|
|
{
|
|
path: "/",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
{
|
|
path: "/index.html",
|
|
content: "<h1>index.html</h1>",
|
|
redirect: "/",
|
|
},
|
|
{
|
|
path: "/[boop]",
|
|
content: "<h1>[boop].html</h1>",
|
|
redirect: "/%5Bboop%5D",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
|
|
// Test 404 handling:
|
|
// even though 404.html has been uploaded, because not_found_handling is set to "none"
|
|
// we expect to get an empty response
|
|
const { text } = await retry(
|
|
(s) => s.status !== 404,
|
|
async () => {
|
|
const r = await fetch(new URL("/try-404", deployedUrl));
|
|
const temp = { text: await r.text(), status: r.status };
|
|
return temp;
|
|
}
|
|
);
|
|
expect(text).toBeFalsy();
|
|
});
|
|
|
|
it("deploys a Worker with static assets and user Worker", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
binding = "ASSETS"
|
|
html_handling = "none"
|
|
not_found_handling = "404-page"
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
async fetch(request, env) {
|
|
const url = new URL(request.url);
|
|
if (url.pathname === "/binding") {
|
|
return await env.ASSETS.fetch(new URL("index.html", request.url));
|
|
} else if (url.pathname === "/try-404") {
|
|
return await env.ASSETS.fetch(request.url);
|
|
}
|
|
return new Response("Hello World!")
|
|
}
|
|
}`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// deploy user Worker && verify output
|
|
const output = await helper.run(`wrangler deploy`);
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/index.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
// because html handling has now been set to "none", only exact matches will be served
|
|
{
|
|
path: "/index.html",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
// 404s should fall through to the user worker, and "/" is not an exact match
|
|
// so we should expect the UW response
|
|
{ path: "/", content: "Hello World!" },
|
|
{
|
|
path: "/binding",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
{
|
|
path: "/worker",
|
|
content: "Hello World!",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
|
|
// unlike before, not_found_handling has been set to "404-page" instead of the default "none"
|
|
// note that with a user worker, the request must be passed back to the asset worker via the ASSET binding
|
|
// in order to return the 404 page
|
|
const { text } = await retry(
|
|
// Retry while the status isn't 404 OR the content hasn't propagated yet.
|
|
// Before assets are live the platform may return its own 404 page (status 200)
|
|
// instead of the user's 404.html asset.
|
|
(s) => s.status !== 404 || !s.text.includes("<h1>404.html</h1>"),
|
|
async () => {
|
|
const r = await fetch(new URL("/try-404", deployedUrl));
|
|
const temp = { text: await r.text(), status: r.status };
|
|
return temp;
|
|
}
|
|
);
|
|
expect(text).toContain("<h1>404.html</h1>");
|
|
});
|
|
|
|
it("deploys a Workers + Assets project with helpful debug logs", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// deploy user Worker && verify output
|
|
const output = await helper.run(`wrangler deploy`, {
|
|
debug: true,
|
|
});
|
|
|
|
validateAssetUploadLogs(
|
|
expect,
|
|
output,
|
|
["/404.html", "/index.html", "/[boop].html"],
|
|
{ includeDebug: true }
|
|
);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
// Tests html_handling = "auto_trailing_slash" (default):
|
|
{
|
|
path: "/",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
{
|
|
path: "/index.html",
|
|
content: "<h1>index.html</h1>",
|
|
redirect: "/",
|
|
},
|
|
{
|
|
path: "/[boop]",
|
|
content: "<h1>[boop].html</h1>",
|
|
redirect: "/%5Bboop%5D",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
|
|
// Test 404 handling:
|
|
// even though 404.html has been uploaded, because not_found_handling is set to "none"
|
|
// we expect to get an empty response
|
|
const { text } = await retry(
|
|
(s) => s.status !== 404,
|
|
async () => {
|
|
const r = await fetch(new URL("/try-404", deployedUrl));
|
|
const temp = { text: await r.text(), status: r.status };
|
|
return temp;
|
|
}
|
|
);
|
|
expect(text).toBeFalsy();
|
|
});
|
|
|
|
it("runs the user Worker ahead of matching assets when run_worker_first = true", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
binding = "ASSETS"
|
|
html_handling = "none"
|
|
not_found_handling = "404-page"
|
|
run_worker_first = true
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
async fetch(request, env) {
|
|
return new Response("Hello World from User Worker!")
|
|
}
|
|
}`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// deploy user Worker && verify output
|
|
const output = await helper.run(`wrangler deploy`);
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/index.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
{
|
|
path: "/index.html",
|
|
content: "Hello World from User Worker!",
|
|
},
|
|
{
|
|
path: "/",
|
|
content: "Hello World from User Worker!",
|
|
},
|
|
{
|
|
path: "/worker",
|
|
content: "Hello World from User Worker!",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
});
|
|
|
|
it("runs the user Worker ahead of matching assets for matching run_worker_first routes", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
binding = "ASSETS"
|
|
html_handling = "none"
|
|
not_found_handling = "404-page"
|
|
run_worker_first = ["/api/*", "!/api/assets/*"]
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
async fetch(request, env) {
|
|
return new Response("Hello World from User Worker!")
|
|
}
|
|
}`,
|
|
...generateInitialAssets(workerName),
|
|
"public/api/index.html": "<h1>api/index.html</h1>",
|
|
"public/api/assets/test.html": "<h1>api/assets/test.html</h1>",
|
|
});
|
|
|
|
// deploy user Worker && verify output
|
|
const output = await helper.run(`wrangler deploy`);
|
|
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/api/index.html",
|
|
"/index.html",
|
|
"/api/assets/test.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
const deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
{ path: "/index.html", content: "<h1>index.html</h1>" },
|
|
{ path: "/missing.html", content: "<h1>404.html</h1>" },
|
|
{ path: "/api/", content: "Hello World from User Worker!" },
|
|
{ path: "/api/foo.html", content: "Hello World from User Worker!" },
|
|
{ path: "/api/assets/missing", content: "404.html" },
|
|
{ path: "/api/assets/test.html", content: "api/assets/test.html" },
|
|
];
|
|
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
});
|
|
});
|
|
|
|
describe("Workers for Platforms", () => {
|
|
let dispatchNamespaceName: string;
|
|
let dispatchWorkerName: string;
|
|
|
|
beforeEach(async () => {
|
|
// set up a new dispatch Worker in each test
|
|
dispatchNamespaceName = generateResourceName("dispatch");
|
|
dispatchWorkerName = generateResourceName();
|
|
|
|
await helper.seed({
|
|
"dispatch-worker/wrangler.toml": dedent`
|
|
name = "${dispatchWorkerName}"
|
|
main = "./src/index.js"
|
|
compatibility_date = "2023-01-01"
|
|
|
|
[[dispatch_namespaces]]
|
|
binding = "DISPATCH"
|
|
namespace = "${dispatchNamespaceName}"
|
|
`,
|
|
"dispatch-worker/src/index.js": dedent`
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const stub = env.DISPATCH.get("${workerName}");
|
|
return stub.fetch(request);
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// clean up dispatch Worker
|
|
await helper.bestEffortRun(
|
|
`wrangler delete -c dispatch-worker/wrangler.toml`
|
|
);
|
|
await helper.bestEffortRun(
|
|
`wrangler dispatch-namespace delete ${dispatchNamespaceName}`
|
|
);
|
|
});
|
|
|
|
it("deploys a Workers + Assets project with assets only", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// create a dispatch namespace && verify output
|
|
let output = await helper.run(
|
|
`wrangler dispatch-namespace create ${dispatchNamespaceName}`
|
|
);
|
|
let normalizedStdout = normalizeOutput(output.stdout);
|
|
expect(normalizedStdout).toContain(
|
|
`Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`
|
|
);
|
|
|
|
// upload user Worker to the dispatch namespace && verify output
|
|
output = await helper.run(
|
|
`wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`
|
|
);
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/index.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
// deploy dispatch Worker && verify output
|
|
output = await helper.run(
|
|
`wrangler deploy -c dispatch-worker/wrangler.toml`
|
|
);
|
|
normalizedStdout = normalizeOutput(output.stdout);
|
|
expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB
|
|
Your Worker has access to the following bindings:
|
|
Binding Resource
|
|
env.DISPATCH (tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000) Dispatch Namespace
|
|
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 deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
// Tests html_handling = "auto_trailing_slash" (default):
|
|
{
|
|
path: "/",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
{
|
|
path: "/index.html",
|
|
content: "<h1>index.html</h1>",
|
|
redirect: "/",
|
|
},
|
|
{
|
|
path: "/[boop]",
|
|
content: "<h1>[boop].html</h1>",
|
|
redirect: "/%5Bboop%5D",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
|
|
// Test 404 handling:
|
|
// even though 404.html has been uploaded, because not_found_handling is set to "none"
|
|
// we expect to get an empty response
|
|
const { text } = await retry(
|
|
(s) => s.status !== 404,
|
|
async () => {
|
|
const r = await fetch(new URL("/try-404", deployedUrl));
|
|
const temp = { text: await r.text(), status: r.status };
|
|
return temp;
|
|
}
|
|
);
|
|
expect(text).toBeFalsy();
|
|
});
|
|
|
|
it("deploys a Worker with static assets and user Worker", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
binding = "ASSETS"
|
|
html_handling = "none"
|
|
not_found_handling = "404-page"
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
async fetch(request, env) {
|
|
const url = new URL(request.url);
|
|
if (url.pathname === "/binding") {
|
|
return await env.ASSETS.fetch(new URL("index.html", request.url));
|
|
} else if (url.pathname === "/try-404") {
|
|
return await env.ASSETS.fetch(request.url);
|
|
}
|
|
return new Response("Hello World!")
|
|
}
|
|
}`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// create a dispatch namespace && verify output
|
|
let output = await helper.run(
|
|
`wrangler dispatch-namespace create ${dispatchNamespaceName}`
|
|
);
|
|
let normalizedStdout = normalizeOutput(output.stdout);
|
|
expect(normalizedStdout).toContain(
|
|
`Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`
|
|
);
|
|
|
|
// upload user Worker to the dispatch namespace && verify output
|
|
output = await helper.run(
|
|
`wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`
|
|
);
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/index.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
// deploy dispatch Worker && verify output
|
|
output = await helper.run(
|
|
`wrangler deploy -c dispatch-worker/wrangler.toml`
|
|
);
|
|
normalizedStdout = normalizeOutput(output.stdout);
|
|
expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB
|
|
Your Worker has access to the following bindings:
|
|
Binding Resource
|
|
env.DISPATCH (tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000) Dispatch Namespace
|
|
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 deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
// because html handling has now been set to "none", only exact matches will be served
|
|
{
|
|
path: "/index.html",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
// 404s should fall through to the user worker, and "/" is not an exact match
|
|
// so we should expect the UW response
|
|
{ path: "/", content: "Hello World!" },
|
|
{
|
|
path: "/binding",
|
|
content: "<h1>index.html</h1>",
|
|
},
|
|
{
|
|
path: "/worker",
|
|
content: "Hello World!",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
|
|
// unlike before, not_found_handling has been set to "404-page"
|
|
// instead of the default "none"
|
|
// note that with a user Worker, the request must be passed back to
|
|
// the asset worker via the ASSET binding in order to return the 404
|
|
// page
|
|
const { text } = await retry(
|
|
// Retry while the status isn't 404 OR the content hasn't propagated yet.
|
|
// Before assets are live the platform may return its own 404 page (status 200)
|
|
// instead of the user's 404.html asset.
|
|
(s) => s.status !== 404 || !s.text.includes("<h1>404.html</h1>"),
|
|
async () => {
|
|
const r = await fetch(new URL("/try-404", deployedUrl));
|
|
const temp = { text: await r.text(), status: r.status };
|
|
return temp;
|
|
}
|
|
);
|
|
expect(text).toContain("<h1>404.html</h1>");
|
|
});
|
|
|
|
it("runs the user Worker ahead of matching assets when run_worker_first = true", async ({
|
|
expect,
|
|
}) => {
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
[assets]
|
|
directory = "public"
|
|
binding = "ASSETS"
|
|
html_handling = "none"
|
|
not_found_handling = "404-page"
|
|
run_worker_first = true
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
async fetch(request, env) {
|
|
return new Response("Hello World from User Worker!")
|
|
}
|
|
}`,
|
|
...generateInitialAssets(workerName),
|
|
});
|
|
|
|
// create a dispatch namespace && verify output
|
|
let output = await helper.run(
|
|
`wrangler dispatch-namespace create ${dispatchNamespaceName}`
|
|
);
|
|
let normalizedStdout = normalizeOutput(output.stdout);
|
|
expect(normalizedStdout).toContain(
|
|
`Created dispatch namespace "tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000" with ID "00000000-0000-0000-0000-000000000000"`
|
|
);
|
|
|
|
// upload user Worker to the dispatch namespace && verify output
|
|
output = await helper.run(
|
|
`wrangler deploy --dispatch-namespace ${dispatchNamespaceName}`
|
|
);
|
|
validateAssetUploadLogs(expect, output, [
|
|
"/404.html",
|
|
"/index.html",
|
|
"/[boop].html",
|
|
]);
|
|
|
|
// deploy dispatch Worker && verify output
|
|
output = await helper.run(
|
|
`wrangler deploy -c dispatch-worker/wrangler.toml`
|
|
);
|
|
normalizedStdout = normalizeOutput(output.stdout);
|
|
expect(normalizedStdout).toEqual(`Total Upload: xx KiB / gzip: xx KiB
|
|
Your Worker has access to the following bindings:
|
|
Binding Resource
|
|
env.DISPATCH (tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000) Dispatch Namespace
|
|
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 deployedUrl = getDeployedUrl(output);
|
|
|
|
const testCases: AssetTestCase[] = [
|
|
{
|
|
path: "/index.html",
|
|
content: "Hello World from User Worker!",
|
|
},
|
|
{
|
|
path: "/",
|
|
content: "Hello World from User Worker!",
|
|
},
|
|
{
|
|
path: "/worker",
|
|
content: "Hello World from User Worker!",
|
|
},
|
|
];
|
|
await checkAssets(expect, testCases, deployedUrl);
|
|
});
|
|
});
|
|
});
|
|
|
|
const skipContainersTest =
|
|
!CLOUDFLARE_ACCOUNT_ID || (ci.isCI && process.platform !== "linux");
|
|
describe.skipIf(skipContainersTest)("containers", () => {
|
|
let helper: WranglerE2ETestHelper;
|
|
let workerName: string;
|
|
let applicationId: string | undefined;
|
|
let deployedUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
helper = new WranglerE2ETestHelper();
|
|
workerName = generateResourceName();
|
|
await helper.seed({
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2023-01-01"
|
|
|
|
[durable_objects]
|
|
bindings = [
|
|
{ name = "MY_DO", class_name = "MyDurableObject" }
|
|
]
|
|
|
|
[[containers]]
|
|
name = "e2e-test-${workerName}"
|
|
class_name = "MyDurableObject"
|
|
image = "./Dockerfile"
|
|
max_instances = 1
|
|
unsafe = { configuration = { experimental_flags = ["experimental_enable_snapshots"] } }
|
|
|
|
[[migrations]]
|
|
tag = "v1"
|
|
new_sqlite_classes = ["MyDurableObject"]
|
|
|
|
[observability]
|
|
enabled = true
|
|
`,
|
|
"container/index.js": dedent`
|
|
const { createServer } = require("http");
|
|
|
|
const server = createServer((req, res) => {
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end('hello from container');
|
|
});
|
|
|
|
server.listen(80, () => {
|
|
console.log('Server running on port 80');
|
|
});
|
|
`,
|
|
Dockerfile: dedent`
|
|
FROM node:18-slim
|
|
WORKDIR /app
|
|
COPY container/index.js .
|
|
CMD ["node", "index.js"]
|
|
EXPOSE 80
|
|
`,
|
|
"src/index.ts": dedent`
|
|
export default {
|
|
async fetch(req, env) {
|
|
const url = new URL(req.url)
|
|
if (url.pathname === "/do") {
|
|
const id = env.MY_DO.idFromName(url.pathname);
|
|
const stub = env.MY_DO.get(id);
|
|
try {
|
|
return await stub.fetch(req);
|
|
} catch (err) {
|
|
return new Response("Error fetching from stub: " + err.message, { status: 400 });
|
|
}
|
|
}
|
|
|
|
return new Response("not found", { status: 404 });
|
|
},
|
|
};
|
|
|
|
export class MyDurableObject implements DurableObject {
|
|
constructor(ctx) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
async fetch(_request: Request) {
|
|
if (!this.ctx.container) {
|
|
return new Response('this.ctx.container not defined', { status: 500 });
|
|
}
|
|
|
|
if (!this.ctx.container.running) {
|
|
this.ctx.container.start();
|
|
this.monitor = this.ctx.container.monitor();
|
|
}
|
|
|
|
return this.ctx.container.getTcpPort(80).fetch(new Request("http://foo"));
|
|
}
|
|
}`,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// clean up user Worker after each test
|
|
const deleteWorker = helper.bestEffortRun(`wrangler delete`);
|
|
const deleteContainer = helper.bestEffortRun(
|
|
`wrangler containers delete ${applicationId}`
|
|
);
|
|
await Promise.allSettled([deleteWorker, deleteContainer]);
|
|
}, 30_000);
|
|
|
|
it(
|
|
"can deploy a snapshot-enabled Dockerfile container and skip unchanged rebuilds",
|
|
{ timeout: 60 * 2 * 1000 },
|
|
async ({ expect }) => {
|
|
const outputOne = await helper.run(`wrangler deploy`);
|
|
|
|
deployedUrl = getDeployedUrl(outputOne);
|
|
|
|
const matchApplicationId = outputOne.stdout.match(
|
|
/([(]Application ID: (?<applicationId>.+?)[)])/
|
|
);
|
|
applicationId = matchApplicationId?.groups?.applicationId;
|
|
assert(matchApplicationId?.groups);
|
|
assert(applicationId);
|
|
assert(CLOUDFLARE_ACCOUNT_ID);
|
|
|
|
const containerInfo = await helper.run(
|
|
`wrangler containers info ${applicationId}`
|
|
);
|
|
const application = JSON.parse(containerInfo.stdout) as {
|
|
configuration?: {
|
|
experimental_flags?: unknown;
|
|
image?: unknown;
|
|
};
|
|
};
|
|
expect(application.configuration?.experimental_flags).toContain(
|
|
"experimental_enable_snapshots"
|
|
);
|
|
const image = application.configuration?.image;
|
|
assert(typeof image === "string");
|
|
const expectedImagePrefix = `registry.cloudflare.com/${CLOUDFLARE_ACCOUNT_ID}/e2e-test-${workerName}@sha256:`;
|
|
expect(image.startsWith(expectedImagePrefix)).toBe(true);
|
|
expect(image).toMatch(/@sha256:[a-f0-9]{64}$/);
|
|
|
|
const outputTwo = await helper.run(`wrangler deploy`);
|
|
expect(outputTwo.stdout).toContain(`No changes to be made`);
|
|
}
|
|
);
|
|
|
|
it(
|
|
"can fetch DO container",
|
|
{ timeout: 60 * 2 * 1000 },
|
|
async ({ expect }) => {
|
|
await waitForLong(
|
|
async () => {
|
|
const response = await fetch(`${deployedUrl}/do`, {
|
|
signal: AbortSignal.timeout(5_000),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
"Durable object transient error: " + (await response.text())
|
|
);
|
|
}
|
|
|
|
expect(await response.text()).toEqual("hello from container");
|
|
},
|
|
// big timeout for containers (2m)
|
|
{ timeout: 60 * 2 * 1000, interval: 1000 }
|
|
);
|
|
}
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Extracts the deployed URL from the output of a Wrangler command.
|
|
*/
|
|
function getDeployedUrl(output: { stdout: string }) {
|
|
const match = output.stdout.match(
|
|
/(?<url>https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/
|
|
);
|
|
assert(match?.groups);
|
|
return match.groups.url;
|
|
}
|