Skip to content

Commit f8893be

Browse files
committed
Cloudflare Workers test support with Miniflare
Implement comprehensive test environment setup for Cloudflare Workers using Miniflare, enabling cross-runtime testing compatibility. Add new cfworkers directory with client/server test infrastructure and update existing test files to use fetch-mock for better portability.
1 parent f218880 commit f8893be

48 files changed

Lines changed: 1394 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"btos",
1414
"callouts",
1515
"cfworker",
16+
"cfworkers",
1617
"codegen",
1718
"compactable",
1819
"cryptosuite",

fedify/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
.dnt-import-map.json
44
.test-report.xml
55
apidoc/
6+
cfworkers/dist/
7+
cfworkers/fixtures/
8+
cfworkers/imports.ts
9+
cfworkers/README.md
10+
cfworkers/server.js
11+
cfworkers/server.js.map
612
coverage/
713
dist/
814
fedify-fedify-*.tgz

fedify/cfworkers/client.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Miniflare } from "miniflare";
2+
import { join } from "node:path";
3+
import process from "node:process";
4+
import { styleText } from "node:util";
5+
6+
const filters = process.argv.slice(2).map((f) => f.toLowerCase());
7+
8+
const mf = new Miniflare({
9+
// @ts-ignore: scriptPath is not recognized in the type definitions
10+
scriptPath: join(import.meta.dirname ?? ".", "server.js"),
11+
modules: [
12+
{ type: "ESModule", path: join(import.meta.dirname ?? ".", "server.js") },
13+
],
14+
async outboundService(request: Request) {
15+
const url = new URL(request.url);
16+
if (url.hostname.endsWith(".test")) {
17+
const host = url.hostname.slice(0, -5);
18+
try {
19+
const { default: document } = await import(
20+
"../testing/fixtures/" + host + url.pathname + ".json"
21+
);
22+
return new Response(JSON.stringify(document), {
23+
headers: {
24+
"Content-Type": "application/json",
25+
},
26+
});
27+
} catch (e) {
28+
return new Response(String(e), { status: 404 });
29+
}
30+
}
31+
return await fetch(request);
32+
},
33+
compatibilityDate: "2025-05-23",
34+
compatibilityFlags: ["nodejs_compat"],
35+
});
36+
const url = await mf.ready;
37+
const response = await mf.dispatchFetch(url);
38+
const tests = await response.json() as string[];
39+
let passed = 0;
40+
let failed = 0;
41+
let skipped = 0;
42+
for (const test of tests) {
43+
const testLower = test.toLowerCase();
44+
if (filters.length > 0 && !filters.some((f) => testLower.includes(f))) {
45+
continue;
46+
}
47+
const resp = await mf.dispatchFetch(url, {
48+
method: "POST",
49+
body: test,
50+
headers: { "Content-Type": "text/plain" },
51+
});
52+
if (resp.ok) {
53+
console.log(styleText("green", `PASS: ${test}`));
54+
passed++;
55+
} else if (resp.status === 404) {
56+
console.log(styleText("yellow", `SKIP: ${test}`));
57+
skipped++;
58+
} else {
59+
const text = await resp.text();
60+
console.log(styleText("red", `FAIL: ${test}`));
61+
console.log(text);
62+
failed++;
63+
}
64+
}
65+
await mf.dispose();
66+
console.log(
67+
`Tests completed: ${styleText("green", `${passed} passed`)}, ${
68+
styleText("red", `${failed} failed`)
69+
}, ${styleText("yellow", `${skipped} skipped`)}.`,
70+
);
71+
72+
// cSpell: ignore Miniflare

fedify/cfworkers/server.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {
2+
ansiColorFormatter,
3+
configure,
4+
type LogRecord,
5+
} from "@logtape/logtape";
6+
import { AsyncLocalStorage } from "node:async_hooks";
7+
// @ts-ignore: The following code is generated
8+
import { testDefinitions } from "./dist/testing/mod.js";
9+
import "./imports.ts";
10+
11+
interface TestDefinition {
12+
name: string;
13+
ignore?: boolean;
14+
fn: (
15+
// deno-lint-ignore no-explicit-any
16+
ctx: { name: string; origin: string; step: any },
17+
) => void | Promise<void>;
18+
}
19+
20+
// @ts-ignore: testDefinitions is untyped
21+
const tests: TestDefinition[] = testDefinitions;
22+
const logs: LogRecord[] = [];
23+
24+
await configure({
25+
sinks: {
26+
buffer: logs.push.bind(logs),
27+
},
28+
loggers: [
29+
{ category: [], sinks: ["buffer"], lowestLevel: "debug" },
30+
],
31+
contextLocalStorage: new AsyncLocalStorage(),
32+
});
33+
34+
export default {
35+
async fetch(request: Request): Promise<Response> {
36+
if (request.method === "GET") {
37+
return new Response(
38+
JSON.stringify(tests.map(({ name }) => name)),
39+
{
40+
headers: { "Content-Type": "application/json" },
41+
},
42+
);
43+
}
44+
const testName = await request.text();
45+
for (const def of tests) {
46+
const { name, fn, ignore } = def;
47+
if (testName !== name) continue;
48+
if (ignore) {
49+
return new Response(
50+
"",
51+
{
52+
status: 404,
53+
headers: { "Content-Type": "text/plain" },
54+
},
55+
);
56+
}
57+
let failed: unknown = undefined;
58+
let capturedLogs: LogRecord[] | undefined = undefined;
59+
// deno-lint-ignore no-inner-declarations
60+
async function step(
61+
arg: string | {
62+
name: string;
63+
ignore?: boolean;
64+
// deno-lint-ignore no-explicit-any
65+
fn: (def: any) => void | Promise<void>;
66+
// deno-lint-ignore no-explicit-any
67+
} | ((ctx: any) => void | Promise<void>),
68+
// deno-lint-ignore no-explicit-any
69+
fn?: (ctx: any) => void | Promise<void>,
70+
) {
71+
let def: {
72+
name: string;
73+
ignore?: boolean;
74+
// deno-lint-ignore no-explicit-any
75+
fn: (def: any) => void | Promise<void>;
76+
};
77+
if (typeof arg === "string") {
78+
def = { name: arg, fn: fn! };
79+
} else if (typeof arg === "function") {
80+
def = { name: arg.name, fn: arg };
81+
} else {
82+
def = arg;
83+
}
84+
if (def.ignore) return;
85+
try {
86+
await def.fn({
87+
name: def.name,
88+
origin: "",
89+
step,
90+
});
91+
} catch (e) {
92+
failed ??= e;
93+
capturedLogs ??= [...logs];
94+
return false;
95+
}
96+
return true;
97+
}
98+
logs.splice(0, logs.length); // Clear logs
99+
try {
100+
await fn({ name, origin: "", step });
101+
} catch (e) {
102+
failed ??= e;
103+
}
104+
capturedLogs ??= [...logs];
105+
if (typeof failed === "undefined") {
106+
return new Response(
107+
"",
108+
{ status: 200, headers: { "Content-Type": "text/plain" } },
109+
);
110+
} else {
111+
return new Response(
112+
`${
113+
failed instanceof Error
114+
? `${failed.message}\n${failed.stack ?? ""}`
115+
: String(failed)
116+
}\n${capturedLogs.map(ansiColorFormatter).join("")}`,
117+
{
118+
status: 500,
119+
headers: { "Content-Type": "text/plain" },
120+
},
121+
);
122+
}
123+
}
124+
return new Response(
125+
"Test not found",
126+
{
127+
status: 404,
128+
headers: { "Content-Type": "text/plain" },
129+
},
130+
);
131+
},
132+
};

fedify/deno.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"asn1js": "npm:asn1js@^3.0.5",
3333
"byte-encodings": "npm:byte-encodings@^1.0.11",
3434
"fast-check": "npm:fast-check@^3.22.0",
35+
"fetch-mock": "npm:fetch-mock@^12.5.2",
3536
"json-canon": "npm:json-canon@^1.0.1",
3637
"jsonld": "npm:jsonld@^8.3.2",
3738
"multicodec": "npm:multicodec@^3.2.1",
@@ -46,6 +47,12 @@
4647
],
4748
"exclude": [
4849
"apidoc/",
50+
"cfworkers/dist/",
51+
"cfworkers/fixtures/",
52+
"cfworkers/imports.ts",
53+
"cfworkers/README.md",
54+
"cfworkers/server.js",
55+
"cfworkers/server.js.map",
4956
"codegen/schema.yaml",
5057
"dist/",
5158
"node_modules/",
@@ -124,12 +131,14 @@
124131
"pnpm:build"
125132
]
126133
},
134+
"test:cfworkers": "pnpm run test:cfworkers",
127135
"test-all": {
128136
"dependencies": [
129137
"check",
130138
"test",
131139
"test:node",
132-
"test:bun"
140+
"test:bun",
141+
"test:cfworkers"
133142
]
134143
}
135144
}

0 commit comments

Comments
 (0)