Skip to content

Commit 187d887

Browse files
feat(fixtures): Add new fixtures for Workers + Assets with service bindings (#7980)
* feat(fixtures): Add new fixtures Workers + Assets with service bindings * feedback fixes * fix tests
1 parent 924e7e6 commit 187d887

File tree

24 files changed

+635
-1
lines changed

24 files changed

+635
-1
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# workers-with-assets-and-service-bindings
2+
3+
`workers-with-assets-and-service-bindings` is a test fixture that showcases [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) between a Worker and a [Worker with assets](https://developers.cloudflare.com/workers/static-assets/).
4+
5+
The fixture sets up multiple Workers:
6+
7+
- `worker-A` ➔ a regular Worker without assets
8+
- `worker-B` ➔ a Worker with assets, that exports a default object
9+
10+
```
11+
export default {
12+
async fetch() {}
13+
}
14+
```
15+
16+
- `worker-C` ➔ a Worker with assets, that exports a default entrypoint
17+
18+
```
19+
export default class extends WorkerEntrypoint {
20+
async fetch(){}
21+
}
22+
```
23+
24+
- `worker-D` ➔ a Worker with assets, that exports a named entrypoint
25+
26+
```
27+
export class EntrypointD extends WokrerEntrypoint {}
28+
```
29+
30+
and configures service bindings between `worker-A` and all other Workers:
31+
32+
```
33+
## workerA/wrangler.toml
34+
35+
# service binding to Worker that exports a default object
36+
[[services]]
37+
binding = "DEFAULT_EXPORT"
38+
service = "worker-b"
39+
40+
# service binding to Worker that exports a default entrypoint
41+
[[services]]
42+
binding = "DEFAULT_ENTRYPOINT"
43+
service = "worker-c"
44+
45+
# service binding to Worker that exports a named entrypoint
46+
[[services]]
47+
binding = "NAMED_ENTRYPOINT"
48+
service = "worker-d"
49+
entrypoint = "EntrypointD"
50+
```
51+
52+
## dev
53+
54+
To start a dev session for each Worker individually, run:
55+
56+
```
57+
cd <worker_directory>
58+
wrangler dev
59+
```
60+
61+
## Run tests
62+
63+
```
64+
npm run test:ci
65+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "workers-with-assets-and-service-bindings",
3+
"private": true,
4+
"scripts": {
5+
"dev": "wrangler dev",
6+
"test:ci": "vitest run",
7+
"test:watch": "vitest",
8+
"type:tests": "tsc -p ./tests/tsconfig.json"
9+
},
10+
"devDependencies": {
11+
"@cloudflare/workers-tsconfig": "workspace:*",
12+
"@cloudflare/workers-types": "^4.20250121.0",
13+
"typescript": "catalog:default",
14+
"undici": "catalog:default",
15+
"vitest": "catalog:default",
16+
"wrangler": "workspace:*"
17+
},
18+
"volta": {
19+
"extends": "../../package.json"
20+
}
21+
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { resolve } from "node:path";
2+
import { setTimeout } from "timers/promises";
3+
import { fetch } from "undici";
4+
import { afterAll, beforeAll, describe, it } from "vitest";
5+
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";
6+
7+
const devCmds = [{ args: [] }, { args: ["--x-assets-rpc"] }];
8+
9+
describe.each(devCmds)(
10+
"[wrangler dev $args][Workers + Assets] Service bindings to Worker with assets",
11+
({ args }) => {
12+
let ipWorkerA: string,
13+
portWorkerA: number,
14+
stopWorkerA: (() => Promise<unknown>) | undefined;
15+
let stopWorkerB: (() => Promise<unknown>) | undefined,
16+
getOutputWorkerB: () => string;
17+
let stopWorkerC: (() => Promise<unknown>) | undefined,
18+
getOutputWorkerC: () => string;
19+
let stopWorkerD: (() => Promise<unknown>) | undefined;
20+
21+
beforeAll(async () => {
22+
({ getOutput: getOutputWorkerB } = await runWranglerDev(
23+
resolve(__dirname, "..", "workerB-with-default-export"),
24+
["--port=0", "--inspector-port=0", ...args]
25+
));
26+
27+
({ getOutput: getOutputWorkerC } = await runWranglerDev(
28+
resolve(__dirname, "..", "workerC-with-default-entrypoint"),
29+
["--port=0", "--inspector-port=0", ...args]
30+
));
31+
32+
({ stop: stopWorkerD } = await runWranglerDev(
33+
resolve(__dirname, "..", "workerD-with-named-entrypoint"),
34+
["--port=0", "--inspector-port=0", ...args]
35+
));
36+
37+
({
38+
ip: ipWorkerA,
39+
port: portWorkerA,
40+
stop: stopWorkerA,
41+
} = await runWranglerDev(resolve(__dirname, "..", "workerA"), [
42+
"--port=0",
43+
"--inspector-port=0",
44+
]));
45+
});
46+
47+
afterAll(async () => {
48+
await stopWorkerA?.();
49+
await stopWorkerB?.();
50+
await stopWorkerC?.();
51+
await stopWorkerD?.();
52+
});
53+
54+
describe("Workers running in separate wrangler dev sessions", () => {
55+
describe("Service binding to default export", () => {
56+
// this currently incorrectly returns the User Worker response
57+
// instead of the Asset Worker response
58+
it.fails(
59+
"should return Asset Worker response for routes that serve static content",
60+
async ({ expect }) => {
61+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
62+
let text = await response.text();
63+
expect(response.status).toBe(200);
64+
expect(text).toContain(
65+
`env.DEFAULT_EXPORT.fetch() response: This is an asset of "worker-b"`
66+
);
67+
68+
response = await fetch(
69+
`http://${ipWorkerA}:${portWorkerA}/busy-bee`
70+
);
71+
text = await response.text();
72+
expect(response.status).toBe(200);
73+
expect(text).toContain(
74+
`env.DEFAULT_EXPORT.fetch() response: All "worker-b" 🐝🐝🐝 are 🐝sy. Please come back later`
75+
);
76+
}
77+
);
78+
79+
it("should return User Worker response for routes that don't serve static content", async ({
80+
expect,
81+
}) => {
82+
let response = await fetch(
83+
`http://${ipWorkerA}:${portWorkerA}/no-assets-at-this-path`
84+
);
85+
let text = await response.text();
86+
expect(response.status).toBe(200);
87+
expect(text).toContain(
88+
"env.DEFAULT_EXPORT.fetch() response: Hello from worker-b fetch()"
89+
);
90+
});
91+
92+
it("should return User Worker response for named functions", async ({
93+
expect,
94+
}) => {
95+
// fetch URL is irrelevant here. workerA will internally call
96+
// the appropriate fns on the service binding instead
97+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
98+
let text = await response.text();
99+
expect(response.status).toBe(200);
100+
expect(text).toContain(
101+
"env.DEFAULT_EXPORT.bee() response: Workers in non-class based syntax do not support RPC functions with zero or variable number of arguments. They only support RPC functions with strictly one argument."
102+
);
103+
expect(text).toContain(
104+
'env.DEFAULT_EXPORT.busyBee("🐝") response: Hello busy 🐝s from worker-b busyBee(bee)'
105+
);
106+
});
107+
108+
it("should return cron trigger responses", async ({ expect }) => {
109+
// fetch URL is irrelevant here. workerA will internally call
110+
// env.DEFAULT_EXPORT.scheduled({cron: "* * * * *"}) instead
111+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
112+
let text = await response.text();
113+
expect(response.status).toBe(200);
114+
expect(text).toContain(
115+
"env.DEFAULT_EXPORT.scheduled() response: undefined"
116+
);
117+
118+
// add a timeout to allow stdout to update
119+
await setTimeout(500);
120+
console.log(getOutputWorkerB());
121+
expect(getOutputWorkerB()).toContain(
122+
"Hello from worker-b scheduled()"
123+
);
124+
});
125+
});
126+
127+
describe("Service binding to default entrypoint", () => {
128+
// this currently incorrectly returns the User Worker response
129+
// instead of the Asset Worker response
130+
it.fails(
131+
"should return Asset Worker response for fetch requestsfor routes that serve static content",
132+
async ({ expect }) => {
133+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
134+
let text = await response.text();
135+
expect(response.status).toBe(200);
136+
expect(text).toContain(
137+
`env.DEFAULT_ENTRYPOINT.fetch() response: This is an asset of "worker-c"`
138+
);
139+
140+
response = await fetch(
141+
`http://${ipWorkerA}:${portWorkerA}/busy-bee`
142+
);
143+
text = await response.text();
144+
expect(response.status).toBe(200);
145+
expect(text).toContain(
146+
`env.DEFAULT_ENTRYPOINT.fetch() response: All "worker-c" 🐝🐝🐝 are 🐝sy. Please come back later`
147+
);
148+
}
149+
);
150+
151+
it("should return User Worker response for routes that don't serve static content", async ({
152+
expect,
153+
}) => {
154+
let response = await fetch(
155+
`http://${ipWorkerA}:${portWorkerA}/no-assets-at-this-path`
156+
);
157+
let text = await response.text();
158+
expect(response.status).toBe(200);
159+
expect(text).toContain(
160+
"env.DEFAULT_ENTRYPOINT.fetch() response: Hello from worker-c fetch()"
161+
);
162+
});
163+
164+
it("should return User Worker response for named functions", async ({
165+
expect,
166+
}) => {
167+
// fetch URL is irrelevant here. workerA will internally call
168+
// the appropriate fns on the service binding instead
169+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
170+
let text = await response.text();
171+
expect(response.status).toBe(200);
172+
expect(text).toContain(
173+
"env.DEFAULT_ENTRYPOINT.bee() response: Hello from worker-c bee()"
174+
);
175+
expect(text).toContain(
176+
'env.DEFAULT_ENTRYPOINT.busyBee("🐝") response: Hello busy 🐝s from worker-c busyBee(bee)'
177+
);
178+
});
179+
180+
it("should return cron trigger responses", async ({ expect }) => {
181+
// fetch URL is irrelevant here. workerA will internally call
182+
// env.DEFAULT_ENTRYPOINT.scheduled({cron: "* * * * *"}) instead
183+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
184+
let text = await response.text();
185+
expect(response.status).toBe(200);
186+
expect(text).toContain(
187+
"env.DEFAULT_ENTRYPOINT.scheduled() response: undefined"
188+
);
189+
190+
// add a timeout to allow stdout to update
191+
await setTimeout(500);
192+
expect(getOutputWorkerC()).toContain(
193+
"Hello from worker-c scheduled()"
194+
);
195+
});
196+
});
197+
198+
describe("Service binding to named entrypoint", () => {
199+
it("should return User Worker response for fetch requests", async ({
200+
expect,
201+
}) => {
202+
// static asset route
203+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
204+
let text = await response.text();
205+
expect(response.status).toBe(200);
206+
expect(text).toContain(
207+
`env.NAMED_ENTRYPOINT.fetch() response: Hello from worker-d fetch()`
208+
);
209+
210+
// static asset route
211+
response = await fetch(`http://${ipWorkerA}:${portWorkerA}/busy-bee`);
212+
text = await response.text();
213+
expect(response.status).toBe(200);
214+
expect(text).toContain(
215+
`env.NAMED_ENTRYPOINT.fetch() response: Hello from worker-d fetch()`
216+
);
217+
218+
// User Worker route
219+
response = await fetch(
220+
`http://${ipWorkerA}:${portWorkerA}/no-assets-at-this-path`
221+
);
222+
text = await response.text();
223+
expect(response.status).toBe(200);
224+
expect(text).toContain(
225+
"env.NAMED_ENTRYPOINT.fetch() response: Hello from worker-d fetch()"
226+
);
227+
});
228+
229+
it("should return User Worker response for named functions", async ({
230+
expect,
231+
}) => {
232+
// fetch URL is irrelevant here. workerA will internally call
233+
// the appropriate fns on the service binding instead
234+
let response = await fetch(`http://${ipWorkerA}:${portWorkerA}`);
235+
let text = await response.text();
236+
expect(response.status).toBe(200);
237+
expect(text).toContain(
238+
"env.NAMED_ENTRYPOINT.bee() response: Hello from worker-d bee()"
239+
);
240+
expect(text).toContain(
241+
'env.NAMED_ENTRYPOINT.busyBee("🐝") response: Hello busy 🐝s from worker-d busyBee(bee)'
242+
);
243+
});
244+
});
245+
});
246+
}
247+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "@cloudflare/workers-tsconfig/tsconfig.json",
3+
"compilerOptions": {
4+
"types": ["node"]
5+
},
6+
"include": ["**/*.ts", "../../../node-types.d.ts"]
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "CommonJS",
5+
"lib": ["ES2020"],
6+
"types": ["@cloudflare/workers-types"],
7+
"moduleResolution": "node",
8+
"noEmit": true,
9+
"skipLibCheck": true
10+
},
11+
"include": ["**/*.ts"],
12+
"exclude": ["tests"]
13+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getWorkerBResponses } from "./workerB.util";
2+
import { getWorkerCResponses } from "./workerC.util";
3+
import { getWorkerDResponses } from "./workerD.util";
4+
5+
export default {
6+
async fetch(request, env) {
7+
const workerBResponses = await getWorkerBResponses(request, env);
8+
const workerCResponses = await getWorkerCResponses(request, env);
9+
const workerDResponses = await getWorkerDResponses(request, env);
10+
11+
// let's return everything for now to make testing easier
12+
return new Response(
13+
`"worker-b" Responses\n` +
14+
`env.DEFAULT_EXPORT.fetch() response: ${workerBResponses.fetchResponse}\n` +
15+
`env.DEFAULT_EXPORT.bee() response: ${workerBResponses.beeResult}\n` +
16+
`env.DEFAULT_EXPORT.busyBee("🐝") response: ${workerBResponses.busyBeeResult}\n` +
17+
`env.DEFAULT_EXPORT.scheduled() response: ${workerBResponses.scheduledResponse}\n\n` +
18+
`"worker-c" Responses\n` +
19+
`env.DEFAULT_ENTRYPOINT.fetch() response: ${workerCResponses.fetchResponse}\n` +
20+
`env.DEFAULT_ENTRYPOINT.bee() response: ${workerCResponses.beeResult}\n` +
21+
`env.DEFAULT_ENTRYPOINT.busyBee("🐝") response: ${workerCResponses.busyBeeResult}\n` +
22+
`env.DEFAULT_ENTRYPOINT.scheduled() response: ${workerCResponses.scheduledResponse}\n\n` +
23+
`"worker-d" Responses\n` +
24+
`env.NAMED_ENTRYPOINT.fetch() response: ${workerDResponses.fetchResponse}\n` +
25+
`env.NAMED_ENTRYPOINT.bee() response: ${workerDResponses.beeResult}\n` +
26+
`env.NAMED_ENTRYPOINT.busyBee("🐝") response: ${workerDResponses.busyBeeResult}\n` +
27+
`env.NAMED_ENTRYPOINT.scheduled() response: ${workerDResponses.scheduledResponse}\n\n`
28+
);
29+
},
30+
};

0 commit comments

Comments
 (0)