Skip to content

test: migrate unit tests and add custom nock compatibility helper (Part 2)#10724

Open
joehan wants to merge 1 commit into
refactor/migrate-to-native-fetch-part1from
refactor/migrate-to-native-fetch-part2
Open

test: migrate unit tests and add custom nock compatibility helper (Part 2)#10724
joehan wants to merge 1 commit into
refactor/migrate-to-native-fetch-part1from
refactor/migrate-to-native-fetch-part2

Conversation

@joehan

@joehan joehan commented Jun 25, 2026

Copy link
Copy Markdown
Member

Description

This is Part 2 of the native fetch migration. It contains the unit test suite migration and custom mock helper.

  • Implemented a dynamic, self-restoring fetch override in a new custom mock helper (src/test/helpers/nock.ts) to sandbox undici mocks and prevent them from leaking into other tests.
  • Added automatic query parameter extraction and semantic query matching to our custom helper to handle URL encoding differences (%20 vs +).
  • Re-routed globalThis.fetch to node-fetch in the test runner using a new, native JavaScript bootstrap helper (src/test/helpers/mocha-bootstrap.js) that avoids type-checking conflicts and ESM loader failures under Mocha v11.
  • Polyfilled Headers.prototype.getSetCookie on node-fetchs prototype in the test bootstrap to support multiple cookies in tests.
  • Enabled transpileOnly: true for ts-node in tsconfig.json to accelerate test compilation and bypass dependency type conflicts.
  • Migrated all unit test spec files (src/**/*.spec.ts) to use the new mock helper.

This PR is stacked on top of:

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the external nock library with a custom wrapper in src/test/helpers/nock.ts built on undici's MockAgent, updating imports across numerous test files. Feedback on these changes highlights several critical issues: stream state leakage in functionsRuntimeWorker.spec.ts due to shared mock request/response instances, multiple style guide violations regarding the use of any in the new nock.ts helper, a potential resource leak from not closing the previous mockAgent during resets, a non-deterministic time-based threshold in apiv2.spec.ts that could cause flaky tests, and a Sinon stub leak in taskQueue.spec.ts due to an uncleared stubs array.

Comment on lines +105 to +131
function mockSuccessfulRequest(statusCode: number = 200, body: string = "") {
const mockRes = new PassThrough();
(mockRes as any).statusCode = statusCode;
(mockRes as any).headers = {};

const mockReq = new PassThrough();
requestStub.callsFake((options: any, callback: any) => {
if (callback) {
process.nextTick(() => {
callback(mockRes);
mockRes.push(body);
mockRes.push(null);
});
}
return mockReq as any;
});
}

function mockFailedRequest(err: Error) {
const mockReq = new PassThrough();
requestStub.callsFake(() => {
process.nextTick(() => {
mockReq.emit("error", err);
});
return mockReq as any;
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Creating mockReq and mockRes outside of the callsFake callback causes them to be shared across all invocations of http.request, which leads to stream state leakage and potential test bugs. Instantiate them inside the callback instead, and extract the callback parameter robustly to support all http.request signatures.

  function mockSuccessfulRequest(statusCode: number = 200, body: string = "") {
    requestStub.callsFake((...args: any[]) => {
      const mockRes = new PassThrough();
      (mockRes as any).statusCode = statusCode;
      (mockRes as any).headers = {};

      const mockReq = new PassThrough();
      const callback = args[args.length - 1];
      if (typeof callback === "function") {
        process.nextTick(() => {
          callback(mockRes);
          mockRes.push(body);
          mockRes.push(null);
        });
      }
      return mockReq as any;
    });
  }

  function mockFailedRequest(err: Error) {
    requestStub.callsFake(() => {
      const mockReq = new PassThrough();
      process.nextTick(() => {
        mockReq.emit("error", err);
      });
      return mockReq as any;
    });
  }

Comment thread src/test/helpers/nock.ts Outdated
/* eslint-disable @typescript-eslint/no-namespace */
import { MockAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici";

let originalFetch: any = undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using any as an escape hatch. Type originalFetch more specifically as typeof globalThis.fetch | undefined to ensure type safety.

Suggested change
let originalFetch: any = undefined;
let originalFetch: typeof globalThis.fetch | undefined = undefined;
References
  1. TypeScript: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/test/helpers/nock.ts
Comment on lines +10 to +14
function resetMockAgent() {
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent resource leaks and dangling handles in the test runner, ensure that the previous mockAgent is closed before creating a new one.

Suggested change
function resetMockAgent() {
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
}
function resetMockAgent() {
try {
mockAgent.close();
} catch {
// Ignore errors during close
}
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
}

Comment thread src/test/helpers/nock.ts
private persistFlag = false;
private timesVal = 1;
private delayVal = 0;
private scope?: any;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using any for the scope property. Type it specifically as import("undici").MockScope to leverage proper type checking.

Suggested change
private scope?: any;
private scope?: import("undici").MockScope;
References
  1. TypeScript: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/test/helpers/nock.ts
Comment on lines +28 to +30
private clientWrapper: NockClient,
private client: any,
private options: { path: any; method: string; body?: any; options?: any },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using any for constructor parameters. Define proper types for client and options to ensure type safety.

    private clientWrapper: NockClient,
    private client: import("undici").MockClient,
    private options: {
      path: string | RegExp | PathCallback;
      method: string;
      body?: string | RegExp | Record<string, any> | BodyCallback | null;
      options?: { reqheaders?: Record<string, string> };
    },
References
  1. TypeScript: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/test/helpers/nock.ts
}

class NockClient {
private client: any;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using any for the client property. Type it specifically as import("undici").MockClient.

Suggested change
private client: any;
private client: import("undici").MockClient;
References
  1. TypeScript: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/test/helpers/nock.ts
Comment on lines +496 to +522
function deepEqual(a: any, b: any): boolean {
if (b instanceof RegExp) {
return b.test(String(a));
}
if (a instanceof RegExp) {
return a.test(String(b));
}
if (a === b) return true;
if (a && b && typeof a === "object" && typeof b === "object") {
if (a.constructor !== b.constructor) return false;
if (Array.isArray(a)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
}
const keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!deepEqual(a[key], b[key])) return false;
}
return true;
}
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Refactor deepEqual to use unknown and proper type casting/guards instead of any to adhere to the TypeScript guidelines.

function deepEqual(a: unknown, b: unknown): boolean {
  if (b instanceof RegExp) {
    return b.test(String(a));
  }
  if (a instanceof RegExp) {
    return a.test(String(b));
  }
  if (a === b) return true;
  if (a && b && typeof a === "object" && typeof b === "object") {
    if (a.constructor !== b.constructor) return false;
    if (Array.isArray(a)) {
      if (!Array.isArray(b)) return false;
      if (a.length !== b.length) return false;
      for (let i = 0; i < a.length; i++) {
        if (!deepEqual(a[i], b[i])) return false;
      }
      return true;
    }
    const objA = a as Record<string, unknown>;
    const objB = b as Record<string, unknown>;
    const keys = Object.keys(objA);
    if (keys.length !== Object.keys(objB).length) return false;
    for (const key of keys) {
      if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
      if (!deepEqual(objA[key], objB[key])) return false;
    }
    return true;
  }
  return false;
}
References
  1. TypeScript: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/test/helpers/nock.ts
Comment on lines +524 to +541
function parseBodyIfNeeded(body: any): any {
if (typeof body === "string") {
try {
return JSON.parse(body);
} catch {
return body;
}
}
if (body && (body instanceof Uint8Array || Buffer.isBuffer(body))) {
const str = new TextDecoder().decode(body);
try {
return JSON.parse(str);
} catch {
return str;
}
}
return body;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Refactor parseBodyIfNeeded to use unknown instead of any to avoid bypassing type checks.

Suggested change
function parseBodyIfNeeded(body: any): any {
if (typeof body === "string") {
try {
return JSON.parse(body);
} catch {
return body;
}
}
if (body && (body instanceof Uint8Array || Buffer.isBuffer(body))) {
const str = new TextDecoder().decode(body);
try {
return JSON.parse(str);
} catch {
return str;
}
}
return body;
}
function parseBodyIfNeeded(body: unknown): unknown {
if (typeof body === "string") {
try {
return JSON.parse(body);
} catch {
return body;
}
}
if (body && (body instanceof Uint8Array || Buffer.isBuffer(body))) {
const str = new TextDecoder().decode(body);
try {
return JSON.parse(str);
} catch {
return str;
}
}
return body;
}
References
  1. TypeScript: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment thread src/apiv2.spec.ts Outdated
Comment on lines +167 to +181
let lastPushTime = 0;
const capture = (b: unknown): boolean => {
sentBodies.push(typeof b === "string" ? b : JSON.stringify(b));
const now = Date.now();
if (now - lastPushTime > 5) {
let bodyStr = "";
if (b instanceof Uint8Array || Buffer.isBuffer(b)) {
bodyStr = Buffer.from(b).toString("utf8");
} else if (typeof b === "string") {
bodyStr = b;
} else {
bodyStr = JSON.stringify(b);
}
sentBodies.push(bodyStr);
lastPushTime = now;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Date.now() with a magic number 5ms threshold to throttle/deduplicate captures is non-deterministic and can lead to flaky tests in slow or CPU-constrained CI environments. Use a deterministic tick-based lock with process.nextTick instead.

      let lock = false;
      const capture = (b: unknown): boolean => {
        if (!lock) {
          lock = true;
          process.nextTick(() => {
            lock = false;
          });
          let bodyStr = "";
          if (b instanceof Uint8Array || Buffer.isBuffer(b)) {
            bodyStr = Buffer.from(b).toString("utf8");
          } else if (typeof b === "string") {
            bodyStr = b;
          } else {
            bodyStr = JSON.stringify(b);
          }
          sentBodies.push(bodyStr);
        }

Comment on lines 192 to 195
afterEach(() => {
dateStub.restore();
stubs.forEach((s) => s.restore());
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The stubs array is never cleared, which means stubs from previous tests accumulate and are repeatedly restored in subsequent tests. Clear the array in afterEach to prevent Sinon stub leaks and test pollution.

Suggested change
afterEach(() => {
dateStub.restore();
stubs.forEach((s) => s.restore());
});
afterEach(() => {
dateStub.restore();
stubs.forEach((s) => s.restore());
stubs.length = 0;
});

@joehan joehan force-pushed the refactor/migrate-to-native-fetch-part1 branch from 4d2c3cd to 17625de Compare June 25, 2026 22:46
@joehan joehan force-pushed the refactor/migrate-to-native-fetch-part2 branch from fd88b59 to 7b512c3 Compare June 25, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants