Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/msw-cloudflare-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/vitest-pool-workers": minor
---

Mocking requests with MSW in Worker tests now requires MSW >= 2.14

`@cloudflare/vitest-pool-workers` previously shipped internal shims to make MSW work inside the workerd runtime. MSW 2.14 added that support natively, so those shims have been removed.

If you mock requests with MSW in your Worker tests, make sure you're on MSW `>= 2.14`; older versions will no longer intercept requests. You can keep using `setupServer()` from `msw/node`, or adopt the official [`@msw/cloudflare`](https://github.com/mswjs/cloudflare) integration via `setupNetwork()`. See the updated [`request-mocking` example fixture](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples/request-mocking) for the recommended pattern.
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { test as base, expect } from "@playwright/test";
import { http, HttpResponse } from "msw";
import { setupServer, type SetupServerApi } from "msw/node";
import { setupServer } from "msw/node";
import { createTestHarness, type TestHarness } from "wrangler";

type TestFixtures = {
reset: void;
};

type WorkerFixtures = {
network: SetupServerApi;
network: ReturnType<typeof setupServer>;
server: TestHarness;
};

Expand Down
4 changes: 2 additions & 2 deletions fixtures/vitest-pool-workers-examples/hyperdrive/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "vitest";

declare module "vitest" {
interface ProvidedContext {
echoServerPort: number;
}
}

export {};
1 change: 1 addition & 0 deletions fixtures/vitest-pool-workers-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@cloudflare/vitest-pool-workers": "workspace:*",
"@cloudflare/workers-types": "catalog:default",
"@microlabs/otel-cf-workers": "1.0.0-rc.45",
"@msw/cloudflare": "0.0.1",
"@types/mime-types": "^3.0.1",
"@types/node": "catalog:default",
"@types/nunjucks": "^3.2.6",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# 🤹 request-mocking

This Worker rewrites the host of all incoming requests to `cloudflare.com` then forwards the request on. Tests demonstrate declarative mocking with [MSW (Mock Service Worker)](https://mswjs.io/), and imperative mocks of `globalThis.fetch()`. Note mocking WebSocket requests is only supported with imperative mocking.
This Worker rewrites the host of all incoming requests to `cloudflare.com` then forwards the request on, except for the `/echo-ws` path which opens an outbound WebSocket. Tests demonstrate declarative request mocking with [MSW (Mock Service Worker)](https://mswjs.io/) via the [`@msw/cloudflare`](https://github.com/mswjs/cloudflare) integration, including outbound WebSocket connections.

| Test | Overview |
| ----------------------------------------------- | ----------------------------------------------------------------------- |
| [declarative.test.ts](test/declarative.test.ts) | Integration tests with declarative request mocking using MSW |
| [imperative.test.ts](test/imperative.test.ts) | Integration tests with imperative request mocking, including WebSockets |
| Test | Worker invocation style | Overview |
| ------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------ |
| [direct.test.ts](test/direct.test.ts) | `worker.fetch(req, env, ctx)` | Mocking HTTP requests with `http.get` / `http.post` handlers |
| [websocket.test.ts](test/websocket.test.ts) | `worker.fetch(req, env, ctx)` | Mocking outbound WebSocket connections (`new WebSocket(url)`) with the `ws.link` API |
| [exports.test.ts](test/exports.test.ts) | `exports.default.fetch(...)` | Mocking HTTP requests dispatched into a separate request I/O context |

`exports.test.ts` verifies that MSW handlers registered in the test runner also intercept requests dispatched through `exports.default.fetch(...)` into a separate request I/O context.
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
export default <ExportedHandler>{
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
url.host = "cloudflare.com";

try {
// Special handler: open a WebSocket to the rewritten URL, send a
// message, then return the first reply as the response.
if (url.pathname === "/echo-ws") {
url.protocol = "wss:";
const ws = new WebSocket(url.toString());
try {
const messagePromise = new Promise<string>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("WebSocket connection timed out")),
5_000
);
ws.addEventListener("message", (event) => {
clearTimeout(timeout);
resolve(String(event.data));
});
ws.addEventListener("error", () => {
clearTimeout(timeout);
reject(new Error("WebSocket connection errored"));
});
});
ws.addEventListener("open", () => ws.send("hello"));
return Response.json({ message: await messagePromise });
} finally {
ws.close();
}
}

return await fetch(url, request);
} catch (e) {
return new Response(String(e), { status: 500 });
}
},
};
} satisfies ExportedHandler;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Exercises the "direct-import" worker invocation pattern:
// `import worker from "../src/index"` followed by `worker.fetch(req, env, ctx)`.
// The worker's `fetch` handler runs in the same I/O context as the test
// runner where `setupNetwork()` was enabled.
//
// For the `exports.default.fetch(...)` counterpart (different request I/O
// context per call) see `exports.test.ts`.
import {
createExecutionContext,
waitOnExecutionContext,
} from "cloudflare:test";
import { env } from "cloudflare:workers";
import { http, HttpResponse } from "msw";
import { it } from "vitest";
import worker from "../src/index";
import { network } from "./server";

it("mocks GET requests", async ({ expect }) => {
network.use(
http.get(
"https://cloudflare.com/once",
() => {
return HttpResponse.text("😉");
},
{ once: true }
),
http.get("https://cloudflare.com/persistent", () => {
return HttpResponse.text("📌");
})
);

// Host `example.com` will be rewritten to `cloudflare.com` by the Worker
let ctx = createExecutionContext();
let response = await worker.fetch(
new Request("https://example.com/once"),
env,
ctx
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
expect(await response.text()).toBe("😉");

// Persistent handlers match forever
for (let i = 0; i < 3; i++) {
ctx = createExecutionContext();
response = await worker.fetch(
new Request("https://example.com/persistent"),
env,
ctx
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
expect(await response.text()).toBe("📌");
}
});

it("mocks POST requests", async ({ expect }) => {
network.use(
http.post("https://cloudflare.com/path", async ({ request }) => {
const text = await request.text();
if (text !== "✨") {
return HttpResponse.text("Bad request body", { status: 400 });
}
return HttpResponse.text("✅");
})
);

// Sending a request without the expected body returns an error response...
let ctx = createExecutionContext();
let response = await worker.fetch(
new Request("https://example.com/path", { method: "POST", body: "🙃" }),
env,
ctx
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(400);
expect(await response.text()).toBe("Bad request body");

// ...but the correct body should succeed
ctx = createExecutionContext();
response = await worker.fetch(
new Request("https://example.com/path", { method: "POST", body: "✨" }),
env,
ctx
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
expect(await response.text()).toBe("✅");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Exercises the "integration-self" worker invocation pattern:
// `exports.default.fetch(...)`, which dispatches each call into a fresh
// request I/O context separate from the runner DO context where
// `setupNetwork()` was enabled in `beforeAll`.
import { exports } from "cloudflare:workers";
import { http, HttpResponse } from "msw";
import { it } from "vitest";
import { network } from "./server";

it("mocks GET requests via exports.default.fetch", async ({ expect }) => {
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
network.use(
http.get("https://cloudflare.com/exports", () => {
return HttpResponse.text("🟢");
})
);

const response = await exports.default.fetch("https://example.com/exports");
expect(response.status).toBe(200);
expect(await response.text()).toBe("🟢");
});

it("mocks POST requests via exports.default.fetch", async ({ expect }) => {
network.use(
http.post("https://cloudflare.com/exports", async ({ request }) => {
const text = await request.text();
if (text !== "✨") {
return HttpResponse.text("Bad request body", { status: 400 });
}
return HttpResponse.text("✅");
})
);

let response = await exports.default.fetch("https://example.com/exports", {
method: "POST",
body: "🙃",
});
expect(response.status).toBe(400);
expect(await response.text()).toBe("Bad request body");

response = await exports.default.fetch("https://example.com/exports", {
method: "POST",
body: "✨",
});
expect(response.status).toBe(200);
expect(await response.text()).toBe("✅");
});
Loading
Loading