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
653 lines
17 KiB
TypeScript
653 lines
17 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import dedent from "ts-dedent";
|
|
import { fetch } from "undici";
|
|
import { beforeEach, describe, it } from "vitest";
|
|
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
|
|
import { fetchJson } from "./helpers/fetch-json";
|
|
import { fetchText } from "./helpers/fetch-text";
|
|
import { generateResourceName } from "./helpers/generate-resource-name";
|
|
import { seed as baseSeed, makeRoot } from "./helpers/setup";
|
|
import { waitFor, waitForLong } from "./helpers/wait-for";
|
|
|
|
describe("multiworker", () => {
|
|
let workerName: string;
|
|
let workerName2: string;
|
|
let workerName3: string;
|
|
let a: string;
|
|
let b: string;
|
|
let c: string;
|
|
let helper: WranglerE2ETestHelper;
|
|
beforeEach(async () => {
|
|
workerName = generateResourceName("worker");
|
|
workerName2 = generateResourceName("worker");
|
|
workerName3 = generateResourceName("worker");
|
|
helper = new WranglerE2ETestHelper();
|
|
a = await makeRoot();
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
import { DurableObject } from "cloudflare:workers";
|
|
|
|
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);
|
|
return stub.fetch(req);
|
|
}
|
|
if (url.pathname === "/service") {
|
|
return env.CEE.fetch(req);
|
|
}
|
|
if (url.pathname === "/count") {
|
|
const counter = await env.COUNTER.newCounter()
|
|
await counter.increment(1)
|
|
await counter.increment(2)
|
|
await counter.increment(3)
|
|
return new Response(String(await counter.value))
|
|
}
|
|
if (url.pathname === "/props") {
|
|
const props = await env.COUNTER.getProps()
|
|
return new Response(JSON.stringify(props))
|
|
}
|
|
return env.BEE.fetch(req);
|
|
},
|
|
};
|
|
|
|
export class MyDurableObject extends DurableObject {
|
|
async fetch(request: Request) {
|
|
if (request.headers.has("X-Reset-Count")) {
|
|
await this.ctx.storage.put("count", 0);
|
|
}
|
|
let count: number = (await this.ctx.storage.get("count")) || 0;
|
|
await this.ctx.storage.put("count", ++count);
|
|
return Response.json({ count });
|
|
}
|
|
|
|
sayHello(name: string) {
|
|
return "Hello " + name
|
|
}
|
|
}
|
|
`,
|
|
"package.json": dedent`
|
|
{
|
|
"name": "a",
|
|
"version": "0.0.0",
|
|
"private": true
|
|
}
|
|
`,
|
|
});
|
|
|
|
b = await makeRoot();
|
|
await baseSeed(b, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName2}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
|
|
[durable_objects]
|
|
bindings = [
|
|
{ name = "REFERENCED_DO", class_name = "MyDurableObject", script_name = "${workerName}" }
|
|
]
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers";
|
|
|
|
class Counter extends RpcTarget {
|
|
#value = 0;
|
|
|
|
increment(amount) {
|
|
this.#value += amount;
|
|
return this.#value;
|
|
}
|
|
|
|
get value() {
|
|
return this.#value;
|
|
}
|
|
}
|
|
|
|
export class CounterService extends WorkerEntrypoint {
|
|
async newCounter() {
|
|
return new Counter();
|
|
}
|
|
async getProps() {
|
|
return this.ctx.props;
|
|
}
|
|
}
|
|
export default{
|
|
async fetch(req, env) {
|
|
const url = new URL(req.url)
|
|
if (url.pathname === "/do") {
|
|
const id = env.REFERENCED_DO.idFromName(url.pathname);
|
|
const stub = env.REFERENCED_DO.get(id);
|
|
return stub.fetch(req);
|
|
}
|
|
if (url.pathname === "/do-rpc") {
|
|
const id = env.REFERENCED_DO.idFromName(url.pathname);
|
|
const stub = env.REFERENCED_DO.get(id);
|
|
return new Response(await stub.sayHello("through DO RPC"));
|
|
}
|
|
return new Response("hello world");
|
|
},
|
|
};
|
|
`,
|
|
"package.json": dedent`
|
|
{
|
|
"name": "b",
|
|
"version": "0.0.0",
|
|
"private": true
|
|
}
|
|
`,
|
|
});
|
|
|
|
c = await makeRoot();
|
|
await baseSeed(c, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName3}"
|
|
main = "src/index.ts"
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
addEventListener("fetch", (event) => {
|
|
event.respondWith(new Response("Hello from service worker"));
|
|
});
|
|
`,
|
|
"package.json": dedent`
|
|
{
|
|
"name": "c",
|
|
"version": "0.0.0",
|
|
"private": true
|
|
}
|
|
`,
|
|
});
|
|
});
|
|
|
|
describe("module workers", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[[services]]
|
|
binding = "BEE"
|
|
service = '${workerName2}'
|
|
|
|
[[services]]
|
|
binding = "COUNTER"
|
|
service = '${workerName2}'
|
|
entrypoint = 'CounterService'
|
|
props = { foo = 123, bar = { baz = "hello from props" } }
|
|
`,
|
|
});
|
|
});
|
|
it("can fetch b", async ({ expect }) => {
|
|
const worker = helper.runLongLived(`wrangler dev`, { cwd: b });
|
|
|
|
const { url } = await worker.waitForReady(5_000);
|
|
|
|
await expect(fetch(url).then((r) => r.text())).resolves.toBe(
|
|
"hello world"
|
|
);
|
|
});
|
|
|
|
it("can fetch b through a", async ({ expect }) => {
|
|
const workerA = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await workerA.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () => await expect(fetchText(url)).resolves.toBe("hello world")
|
|
);
|
|
});
|
|
|
|
it("can fetch named entrypoint on b through a and do RPC", async ({
|
|
expect,
|
|
}) => {
|
|
const workerA = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await workerA.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () => await expect(fetchText(`${url}/count`)).resolves.toBe("6")
|
|
);
|
|
});
|
|
|
|
it("can access service props through a binding", async ({ expect }) => {
|
|
const workerA = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await workerA.waitForReady(5_000);
|
|
|
|
await waitForLong(async () => {
|
|
const response = await fetch(`${url}/props`);
|
|
const props = await response.json();
|
|
expect(props).toEqual({
|
|
foo: 123,
|
|
bar: { baz: "hello from props" },
|
|
});
|
|
});
|
|
});
|
|
|
|
it("shows runtime error when fetching non-existent service", async ({
|
|
expect,
|
|
}) => {
|
|
const service = randomUUID();
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[[services]]
|
|
binding = "BEE"
|
|
service = '${service}'
|
|
`,
|
|
});
|
|
|
|
const workerA = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
|
|
const { url } = await workerA.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () =>
|
|
await expect(fetchText(url)).resolves.toBe(
|
|
`Worker "${service}" not found. Make sure it is running locally.`
|
|
)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("service workers", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[[services]]
|
|
binding = "CEE"
|
|
service = '${workerName3}'
|
|
`,
|
|
});
|
|
});
|
|
|
|
it("can fetch service worker c through a", async ({ expect }) => {
|
|
const workerA = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${c}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await workerA.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () =>
|
|
await expect(fetchText(`${url}/service`)).resolves.toBe(
|
|
"Hello from service worker"
|
|
)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("durable objects", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[[services]]
|
|
binding = "BEE"
|
|
service = '${workerName2}'
|
|
|
|
[durable_objects]
|
|
bindings = [
|
|
{ name = "MY_DO", class_name = "MyDurableObject" }
|
|
]
|
|
|
|
[[migrations]]
|
|
tag = "v1"
|
|
new_classes = ["MyDurableObject"]
|
|
`,
|
|
});
|
|
});
|
|
it("can fetch DO through a", async ({ expect }) => {
|
|
const worker = helper.runLongLived(`wrangler dev`, { cwd: a });
|
|
|
|
const { url } = await worker.waitForReady(5_000);
|
|
|
|
await expect(
|
|
fetchJson(`${url}/do`, {
|
|
headers: {
|
|
"X-Reset-Count": "true",
|
|
},
|
|
})
|
|
).resolves.toMatchObject({ count: 1 });
|
|
});
|
|
|
|
it("can fetch remote DO attached to a through b", async ({ expect }) => {
|
|
const workerB = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${a}/wrangler.toml`,
|
|
{ cwd: b }
|
|
);
|
|
const { url } = await workerB.waitForReady(5_000);
|
|
|
|
await expect(
|
|
fetchJson(`${url}/do`, {
|
|
headers: {
|
|
"X-Reset-Count": "true",
|
|
},
|
|
})
|
|
).resolves.toMatchObject({ count: 1 });
|
|
});
|
|
|
|
it("can fetch remote DO attached to a through b with RPC", async ({
|
|
expect,
|
|
}) => {
|
|
const workerB = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${a}/wrangler.toml`,
|
|
{ cwd: b }
|
|
);
|
|
const { url } = await workerB.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () =>
|
|
await expect(fetchText(`${url}/do-rpc`)).resolves.toBe(
|
|
"Hello through DO RPC"
|
|
)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Tail consumers", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2025-04-28"
|
|
|
|
[[tail_consumers]]
|
|
service = "${workerName2}"
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
export default {
|
|
async fetch(req, env) {
|
|
console.log("log something")
|
|
return new Response("hello from a")
|
|
},
|
|
};
|
|
`,
|
|
});
|
|
|
|
b = await makeRoot();
|
|
await baseSeed(b, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName2}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2025-04-28"
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
export default {
|
|
async tail(event) {
|
|
console.log("received tail event", event)
|
|
},
|
|
};
|
|
`,
|
|
});
|
|
});
|
|
|
|
it("can fetch a without b running", async ({ expect }) => {
|
|
const worker = helper.runLongLived(`wrangler dev`, { cwd: a });
|
|
|
|
const { url } = await worker.waitForReady(5_000);
|
|
|
|
await expect(fetchText(`${url}`)).resolves.toBe("hello from a");
|
|
});
|
|
|
|
it("tail event sent to b", async ({ expect }) => {
|
|
const worker = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await worker.waitForReady(5_000);
|
|
|
|
await expect(fetchText(`${url}`)).resolves.toBe("hello from a");
|
|
|
|
await waitFor(() =>
|
|
expect(worker.currentOutput).includes("received tail event")
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Streaming tail consumers", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2025-04-28"
|
|
compatibility_flags = ["streaming_tail_worker"]
|
|
|
|
[[tail_consumers]]
|
|
service = "${workerName2}"
|
|
|
|
[[streaming_tail_consumers]]
|
|
service = "${workerName2}"
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
export default {
|
|
async fetch(req, env) {
|
|
console.log("log something")
|
|
return new Response("hello from a")
|
|
},
|
|
};
|
|
`,
|
|
});
|
|
|
|
b = await makeRoot();
|
|
await baseSeed(b, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName2}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2025-04-28"
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
export default {
|
|
async tail(event) {
|
|
console.log("received tail event", event)
|
|
},
|
|
async tailStream(event) {
|
|
console.log("received tail stream event", event)
|
|
},
|
|
};
|
|
`,
|
|
});
|
|
});
|
|
|
|
it("logs tail event sent to b", async ({ expect }) => {
|
|
const worker = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await worker.waitForReady(5_000);
|
|
|
|
await waitForLong(() =>
|
|
expect(fetchText(`${url}`)).resolves.toBe("hello from a")
|
|
);
|
|
await waitFor(() =>
|
|
expect(worker.currentOutput).includes("received tail stream event")
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("pages", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
pages_build_output_dir = "./public"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[[services]]
|
|
binding = "CEE"
|
|
service = '${workerName3}'
|
|
|
|
[[services]]
|
|
binding = "BEE"
|
|
service = '${workerName2}'
|
|
`,
|
|
"functions/cee.ts": dedent /* javascript */ `
|
|
export async function onRequest(context) {
|
|
return context.env.CEE.fetch("https://example.com");
|
|
}`,
|
|
"functions/bee.ts": dedent /* javascript */ `
|
|
export async function onRequest(context) {
|
|
return context.env.BEE.fetch("https://example.com");
|
|
}`,
|
|
"public/index.html": `<h1>hello pages assets</h1>`,
|
|
});
|
|
});
|
|
|
|
it("pages project assets", async ({ expect }) => {
|
|
const pages = helper.runLongLived(
|
|
`wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await pages.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () =>
|
|
await expect(fetchText(`${url}`)).resolves.toBe(
|
|
"<h1>hello pages assets</h1>"
|
|
)
|
|
);
|
|
});
|
|
|
|
it("pages project fetching service worker", async ({ expect }) => {
|
|
const pages = helper.runLongLived(
|
|
`wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await pages.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () =>
|
|
await expect(fetchText(`${url}/cee`)).resolves.toBe(
|
|
"Hello from service worker"
|
|
)
|
|
);
|
|
});
|
|
|
|
it("pages project fetching module worker", async ({ expect }) => {
|
|
const pages = helper.runLongLived(
|
|
`wrangler pages dev -c wrangler.toml -c ${b}/wrangler.toml -c ${c}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
const { url } = await pages.waitForReady(5_000);
|
|
|
|
await waitForLong(
|
|
async () =>
|
|
await expect(fetchText(`${url}/bee`)).resolves.toBe("hello world")
|
|
);
|
|
});
|
|
|
|
it("should error if multiple pages configs are provided", async () => {
|
|
const pages = helper.runLongLived(
|
|
`wrangler pages dev -c wrangler.toml -c wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
await pages.readUntil(
|
|
/You cannot use a Pages project as a service binding target/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("scheduled worker warnings", () => {
|
|
beforeEach(async () => {
|
|
await baseSeed(a, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[triggers]
|
|
crons = ["* * * * *"]
|
|
|
|
[[services]]
|
|
binding = "BEE"
|
|
service = '${workerName2}'
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
export default {
|
|
async fetch(req, env) {
|
|
return env.BEE.fetch(req);
|
|
},
|
|
scheduled(event) {
|
|
console.log("Worker A scheduled event");
|
|
}
|
|
};
|
|
`,
|
|
});
|
|
|
|
await baseSeed(b, {
|
|
"wrangler.toml": dedent`
|
|
name = "${workerName2}"
|
|
main = "src/index.ts"
|
|
compatibility_date = "2024-11-01"
|
|
|
|
[triggers]
|
|
crons = ["0 * * * *"]
|
|
`,
|
|
"src/index.ts": dedent /* javascript */ `
|
|
export default {
|
|
async fetch(req, env) {
|
|
return new Response("hello world");
|
|
},
|
|
scheduled(event) {
|
|
console.log("Worker B scheduled event");
|
|
}
|
|
};
|
|
`,
|
|
});
|
|
});
|
|
|
|
it("shows warning with correct port when multiple workers have cron triggers", async ({
|
|
expect,
|
|
}) => {
|
|
const worker = helper.runLongLived(
|
|
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
|
|
{ cwd: a }
|
|
);
|
|
|
|
const { url } = await worker.waitForReady(5_000);
|
|
const { hostname, port } = new URL(url);
|
|
|
|
await waitFor(() => {
|
|
// The warning should contain the actual port, not "undefined"
|
|
expect(worker.currentOutput).toContain(
|
|
"Scheduled Workers are not automatically triggered"
|
|
);
|
|
expect(worker.currentOutput).toContain(
|
|
`curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"`
|
|
);
|
|
expect(worker.currentOutput).not.toContain("undefined");
|
|
});
|
|
});
|
|
});
|
|
});
|