Skip to content

Commit

Permalink
Add additional tests for proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Jul 25, 2023
1 parent dfb1e5b commit d45205d
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 5 deletions.
34 changes: 33 additions & 1 deletion packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,38 @@ export class Miniflare {
}
}

// For testing proxy client serialisation, add an API that just returns its
// arguments. Note without the `.pipeThrough(new TransformStream())` below,
// we'll see `TypeError: Inter-TransformStream ReadableStream.pipeTo() is
// not implemented.`. `IdentityTransformStream` doesn't work here.
// TODO(soon): add support for wrapped bindings and remove this. The API
// will probably look something like `{ wrappedBindings: { A: "a" } }`
// where `"a"` is the name of a "worker" in `workers`.
const extensions: Extension[] = [
{
modules: [
{
name: "miniflare-internal:identity",
internal: true, // Not accessible to user code
esModule: `
class Identity {
async asyncIdentity(...args) {
const i = args.findIndex((arg) => arg instanceof ReadableStream);
if (i !== -1) args[i] = args[i].pipeThrough(new TransformStream());
return args;
}
}
export default function() { return new Identity(); }
`,
},
],
},
];
proxyBindings.push({
name: "IDENTITY",
wrapped: { moduleName: "miniflare-internal:identity" },
});

const globalServices = getGlobalServices({
sharedOptions: sharedOpts.core,
allWorkerRoutes,
Expand All @@ -915,7 +947,7 @@ export class Miniflare {
services.set(service.name, service);
}

return { services: Array.from(services.values()), sockets };
return { services: Array.from(services.values()), sockets, extensions };
}

async #assembleAndUpdateConfig() {
Expand Down
2 changes: 1 addition & 1 deletion packages/miniflare/src/runtime/config/workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface Config {
services?: Service[];
sockets?: Socket[];
v8Flags?: string[];
extension?: Extension[];
extensions?: Extension[];
}

export type Socket = {
Expand Down
157 changes: 154 additions & 3 deletions packages/miniflare/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// noinspection TypeScriptValidateJSTypes

import assert from "assert";
import fs from "fs/promises";
import http from "http";
import { AddressInfo } from "net";
import path from "path";
import { Writable } from "stream";
import test from "ava";
import util from "util";
import test, { ThrowsExpectation } from "ava";
import {
DeferredPromise,
MessageEvent,
Expand All @@ -13,13 +18,14 @@ import {
_transformsForContentEncoding,
createFetchMock,
fetch,
viewToBuffer,
} from "miniflare";
import {
CloseEvent as StandardCloseEvent,
MessageEvent as StandardMessageEvent,
WebSocketServer,
} from "ws";
import { TestLog, useServer } from "./test-shared";
import { TestLog, useServer, useTmp, utf8Encode } from "./test-shared";

test("Miniflare: validates options", async (t) => {
// Check empty workers array rejected
Expand Down Expand Up @@ -67,6 +73,7 @@ test("Miniflare: keeps port between updates", async (t) => {
})`,
};
const mf = new Miniflare(opts);
t.teardown(() => mf.dispose());
const initialURL = await mf.ready;

await mf.setOptions(opts);
Expand Down Expand Up @@ -96,6 +103,7 @@ test("Miniflare: routes to multiple workers with fallback", async (t) => {
],
};
const mf = new Miniflare(opts);
t.teardown(() => mf.dispose());

// Check "a"'s more specific route checked first
let res = await mf.dispatchFetch("http://localhost/api");
Expand Down Expand Up @@ -363,6 +371,7 @@ test("Miniflare: custom outbound service", async (t) => {
},
],
});
t.teardown(() => mf.dispose());
const res = await mf.dispatchFetch("http://localhost");
t.deepEqual(await res.json(), {
res1: "one",
Expand All @@ -385,6 +394,7 @@ test("Miniflare: fetch mocking", async (t) => {
}`,
fetchMock,
});
t.teardown(() => mf.dispose());
const res = await mf.dispatchFetch("http://localhost");
t.is(await res.text(), "Mocked response!");

Expand Down Expand Up @@ -417,6 +427,7 @@ test("Miniflare: custom upstream as origin", async (t) => {
}
}`,
});
t.teardown(() => mf.dispose());
// Check rewrites protocol, hostname, and port, but keeps pathname and query
const res = await mf.dispatchFetch("https://random:0/path?a=1");
t.is(await res.text(), "upstream: http://upstream/extra/path?a=1");
Expand All @@ -438,6 +449,7 @@ test("Miniflare: `node:` and `cloudflare:` modules", async (t) => {
}
`,
});
t.teardown(() => mf.dispose());
const res = await mf.dispatchFetch("http://localhost");
t.is(await res.text(), "dGVzdA==");
});
Expand All @@ -462,6 +474,7 @@ test("Miniflare: modules in sub-directories", async (t) => {
},
],
});
t.teardown(() => mf.dispose());
const res = await mf.dispatchFetch("http://localhost");
t.is(await res.text(), "123");
});
Expand All @@ -475,11 +488,12 @@ test("Miniflare: HTTPS fetches using browser CA certificates", async (t) => {
}
}`,
});
t.teardown(() => mf.dispose());
const res = await mf.dispatchFetch("http://localhost");
t.true(res.ok);
});

test("Miniflare: Accepts https requests", async (t) => {
test("Miniflare: accepts https requests", async (t) => {
const log = new TestLog(t);

const mf = new Miniflare({
Expand All @@ -492,9 +506,146 @@ test("Miniflare: Accepts https requests", async (t) => {
}
}`,
});
t.teardown(() => mf.dispose());

const res = await mf.dispatchFetch("https://localhost");
t.true(res.ok);

t.assert(log.logs[0][1].startsWith("Ready on https://"));
});

test("Miniflare: getBindings() returns all bindings", async (t) => {
const tmp = await useTmp(t);
const blobPath = path.join(tmp, "blob.txt");
await fs.writeFile(blobPath, "blob");
const mf = new Miniflare({
modules: true,
script: `
export class DurableObject {}
export default { fetch() { return new Response(null, { status: 404 }); } }
`,
bindings: { STRING: "hello", OBJECT: { a: 1, b: { c: 2 } } },
textBlobBindings: { TEXT: blobPath },
dataBlobBindings: { DATA: blobPath },
serviceBindings: { SELF: "" },
d1Databases: ["DB"],
durableObjects: { DO: "DurableObject" },
kvNamespaces: ["KV"],
queueProducers: ["QUEUE"],
r2Buckets: ["BUCKET"],
});
t.teardown(() => mf.dispose());
const bindings = await mf.getBindings();

t.like(bindings, {
STRING: "hello",
OBJECT: { a: 1, b: { c: 2 } },
TEXT: "blob",
});
t.deepEqual(bindings.DATA, viewToBuffer(utf8Encode("blob")));

const opts: util.InspectOptions = { colors: false };
t.regex(util.inspect(bindings.SELF, opts), /name: 'Fetcher'/);
t.regex(util.inspect(bindings.DB, opts), /name: 'D1Database'/);
t.regex(util.inspect(bindings.DO, opts), /name: 'DurableObjectNamespace'/);
t.regex(util.inspect(bindings.KV, opts), /name: 'KvNamespace'/);
t.regex(util.inspect(bindings.QUEUE, opts), /name: 'WorkerQueue'/);
t.regex(util.inspect(bindings.BUCKET, opts), /name: 'R2Bucket'/);

// Check with WebAssembly binding (aren't supported by modules workers)
// (base64 encoded module containing a single `add(i32, i32): i32` export)
const addWasmModule =
"AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAACgkBBwAgACABagsACgRuYW1lAgMBAAA=";
const addWasmPath = path.join(tmp, "add.wasm");
await fs.writeFile(addWasmPath, Buffer.from(addWasmModule, "base64"));
await mf.setOptions({
script:
'addEventListener("fetch", (event) => event.respondWith(new Response(null, { status: 404 })));',
wasmBindings: { ADD: addWasmPath },
});
const { ADD } = await mf.getBindings<{ ADD: WebAssembly.Module }>();
const instance = new WebAssembly.Instance(ADD);
assert(typeof instance.exports.add === "function");
t.is(instance.exports.add(1, 2), 3);
});
test("Miniflare: getBindings() and friends return bindings for different workers", async (t) => {
const mf = new Miniflare({
workers: [
{
name: "a",
modules: true,
script: `
export class DurableObject {}
export default { fetch() { return new Response(null, { status: 404 }); } }
`,
d1Databases: ["DB"],
durableObjects: { DO: "DurableObject" },
},
{
// 2nd worker unnamed, to validate that not specifying a name when
// getting bindings gives the entrypoint, not the unnamed worker
script:
'addEventListener("fetch", (event) => event.respondWith(new Response(null, { status: 404 })));',
kvNamespaces: ["KV"],
queueProducers: ["QUEUE"],
},
{
name: "b",
script:
'addEventListener("fetch", (event) => event.respondWith(new Response(null, { status: 404 })));',
r2Buckets: ["BUCKET"],
},
],
});
t.teardown(() => mf.dispose());

// Check `getBindings()`
let bindings = await mf.getBindings();
t.deepEqual(Object.keys(bindings), ["DB", "DO"]);
bindings = await mf.getBindings("");
t.deepEqual(Object.keys(bindings), ["KV", "QUEUE"]);
bindings = await mf.getBindings("b");
t.deepEqual(Object.keys(bindings), ["BUCKET"]);
await t.throwsAsync(() => mf.getBindings("c"), {
instanceOf: TypeError,
message: '"c" worker not found',
});

const unboundExpectations = (name: string): ThrowsExpectation<TypeError> => ({
instanceOf: TypeError,
message: `"${name}" unbound in "c" worker`,
});

// Check `getD1Database()`
let binding: unknown = await mf.getD1Database("DB");
t.not(binding, undefined);
let expectations = unboundExpectations("DB");
await t.throwsAsync(() => mf.getD1Database("DB", "c"), expectations);

// Check `getDurableObjectNamespace()`
binding = await mf.getDurableObjectNamespace("DO");
t.not(binding, undefined);
expectations = unboundExpectations("DO");
await t.throwsAsync(
() => mf.getDurableObjectNamespace("DO", "c"),
expectations
);

// Check `getKVNamespace()`
binding = await mf.getKVNamespace("KV", "");
t.not(binding, undefined);
expectations = unboundExpectations("KV");
await t.throwsAsync(() => mf.getKVNamespace("KV", "c"), expectations);

// Check `getQueueProducer()`
binding = await mf.getQueueProducer("QUEUE", "");
t.not(binding, undefined);
expectations = unboundExpectations("QUEUE");
await t.throwsAsync(() => mf.getQueueProducer("QUEUE", "c"), expectations);

// Check `getR2Bucket()`
binding = await mf.getR2Bucket("BUCKET", "b");
t.not(binding, undefined);
expectations = unboundExpectations("BUCKET");
await t.throwsAsync(() => mf.getQueueProducer("BUCKET", "c"), expectations);
});
Loading

0 comments on commit d45205d

Please sign in to comment.