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
293 lines
9.7 KiB
TypeScript
293 lines
9.7 KiB
TypeScript
import {
|
|
createExecutionContext,
|
|
runDurableObjectAlarm,
|
|
runInDurableObject,
|
|
} from "cloudflare:test";
|
|
import { env, RpcStub } from "cloudflare:workers";
|
|
import { describe, it, onTestFinished } from "vitest";
|
|
import TestDefaultEntrypoint, { TestObject } from "../src";
|
|
import { Counter } from "../src/counter";
|
|
|
|
describe("named entrypoints", () => {
|
|
it("dispatches fetch request to named ExportedHandler", async ({
|
|
expect,
|
|
}) => {
|
|
const response = await env.TEST_NAMED_HANDLER.fetch("https://example.com");
|
|
expect(await response.json()).toMatchObject({
|
|
ctxWaitUntil: "function",
|
|
method: "GET",
|
|
source: "testNamedHandler",
|
|
url: "https://example.com/",
|
|
});
|
|
});
|
|
it("dispatches fetch request to named WorkerEntrypoint", async ({
|
|
expect,
|
|
}) => {
|
|
const response = await env.TEST_NAMED_ENTRYPOINT.fetch(
|
|
"https://example.com"
|
|
);
|
|
expect(await response.json()).toMatchObject({
|
|
ctxWaitUntil: "function",
|
|
method: "GET",
|
|
source: "TestNamedEntrypoint",
|
|
url: "https://example.com/",
|
|
});
|
|
});
|
|
it("calls method with rpc", async ({ expect }) => {
|
|
const result = await env.TEST_NAMED_ENTRYPOINT.ping();
|
|
expect(result).toBe("pong");
|
|
});
|
|
|
|
it("receives RpcTarget over RPC", async ({ expect }) => {
|
|
const result = await env.TEST_NAMED_ENTRYPOINT.getCounter();
|
|
expect(await result.value).toBe(0);
|
|
result.increment();
|
|
result.increment();
|
|
expect(await result.value).toBe(2);
|
|
const counter2 = result.clone();
|
|
counter2.increment();
|
|
expect(await counter2.value).toBe(3);
|
|
expect(await result.value).toBe(2);
|
|
});
|
|
|
|
it("receives plain objects over RPC", async ({ expect }) => {
|
|
const result = await env.TEST_NAMED_ENTRYPOINT.getCounter();
|
|
result.increment();
|
|
expect(await result.asObject()).toMatchObject({ val: 1 });
|
|
});
|
|
});
|
|
|
|
describe("Durable Object", () => {
|
|
const orderingAttempts = 5;
|
|
const orderingCalls = 100;
|
|
|
|
function expectedOrderingLog() {
|
|
return Array.from({ length: orderingCalls }, (_, i) => `call-${i}`);
|
|
}
|
|
function orderingTargetName(prefix: string, attempt: number) {
|
|
return `${prefix}-${crypto.randomUUID()}-${attempt}`;
|
|
}
|
|
|
|
it("dispatches fetch request", async ({ expect }) => {
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
const response = await stub.fetch("https://example.com");
|
|
expect(await response.json()).toMatchObject({
|
|
ctxWaitUntil: "function",
|
|
method: "GET",
|
|
source: "TestObject",
|
|
url: "https://example.com/",
|
|
});
|
|
});
|
|
it("increments count and allows direct/rpc access to instance/storage", async ({
|
|
expect,
|
|
}) => {
|
|
// Check sending request directly to instance
|
|
const id = env.TEST_OBJECT.idFromName("/path");
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
const result = await runInDurableObject(stub, (instance: TestObject) => {
|
|
expect(instance).toBeInstanceOf(TestObject); // Exact same class as import
|
|
return instance.increment(1);
|
|
});
|
|
expect(result).toBe(1);
|
|
|
|
// Check direct access to properties and storage
|
|
await runInDurableObject(stub, async (instance: TestObject, state) => {
|
|
expect(instance.value).toBe(1);
|
|
expect(await state.storage.get<number>("count")).toBe(1);
|
|
});
|
|
|
|
// Check calling method over RPC
|
|
expect(await stub.increment(3)).toBe(4);
|
|
|
|
// Check accessing property over RPC
|
|
expect(await stub.value).toBe(4);
|
|
});
|
|
it("dispatches RPC methods from proxy-returning Durable Objects", async ({
|
|
expect,
|
|
}) => {
|
|
const id = env.PROXIED_TEST_OBJECT.newUniqueId();
|
|
const stub = env.PROXIED_TEST_OBJECT.get(id);
|
|
expect(await stub.readPrivateValue()).toBe("private value");
|
|
});
|
|
it("rejects instance overrides of prototype methods", async ({ expect }) => {
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
await expect(
|
|
async () => await stub.overriddenPrototypeMethod()
|
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
`[TypeError: The RPC receiver does not implement the method "overriddenPrototypeMethod".]`
|
|
);
|
|
});
|
|
it("immediately executes alarm", async ({ expect }) => {
|
|
// Schedule alarm by directly calling method over RPC
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
await stub.increment(3);
|
|
await stub.scheduleReset(60_000);
|
|
|
|
// Check counter has non-zero value
|
|
expect(await stub.value).toBe(3);
|
|
|
|
// Immediately execute the alarm to reset the counter
|
|
let ran = await runDurableObjectAlarm(stub);
|
|
expect(ran).toBe(true); // ...as there was an alarm scheduled
|
|
|
|
// Check counter value was reset
|
|
expect(await stub.value).toBe(0);
|
|
});
|
|
it("cannot access instance properties or methods", async ({ expect }) => {
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
await expect(async () => await stub.instanceProperty).rejects
|
|
.toThrowErrorMatchingInlineSnapshot(`
|
|
[TypeError: The RPC receiver's prototype does not implement "instanceProperty", but the receiver instance does.
|
|
Only properties and methods defined on the prototype can be accessed over RPC.
|
|
Ensure properties are declared like \`get instanceProperty() { ... }\` instead of \`instanceProperty = ...\`,
|
|
and methods are declared like \`instanceProperty() { ... }\` instead of \`instanceProperty = () => { ... }\`.]
|
|
`);
|
|
});
|
|
it("cannot access non-existent properties or methods", async ({ expect }) => {
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
await expect(
|
|
// @ts-expect-error intentionally testing incorrect types
|
|
async () => await stub.nonExistentProperty
|
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
`[TypeError: The RPC receiver does not implement "nonExistentProperty".]`
|
|
);
|
|
});
|
|
it("receives RpcTarget over RPC", async ({ expect }) => {
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
using result = await stub.getCounter();
|
|
expect(await result.value).toBe(0);
|
|
await result.increment();
|
|
await result.increment();
|
|
expect(await result.value).toBe(2);
|
|
// TODO: Improve RPC types so this casting isn't required
|
|
using counter2 = (await result.clone()) as unknown as Counter & Disposable;
|
|
await counter2.increment();
|
|
expect(await counter2.value).toBe(3);
|
|
expect(await result.value).toBe(2);
|
|
});
|
|
|
|
it("receives plain objects over RPC", async ({ expect }) => {
|
|
const id = env.TEST_OBJECT.newUniqueId();
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
using result = await stub.getObject();
|
|
expect(result).toMatchObject({ hello: "world" });
|
|
});
|
|
|
|
// Regression repro for https://github.com/cloudflare/workers-sdk/issues/13433.
|
|
it("preserves same-type RPC call order", async ({ expect }) => {
|
|
for (let attempt = 0; attempt < orderingAttempts; attempt++) {
|
|
const id = env.TEST_OBJECT.idFromName(
|
|
orderingTargetName("ordering", attempt)
|
|
);
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
const promises: Promise<void>[] = [];
|
|
|
|
for (let i = 0; i < orderingCalls; i++) {
|
|
promises.push(stub.record(`call-${i}`));
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
expect(await stub.getLog()).toEqual(expectedOrderingLog());
|
|
}
|
|
});
|
|
|
|
it("preserves same-type RPC call order after prewarming instance", async ({
|
|
expect,
|
|
}) => {
|
|
for (let attempt = 0; attempt < orderingAttempts; attempt++) {
|
|
const id = env.TEST_OBJECT.idFromName(
|
|
orderingTargetName("prewarmed-ordering", attempt)
|
|
);
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
await stub.getLog();
|
|
const promises: Promise<void>[] = [];
|
|
|
|
for (let i = 0; i < orderingCalls; i++) {
|
|
promises.push(stub.record(`call-${i}`));
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
expect(await stub.getLog()).toEqual(expectedOrderingLog());
|
|
}
|
|
});
|
|
|
|
it("preserves same-type RPC call order from a WorkerEntrypoint caller", async ({
|
|
expect,
|
|
}) => {
|
|
for (let attempt = 0; attempt < orderingAttempts; attempt++) {
|
|
expect(
|
|
await env.TEST_NAMED_ENTRYPOINT.recordFromWorkerEntrypoint(
|
|
orderingTargetName("worker-entrypoint-ordering", attempt),
|
|
orderingCalls
|
|
)
|
|
).toEqual(expectedOrderingLog());
|
|
}
|
|
});
|
|
|
|
it("preserves same-type RPC call order from a Durable Object caller", async ({
|
|
expect,
|
|
}) => {
|
|
for (let attempt = 0; attempt < orderingAttempts; attempt++) {
|
|
const id = env.TEST_OBJECT.idFromName(
|
|
orderingTargetName("do-caller", attempt)
|
|
);
|
|
const stub = env.TEST_OBJECT.get(id);
|
|
expect(
|
|
await stub.recordFromDurableObject(
|
|
orderingTargetName("do-caller-ordering", attempt),
|
|
orderingCalls
|
|
)
|
|
).toEqual(expectedOrderingLog());
|
|
}
|
|
});
|
|
});
|
|
|
|
// Regression: https://github.com/cloudflare/workers-sdk/issues/7077
|
|
// Fixed in workerd by https://github.com/cloudflare/workerd/pull/3782
|
|
it("can construct a WorkerEntrypoint with mocked env", async ({ expect }) => {
|
|
const data = new Map<string, string>([["mocked-key", "mocked-value"]]);
|
|
const mockedKv = new Proxy(env.KV_NAMESPACE, {
|
|
get: (target, prop, receiver) =>
|
|
prop === "get"
|
|
? async (key: string) => data.get(key) ?? null
|
|
: Reflect.get(target, prop, receiver),
|
|
});
|
|
|
|
const ctx = createExecutionContext();
|
|
const worker = new TestDefaultEntrypoint(ctx, {
|
|
...env,
|
|
KV_NAMESPACE: mockedKv,
|
|
});
|
|
expect(await worker.read("mocked-key")).toBe("mocked-value");
|
|
});
|
|
|
|
describe("counter", () => {
|
|
it("increments count", ({ expect }) => {
|
|
const counter = new Counter(3);
|
|
expect(counter.increment()).toBe(4);
|
|
expect(counter.increment(2)).toBe(6);
|
|
expect(counter.value).toBe(6);
|
|
});
|
|
it("clones counters", ({ expect }) => {
|
|
const counter = new Counter(3);
|
|
const clone = counter.clone();
|
|
expect(counter.increment()).toBe(4);
|
|
expect(clone.value).toBe(3);
|
|
});
|
|
it("calls methods with loopback rpc and pipelining", async ({ expect }) => {
|
|
const stub = new RpcStub(new Counter(1));
|
|
// TODO(soon): replace with `using` when supported
|
|
onTestFinished(() => stub[Symbol.dispose]());
|
|
const result = await stub.clone().increment(3);
|
|
expect(result).toBe(4);
|
|
});
|
|
});
|