Skip to content

Commit e81b7a5

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 e81b7a5

48 files changed

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

fedify/deno.json

Lines changed: 11 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,13 @@
4647
],
4748
"exclude": [
4849
"apidoc/",
50+
"cfworkers/dist/",
51+
"cfworkers/fixtures/",
52+
"cfworkers/imports.ts",
53+
"cfworkers/README.md",
54+
"cfworkers/server.ts",
55+
"cfworkers/server.js",
56+
"cfworkers/server.js.map",
4957
"codegen/schema.yaml",
5058
"dist/",
5159
"node_modules/",
@@ -124,12 +132,14 @@
124132
"pnpm:build"
125133
]
126134
},
135+
"test:cfworkers": "pnpm run test:cfworkers",
127136
"test-all": {
128137
"dependencies": [
129138
"check",
130139
"test",
131140
"test:node",
132-
"test:bun"
141+
"test:bun",
142+
"test:cfworkers"
133143
]
134144
}
135145
}

0 commit comments

Comments
 (0)