test: migrate unit tests and add custom nock compatibility helper (Part 2)#10724
test: migrate unit tests and add custom nock compatibility helper (Part 2)#10724joehan wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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.
| 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; | ||
| }); | ||
| } |
There was a problem hiding this comment.
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;
});
}| /* eslint-disable @typescript-eslint/no-namespace */ | ||
| import { MockAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici"; | ||
|
|
||
| let originalFetch: any = undefined; |
There was a problem hiding this comment.
Avoid using any as an escape hatch. Type originalFetch more specifically as typeof globalThis.fetch | undefined to ensure type safety.
| let originalFetch: any = undefined; | |
| let originalFetch: typeof globalThis.fetch | undefined = undefined; |
References
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| function resetMockAgent() { | ||
| mockAgent = new MockAgent(); | ||
| mockAgent.disableNetConnect(); | ||
| setGlobalDispatcher(mockAgent); | ||
| } |
There was a problem hiding this comment.
To prevent resource leaks and dangling handles in the test runner, ensure that the previous mockAgent is closed before creating a new one.
| 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); | |
| } |
| private persistFlag = false; | ||
| private timesVal = 1; | ||
| private delayVal = 0; | ||
| private scope?: any; |
There was a problem hiding this comment.
Avoid using any for the scope property. Type it specifically as import("undici").MockScope to leverage proper type checking.
| private scope?: any; | |
| private scope?: import("undici").MockScope; |
References
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| private clientWrapper: NockClient, | ||
| private client: any, | ||
| private options: { path: any; method: string; body?: any; options?: any }, |
There was a problem hiding this comment.
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
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| } | ||
|
|
||
| class NockClient { | ||
| private client: any; |
There was a problem hiding this comment.
Avoid using any for the client property. Type it specifically as import("undici").MockClient.
| private client: any; | |
| private client: import("undici").MockClient; |
References
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| 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; | ||
| } |
There was a problem hiding this comment.
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
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| 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; | ||
| } |
There was a problem hiding this comment.
Refactor parseBodyIfNeeded to use unknown instead of any to avoid bypassing type checks.
| 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
- TypeScript: Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
| 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; | ||
| } |
There was a problem hiding this comment.
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);
}| afterEach(() => { | ||
| dateStub.restore(); | ||
| stubs.forEach((s) => s.restore()); | ||
| }); |
There was a problem hiding this comment.
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.
| afterEach(() => { | |
| dateStub.restore(); | |
| stubs.forEach((s) => s.restore()); | |
| }); | |
| afterEach(() => { | |
| dateStub.restore(); | |
| stubs.forEach((s) => s.restore()); | |
| stubs.length = 0; | |
| }); |
4d2c3cd to
17625de
Compare
fd88b59 to
7b512c3
Compare
Description
This is Part 2 of the native fetch migration. It contains the unit test suite migration and custom mock helper.
src/test/helpers/nock.ts) to sandboxundicimocks and prevent them from leaking into other tests.%20vs+).globalThis.fetchtonode-fetchin 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.Headers.prototype.getSetCookieonnode-fetchs prototype in the test bootstrap to support multiple cookies in tests.transpileOnly: trueforts-nodeintsconfig.jsonto accelerate test compilation and bypass dependency type conflicts.src/**/*.spec.ts) to use the new mock helper.This PR is stacked on top of: