Unverified Commit e81b7a5d authored by Hong Minhee's avatar Hong Minhee
Browse files

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.
parent f218880b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@
    "btos",
    "callouts",
    "cfworker",
    "cfworkers",
    "codegen",
    "compactable",
    "cryptosuite",
+6 −0
Original line number Diff line number Diff line
@@ -3,6 +3,12 @@
.dnt-import-map.json
.test-report.xml
apidoc/
cfworkers/dist/
cfworkers/fixtures/
cfworkers/imports.ts
cfworkers/README.md
cfworkers/server.js
cfworkers/server.js.map
coverage/
dist/
fedify-fedify-*.tgz
+72 −0
Original line number Diff line number Diff line
import { Miniflare } from "miniflare";
import { join } from "node:path";
import process from "node:process";
import { styleText } from "node:util";

const filters = process.argv.slice(2).map((f) => f.toLowerCase());

const mf = new Miniflare({
  // @ts-ignore: scriptPath is not recognized in the type definitions
  scriptPath: join(import.meta.dirname ?? ".", "server.js"),
  modules: [
    { type: "ESModule", path: join(import.meta.dirname ?? ".", "server.js") },
  ],
  async outboundService(request: Request) {
    const url = new URL(request.url);
    if (url.hostname.endsWith(".test")) {
      const host = url.hostname.slice(0, -5);
      try {
        const { default: document } = await import(
          "../testing/fixtures/" + host + url.pathname + ".json"
        );
        return new Response(JSON.stringify(document), {
          headers: {
            "Content-Type": "application/json",
          },
        });
      } catch (e) {
        return new Response(String(e), { status: 404 });
      }
    }
    return await fetch(request);
  },
  compatibilityDate: "2025-05-23",
  compatibilityFlags: ["nodejs_compat"],
});
const url = await mf.ready;
const response = await mf.dispatchFetch(url);
const tests = await response.json() as string[];
let passed = 0;
let failed = 0;
let skipped = 0;
for (const test of tests) {
  const testLower = test.toLowerCase();
  if (filters.length > 0 && !filters.some((f) => testLower.includes(f))) {
    continue;
  }
  const resp = await mf.dispatchFetch(url, {
    method: "POST",
    body: test,
    headers: { "Content-Type": "text/plain" },
  });
  if (resp.ok) {
    console.log(styleText("green", `PASS: ${test}`));
    passed++;
  } else if (resp.status === 404) {
    console.log(styleText("yellow", `SKIP: ${test}`));
    skipped++;
  } else {
    const text = await resp.text();
    console.log(styleText("red", `FAIL: ${test}`));
    console.log(text);
    failed++;
  }
}
await mf.dispose();
console.log(
  `Tests completed: ${styleText("green", `${passed} passed`)}, ${
    styleText("red", `${failed} failed`)
  }, ${styleText("yellow", `${skipped} skipped`)}.`,
);

// cSpell: ignore Miniflare
+133 −0
Original line number Diff line number Diff line
import {
  ansiColorFormatter,
  configure,
  type LogRecord,
} from "@logtape/logtape";
import { AsyncLocalStorage } from "node:async_hooks";
// @ts-ignore: The following code is generated
import { testDefinitions } from "./dist/testing/mod.js";
// @ts-ignore: The following code is generated
import "./imports.ts";

interface TestDefinition {
  name: string;
  ignore?: boolean;
  fn: (
    // deno-lint-ignore no-explicit-any
    ctx: { name: string; origin: string; step: any },
  ) => void | Promise<void>;
}

// @ts-ignore: testDefinitions is untyped
const tests: TestDefinition[] = testDefinitions;
const logs: LogRecord[] = [];

await configure({
  sinks: {
    buffer: logs.push.bind(logs),
  },
  loggers: [
    { category: [], sinks: ["buffer"], lowestLevel: "debug" },
  ],
  contextLocalStorage: new AsyncLocalStorage(),
});

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method === "GET") {
      return new Response(
        JSON.stringify(tests.map(({ name }) => name)),
        {
          headers: { "Content-Type": "application/json" },
        },
      );
    }
    const testName = await request.text();
    for (const def of tests) {
      const { name, fn, ignore } = def;
      if (testName !== name) continue;
      if (ignore) {
        return new Response(
          "",
          {
            status: 404,
            headers: { "Content-Type": "text/plain" },
          },
        );
      }
      let failed: unknown = undefined;
      let capturedLogs: LogRecord[] | undefined = undefined;
      // deno-lint-ignore no-inner-declarations
      async function step(
        arg: string | {
          name: string;
          ignore?: boolean;
          // deno-lint-ignore no-explicit-any
          fn: (def: any) => void | Promise<void>;
          // deno-lint-ignore no-explicit-any
        } | ((ctx: any) => void | Promise<void>),
        // deno-lint-ignore no-explicit-any
        fn?: (ctx: any) => void | Promise<void>,
      ) {
        let def: {
          name: string;
          ignore?: boolean;
          // deno-lint-ignore no-explicit-any
          fn: (def: any) => void | Promise<void>;
        };
        if (typeof arg === "string") {
          def = { name: arg, fn: fn! };
        } else if (typeof arg === "function") {
          def = { name: arg.name, fn: arg };
        } else {
          def = arg;
        }
        if (def.ignore) return;
        try {
          await def.fn({
            name: def.name,
            origin: "",
            step,
          });
        } catch (e) {
          failed ??= e;
          capturedLogs ??= [...logs];
          return false;
        }
        return true;
      }
      logs.splice(0, logs.length); // Clear logs
      try {
        await fn({ name, origin: "", step });
      } catch (e) {
        failed ??= e;
      }
      capturedLogs ??= [...logs];
      if (typeof failed === "undefined") {
        return new Response(
          "",
          { status: 200, headers: { "Content-Type": "text/plain" } },
        );
      } else {
        return new Response(
          `${
            failed instanceof Error
              ? `${failed.message}\n${failed.stack ?? ""}`
              : String(failed)
          }\n${capturedLogs.map(ansiColorFormatter).join("")}`,
          {
            status: 500,
            headers: { "Content-Type": "text/plain" },
          },
        );
      }
    }
    return new Response(
      "Test not found",
      {
        status: 404,
        headers: { "Content-Type": "text/plain" },
      },
    );
  },
};
+11 −1
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@
    "asn1js": "npm:asn1js@^3.0.5",
    "byte-encodings": "npm:byte-encodings@^1.0.11",
    "fast-check": "npm:fast-check@^3.22.0",
    "fetch-mock": "npm:fetch-mock@^12.5.2",
    "json-canon": "npm:json-canon@^1.0.1",
    "jsonld": "npm:jsonld@^8.3.2",
    "multicodec": "npm:multicodec@^3.2.1",
@@ -46,6 +47,13 @@
  ],
  "exclude": [
    "apidoc/",
    "cfworkers/dist/",
    "cfworkers/fixtures/",
    "cfworkers/imports.ts",
    "cfworkers/README.md",
    "cfworkers/server.ts",
    "cfworkers/server.js",
    "cfworkers/server.js.map",
    "codegen/schema.yaml",
    "dist/",
    "node_modules/",
@@ -124,12 +132,14 @@
        "pnpm:build"
      ]
    },
    "test:cfworkers": "pnpm run test:cfworkers",
    "test-all": {
      "dependencies": [
        "check",
        "test",
        "test:node",
        "test:bun"
        "test:bun",
        "test:cfworkers"
      ]
    }
  }
Loading