Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

343 lines
9.7 KiB
TypeScript

/**
* Miniflare Isolation Tests
*
* Integration tests verifying that miniflare (wrapping workerd) provides
* the isolation primitives needed for the MiniflareDevRunner:
*
* - Service bindings scope capabilities per plugin
* - External service bindings route calls to Node handler functions
* - Plugin code loads from strings (bundles from DB/R2)
* - KV namespace bindings provide per-plugin isolated storage
* - Plugins without bindings cannot access unavailable capabilities
* - Worker reconfiguration supports plugin install/uninstall
*/
import { Miniflare } from "miniflare";
import { afterEach, describe, expect, it } from "vitest";
describe("miniflare plugin isolation", () => {
let mf: Miniflare | undefined;
afterEach(async () => {
if (mf) {
await mf.dispose();
mf = undefined;
}
});
it("can create an isolated plugin worker with scoped service bindings", async () => {
// This test creates:
// 1. A "bridge" worker that simulates the backing service (content API)
// 2. A "plugin" worker that calls the bridge via service binding
// 3. Verifies the plugin can only access what the binding exposes
//
// dispatchFetch always hits the first worker in the array.
// To invoke a specific worker, we put the plugin first and use
// service bindings to connect it to the bridge.
mf = new Miniflare({
workers: [
{
// Plugin is first so dispatchFetch targets it
name: "plugin-test",
modules: true,
serviceBindings: {
BRIDGE: "bridge",
},
script: `
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/hook/afterSave") {
const res = await env.BRIDGE.fetch("http://bridge/content/get", {
method: "POST",
body: JSON.stringify({ collection: "posts", id: "123" }),
headers: { "Content-Type": "application/json" },
});
const data = await res.json();
return Response.json({
hookResult: "processed",
contentFromBridge: data,
});
}
return new Response("Unknown hook", { status: 404 });
}
};
`,
},
{
name: "bridge",
modules: true,
script: `
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/content/get") {
const { collection, id } = await request.json();
return Response.json({
success: true,
data: { id, type: collection, slug: "test-post", data: { title: "Hello" } }
});
}
return new Response("Not found", { status: 404 });
}
};
`,
},
],
});
// dispatchFetch hits the first worker (plugin-test)
const response = await mf.dispatchFetch("http://localhost/hook/afterSave");
const result = (await response.json()) as {
hookResult: string;
contentFromBridge: {
success: boolean;
data: { id: string; type: string; slug: string };
};
};
expect(result.hookResult).toBe("processed");
expect(result.contentFromBridge.success).toBe(true);
expect(result.contentFromBridge.data.id).toBe("123");
expect(result.contentFromBridge.data.type).toBe("posts");
});
it("plugins are isolated from each other", async () => {
// Two plugins with different service bindings.
// Plugin A has BRIDGE binding (read:content).
// Plugin B has NO bridge binding (no capabilities).
// Use separate Miniflare instances to test isolation,
// since dispatchFetch always hits the first worker.
// Test Plugin A: has BRIDGE binding
mf = new Miniflare({
workers: [
{
name: "plugin-a",
modules: true,
serviceBindings: {
BRIDGE: async () => {
return Response.json({ success: true, data: { secret: "bridge-data" } });
},
},
script: `
export default {
async fetch(request, env) {
const res = await env.BRIDGE.fetch("http://bridge/");
const data = await res.json();
return Response.json({ hasAccess: true, data });
}
};
`,
},
],
});
const resA = await mf.dispatchFetch("http://localhost/");
const dataA = (await resA.json()) as { hasAccess: boolean };
expect(dataA.hasAccess).toBe(true);
await mf.dispose();
// Test Plugin B: NO bridge binding
mf = new Miniflare({
workers: [
{
name: "plugin-b",
modules: true,
// NO service bindings - this plugin has no capabilities
script: `
export default {
async fetch(request, env) {
const hasBridge = "BRIDGE" in env;
return Response.json({ hasBridge });
}
};
`,
},
],
});
const resB = await mf.dispatchFetch("http://localhost/");
const dataB = (await resB.json()) as { hasBridge: boolean };
expect(dataB.hasBridge).toBe(false);
});
it("can load plugin code dynamically from a string", async () => {
// Test that we can pass plugin code as a string (not a file path).
// This is critical for the runtime: plugin bundles come from the DB/R2,
// not from the filesystem.
const pluginCode = `
export default {
async fetch(request, env) {
return Response.json({
pluginId: "dynamic-plugin",
version: "1.0.0",
message: "I was loaded from a string!",
});
}
};
`;
mf = new Miniflare({
workers: [
{
name: "dynamic-plugin",
modules: true,
script: pluginCode,
},
],
});
const response = await mf.dispatchFetch("http://dynamic-plugin/");
const result = (await response.json()) as { pluginId: string; message: string };
expect(result.pluginId).toBe("dynamic-plugin");
expect(result.message).toBe("I was loaded from a string!");
});
it("can use KV namespace bindings per plugin", async () => {
// Plugin with KV namespace binding
mf = new Miniflare({
kvNamespaces: ["PLUGIN_KV"],
modules: true,
script: `
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/set") {
await env.PLUGIN_KV.put("test-key", "test-value");
return new Response("set");
}
if (url.pathname === "/get") {
const val = await env.PLUGIN_KV.get("test-key");
return Response.json({ value: val });
}
return new Response("unknown", { status: 404 });
}
};
`,
});
// Set and get
await mf.dispatchFetch("http://localhost/set");
const getRes = await mf.dispatchFetch("http://localhost/get");
const getData = (await getRes.json()) as { value: string };
expect(getData.value).toBe("test-value");
await mf.dispose();
// Plugin without KV has no access
mf = new Miniflare({
modules: true,
script: `
export default {
async fetch(request, env) {
const hasKv = "PLUGIN_KV" in env;
return Response.json({ hasKv });
}
};
`,
});
const noKvRes = await mf.dispatchFetch("http://localhost/");
const noKvData = (await noKvRes.json()) as { hasKv: boolean };
expect(noKvData.hasKv).toBe(false);
});
it("can reconfigure workers without full restart (add/remove plugins)", async () => {
// Test that we can dispose and recreate miniflare with different workers.
// This simulates plugin install/uninstall.
// Start with one plugin
mf = new Miniflare({
modules: true,
script: `
export default {
async fetch() { return Response.json({ id: "original" }); }
};
`,
});
const res1 = await mf.dispatchFetch("http://localhost/");
const data1 = (await res1.json()) as { id: string };
expect(data1.id).toBe("original");
// Dispose and recreate with a different plugin
await mf.dispose();
mf = new Miniflare({
modules: true,
script: `
export default {
async fetch() { return Response.json({ id: "new-plugin" }); }
};
`,
});
const res2 = await mf.dispatchFetch("http://localhost/");
const data2 = (await res2.json()) as { id: string };
expect(data2.id).toBe("new-plugin");
});
it("external service binding to Node HTTP server works", async () => {
// Critical test: can a plugin worker call an EXTERNAL HTTP service
// (simulating the Node backing service) via a service binding?
//
// Miniflare supports `serviceBindings` with custom handler functions.
// This maps to how the Node process would expose backing services.
mf = new Miniflare({
workers: [
{
name: "plugin-with-external-bridge",
modules: true,
serviceBindings: {
BRIDGE: async (request: Request) => {
// This function runs in Node, not in workerd.
// It simulates the backing service HTTP handler.
const url = new URL(request.url);
if (url.pathname === "/content/get") {
const body = (await request.json()) as { collection: string; id: string };
return Response.json({
success: true,
data: {
id: body.id,
type: body.collection,
data: { title: "From Node backing service" },
},
});
}
return new Response("Not found", { status: 404 });
},
},
script: `
export default {
async fetch(request, env) {
const res = await env.BRIDGE.fetch("http://bridge/content/get", {
method: "POST",
body: JSON.stringify({ collection: "posts", id: "from-plugin" }),
headers: { "Content-Type": "application/json" },
});
const data = await res.json();
return Response.json(data);
}
};
`,
},
],
});
const response = await mf.dispatchFetch("http://plugin-with-external-bridge/");
const result = (await response.json()) as {
success: boolean;
data: { id: string; data: { title: string } };
};
expect(result.success).toBe(true);
expect(result.data.id).toBe("from-plugin");
expect(result.data.data.title).toBe("From Node backing service");
});
});