Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a ctx field to the getBindingsProxy result #4922

Merged
merged 6 commits into from
Feb 5, 2024
Merged
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
16 changes: 16 additions & 0 deletions .changeset/soft-tigers-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"wrangler": minor
---

feature: add a `ctx` field to the `getBindingsProxy` result

Add a new `ctx` filed to the `getBindingsProxy` result that people can use to mock the production
`ExecutionContext` object.

Example:

```ts
const { ctx } = await getBindingsProxy();
// ...
ctx.waitUntil(myPromise);
```
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Fetcher,
R2Bucket,
} from "@cloudflare/workers-types";
import { Request, Response } from "undici";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
getBindingsProxy as originalGetBindingsProxy,
Expand Down Expand Up @@ -42,7 +41,7 @@ function getBindingsProxy<T>(
});
}

describe("getBindingsProxy", () => {
describe("getBindingsProxy - bindings", () => {
let devWorkers: UnstableDevWorker[];

beforeAll(async () => {
Expand Down Expand Up @@ -228,25 +227,6 @@ describe("getBindingsProxy", () => {
await dispose();
}
});

describe("caches", () => {
(["default", "named"] as const).forEach((cacheType) =>
it(`correctly obtains a no-op ${cacheType} cache`, async () => {
const { caches, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
try {
const cache =
cacheType === "default"
? caches.default
: await caches.open("my-cache");
testNoOpCache(cache);
} finally {
await dispose();
}
})
);
});
});

/**
Expand Down Expand Up @@ -283,17 +263,3 @@ async function testDoBinding(
const doRespText = await doResp.text();
expect(doRespText).toBe(expectedResponse);
}

async function testNoOpCache(
cache: Awaited<ReturnType<typeof getBindingsProxy>>["caches"]["default"]
) {
let match = await cache.match("http://0.0.0.0/test");
expect(match).toBeUndefined();

const req = new Request("http://0.0.0.0/test");
await cache.put(req, new Response("test"));
const resp = await cache.match(req);
expect(resp).toBeUndefined();
const deleted = await cache.delete(req);
expect(deleted).toBe(false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Request, Response } from "undici";
import { describe, expect, it } from "vitest";
import { getBindingsProxy } from "./shared";

describe("getBindingsProxy - caches", () => {
(["default", "named"] as const).forEach((cacheType) =>
it(`correctly obtains a no-op ${cacheType} cache`, async () => {
const { caches, dispose } = await getBindingsProxy();
try {
const cache =
cacheType === "default"
? caches.default
: await caches.open("my-cache");
testNoOpCache(cache);
} finally {
await dispose();
}
})
);
});

async function testNoOpCache(
cache: Awaited<ReturnType<typeof getBindingsProxy>>["caches"]["default"]
) {
let match = await cache.match("http://0.0.0.0/test");
expect(match).toBeUndefined();

const req = new Request("http://0.0.0.0/test");
await cache.put(req, new Response("test"));
const resp = await cache.match(req);
expect(resp).toBeUndefined();
const deleted = await cache.delete(req);
expect(deleted).toBe(false);
}
55 changes: 55 additions & 0 deletions fixtures/get-bindings-proxy/tests/get-bindings-proxy.ctx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { getBindingsProxy } from "./shared";

describe("getBindingsProxy - ctx", () => {
it("should provide a no-op waitUntil method", async () => {
const { ctx, dispose } = await getBindingsProxy();
try {
let value = 4;
ctx.waitUntil(
new Promise((resolve) => {
value++;
resolve(value);
})
);
expect(value).toBe(5);
} finally {
await dispose();
}
});

it("should provide a no-op passThroughOnException method", async () => {
const { ctx, dispose } = await getBindingsProxy();
try {
expect(ctx.passThroughOnException()).toBe(undefined);
} finally {
await dispose();
}
});

it("should match the production runtime ctx object", async () => {
const { ctx, dispose } = await getBindingsProxy();
try {
expect(ctx.constructor.name).toBe("ExecutionContext");
expect(typeof ctx.waitUntil).toBe("function");
expect(typeof ctx.passThroughOnException).toBe("function");

ctx.waitUntil = ((str: string) => `- ${str} -`) as any;
expect(ctx.waitUntil("waitUntil can be overridden" as any)).toBe(
"- waitUntil can be overridden -"
);

ctx.passThroughOnException = ((str: string) => `_ ${str} _`) as any;
expect(
(ctx.passThroughOnException as any)(
"passThroughOnException can be overridden"
)
).toBe("_ passThroughOnException can be overridden _");

(ctx as any).text = "the ExecutionContext can be extended";
expect((ctx as any).text).toBe("the ExecutionContext can be extended");
mrbbot marked this conversation as resolved.
Show resolved Hide resolved
} finally {
await dispose();
}
});
});
13 changes: 13 additions & 0 deletions fixtures/get-bindings-proxy/tests/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getBindingsProxy as originalGetBindingsProxy } from "wrangler";
import type { GetBindingsProxyOptions } from "wrangler";

// Here we wrap the actual original getBindingsProxy function and disable its persistance, this is to make sure
// that we don't implement any persistance during these tests (which would add unnecessary extra complexity)
export function getBindingsProxy<T>(
options: Omit<GetBindingsProxyOptions, "persist"> = {}
): ReturnType<typeof originalGetBindingsProxy<T>> {
return originalGetBindingsProxy({
...options,
persist: false,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class ExecutionContext {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, unused-imports/no-unused-vars
waitUntil(promise: Promise<any>): void {}
mrbbot marked this conversation as resolved.
Show resolved Hide resolved
passThroughOnException(): void {}

Check warning on line 4 in packages/wrangler/src/api/integrations/bindings/executionContext.ts

View check run for this annotation

Codecov / codecov/patch

packages/wrangler/src/api/integrations/bindings/executionContext.ts#L3-L4

Added lines #L3 - L4 were not covered by tests
}
6 changes: 6 additions & 0 deletions packages/wrangler/src/api/integrations/bindings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getBoundRegisteredWorkers } from "../../../dev-registry";
import { getVarsForDev } from "../../../dev/dev-vars";
import { buildMiniflareBindingOptions } from "../../../dev/miniflare";
import { CacheStorage } from "./caches";
import { ExecutionContext } from "./executionContext";
import { getServiceBindings } from "./services";
import type { Config } from "../../../config";
import type { MiniflareOptions } from "miniflare";
Expand Down Expand Up @@ -40,6 +41,10 @@ export type BindingsProxy<Bindings = Record<string, unknown>> = {
* Object containing the various proxies
*/
bindings: Bindings;
/**
* Mock of the context object that Workers received in their request handler, all the object's methods are no-op
*/
ctx: ExecutionContext;
/**
* Caches object emulating the Workers Cache runtime API
*/
Expand Down Expand Up @@ -88,6 +93,7 @@ export async function getBindingsProxy<Bindings = Record<string, unknown>>(
...vars,
...bindings,
},
ctx: new ExecutionContext(),
mrbbot marked this conversation as resolved.
Show resolved Hide resolved
caches: new CacheStorage(),
dispose: () => mf.dispose(),
};
Expand Down
Loading