From 64bcda012c297d121d7a4bbc1dace6ad0cfe96d2 Mon Sep 17 00:00:00 2001 From: Jae-Hyuk-Jang Date: Fri, 7 Aug 2026 21:07:52 +0900 Subject: [PATCH 1/2] Add an async context data test for @fedify/express integrateFederation() accepts a contextDataFactory that may return either a plain value or a Promise, but only the synchronous path was covered. Add a test that verifies the middleware waits for an async factory to resolve and passes the resolved value to federation.fetch(). Also wire up the test script/task in package.json and deno.json so mise run test:node and test-each actually run it. Changelog: none Assisted-by: Claude Code:claude-sonnet-5 --- packages/express/deno.json | 3 +- packages/express/package.json | 3 +- packages/express/src/index.test.ts | 90 ++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/express/src/index.test.ts diff --git a/packages/express/deno.json b/packages/express/deno.json index c43bb9422..d24d5f86e 100644 --- a/packages/express/deno.json +++ b/packages/express/deno.json @@ -19,6 +19,7 @@ ] }, "tasks": { - "check": "deno fmt --check && deno lint && deno check *.ts" + "check": "deno fmt --check && deno lint && deno check *.ts", + "test": "deno test --allow-all" } } diff --git a/packages/express/package.json b/packages/express/package.json index 0701aec5c..174c5f2e2 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -58,6 +58,7 @@ "build:self": "tsdown", "build": "pnpm --filter @fedify/express... run build:self", "prepack": "pnpm build", - "prepublish": "pnpm build" + "prepublish": "pnpm build", + "test": "node --experimental-transform-types --test" } } diff --git a/packages/express/src/index.test.ts b/packages/express/src/index.test.ts new file mode 100644 index 000000000..529355034 --- /dev/null +++ b/packages/express/src/index.test.ts @@ -0,0 +1,90 @@ +import { strict as assert } from "node:assert"; +import { describe, test } from "node:test"; +import type { Request as ERequest, Response as EResponse } from "express"; +import { integrateFederation } from "./index.ts"; + +interface MockFederation { + fetch(request: Request, options: unknown): Promise; +} + +function createMockRequest(): ERequest { + return { + protocol: "http", + host: "localhost", + url: "/", + method: "GET", + headers: {}, + } as unknown as ERequest; +} + +function createMockResponse(): { + response: EResponse; + ended: Promise; + getBody(): string; +} { + let body = ""; + let resolveEnded: () => void; + const ended = new Promise((resolve) => { + resolveEnded = resolve; + }); + const response = { + statusCode: 200, + status(code: number) { + response.statusCode = code; + return response; + }, + setHeader() { + return response; + }, + removeHeader() { + return response; + }, + write(chunk: Buffer | string) { + body += chunk.toString(); + return true; + }, + end() { + resolveEnded(); + return response; + }, + send() { + return response; + }, + json() { + return response; + }, + }; + return { + response: response as unknown as EResponse, + ended, + getBody: () => body, + }; +} + +describe("integrateFederation()", () => { + test("waits for an async contextDataFactory and passes the resolved value to federation.fetch()", async () => { + const mockFederation: MockFederation = { + fetch(_request, options) { + const { contextData } = options as { contextData: unknown }; + return Promise.resolve(new Response(String(contextData))); + }, + }; + const contextDataFactory = () => Promise.resolve("Hello World"); + const middleware = integrateFederation( + mockFederation as never, + contextDataFactory, + ); + + const req = createMockRequest(); + const { response, ended, getBody } = createMockResponse(); + let nextCalled = false; + + middleware(req, response, () => { + nextCalled = true; + }); + await ended; + + assert.strictEqual(nextCalled, false); + assert.strictEqual(getBody(), "Hello World"); + }); +}); From a59ed13ea73d5f58693b0e3f2b41b599b854bcfe Mon Sep 17 00:00:00 2001 From: Jae-Hyuk-Jang Date: Fri, 7 Aug 2026 21:45:14 +0900 Subject: [PATCH 2/2] Verify federation.fetch() waits for the context data promise The previous test only checked the resolved value, not that federation.fetch() stays uncalled until the async contextDataFactory promise resolves. Use a deferred promise to assert the ordering directly, per CodeRabbit's review on #985. Changelog: none Assisted-by: Claude Code:claude-sonnet-5 --- packages/express/src/index.test.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/express/src/index.test.ts b/packages/express/src/index.test.ts index 529355034..8d94a05d2 100644 --- a/packages/express/src/index.test.ts +++ b/packages/express/src/index.test.ts @@ -1,6 +1,6 @@ +import type { Request as ERequest, Response as EResponse } from "express"; import { strict as assert } from "node:assert"; import { describe, test } from "node:test"; -import type { Request as ERequest, Response as EResponse } from "express"; import { integrateFederation } from "./index.ts"; interface MockFederation { @@ -63,13 +63,22 @@ function createMockResponse(): { describe("integrateFederation()", () => { test("waits for an async contextDataFactory and passes the resolved value to federation.fetch()", async () => { + let resolveContextData!: (value: string) => void; + let fetchCalled = false; + const mockFederation: MockFederation = { fetch(_request, options) { + fetchCalled = true; const { contextData } = options as { contextData: unknown }; return Promise.resolve(new Response(String(contextData))); }, }; - const contextDataFactory = () => Promise.resolve("Hello World"); + + const contextDataFactory = () => + new Promise((resolve) => { + resolveContextData = resolve; + }); + const middleware = integrateFederation( mockFederation as never, contextDataFactory, @@ -82,6 +91,11 @@ describe("integrateFederation()", () => { middleware(req, response, () => { nextCalled = true; }); + + await Promise.resolve(); + assert.strictEqual(fetchCalled, false); + + resolveContextData("Hello World"); await ended; assert.strictEqual(nextCalled, false);