Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/pnpm-lockfile-check/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ runs:
git checkout -- pnpm-lock.yaml
pnpm --dir core/packages/gaxios run compile
pnpm --dir core/packages/teeny-request run compile
pnpm --dir core/test-utils run compile
node -e "require('./bin/run-test.cjs').ensureWorkspacePackagesLinked()"
5 changes: 5 additions & 0 deletions bin/run-test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ const WORKSPACE_LINK_PACKAGES = [
relDir: 'core/packages/teeny-request',
entryPoint: 'build/src/index.js',
},
{
name: 'google-test-utils',
relDir: 'core/test-utils',
entryPoint: 'build/src/index.js',
},
];

function linkTargetToWorkspacePackage(
Expand Down
3 changes: 2 additions & 1 deletion core/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
],
"scripts": {
"docs": "jsdoc -c .jsdoc.js",
"test": "node ../../bin/run-test.cjs --proxyquire-shim build/test",
"test": "node ../../bin/run-test.cjs build/test",
"prepare": "npm run compile",
"pretest": "npm run compile",
"compile": "tsc -p .",
Expand Down Expand Up @@ -57,6 +57,7 @@
"@types/tmp": "^0.2.6",
"c8": "^10.1.3",
"codecov": "^3.8.3",
"google-test-utils": "^0.3.0",
"gts": "^6.0.2",
"jsdoc": "^4.0.4",
"jsdoc-fresh": "^6.0.0",
Expand Down
3 changes: 2 additions & 1 deletion core/common/test/service-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import * as assert from 'assert';
import {describe, it, beforeEach, afterEach} from 'mocha';
import * as extend from 'extend';
import * as proxyquire from 'proxyquire';
import {proxyquire} from 'google-test-utils';
// eslint-disable-next-line import/namespace
import * as r from 'teeny-request';
import * as sinon from 'sinon';

Expand Down Expand Up @@ -298,7 +299,7 @@
);
assert.strictEqual((reqOpts as DecorateRequestOptions).uri, '');
done();
(callback as any)(null, null, {} as r.Response);

Check warning on line 302 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});
serviceObject.delete(assert.ifError);
});
Expand All @@ -313,7 +314,7 @@
options,
);
done();
(callback as any)(null, null, {} as r.Response);

Check warning on line 317 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});
serviceObject.delete(options, assert.ifError);
});
Expand Down Expand Up @@ -344,7 +345,7 @@
'PATCH',
);
done();
(callback as any)(null, null, null!);

Check warning on line 348 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

const serviceObject = new ServiceObject(CONFIG) as FakeServiceObject;
Expand Down Expand Up @@ -384,7 +385,7 @@
undefined,
);
done();
(callback as any)(null, null, {} as r.Response);

Check warning on line 388 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});
serviceObject.delete(options, assert.ifError);
});
Expand Down Expand Up @@ -414,7 +415,7 @@
thisPropertyWasOverridden: true,
});
done();
(callback as any)(null, null, null!);

Check warning on line 418 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

const serviceObject = new ServiceObject(CONFIG) as FakeServiceObject;
Expand Down Expand Up @@ -459,7 +460,7 @@
.callsFake((options_, callback) => {
assert.deepStrictEqual(options_, options);
done();
(callback as any)(null, null, {} as r.Response);

Check warning on line 463 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});
serviceObject.exists(options, assert.ifError);
});
Expand Down Expand Up @@ -664,7 +665,7 @@
assert.strictEqual(this, serviceObject);
assert.strictEqual((reqOpts as DecorateRequestOptions).uri, '');
done();
(callback as any)(null, null, {} as r.Response);

Check warning on line 668 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});
void serviceObject.getMetadata(() => {});
});
Expand All @@ -679,7 +680,7 @@
options,
);
done();
(callback as any)(null, null, {} as r.Response);

Check warning on line 683 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});
serviceObject.getMetadata(options, assert.ifError);
});
Expand All @@ -705,7 +706,7 @@
'v2',
);
done();
(callback as any)(null, null, null!);

Check warning on line 709 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

const serviceObject = new ServiceObject(CONFIG) as FakeServiceObject;
Expand Down Expand Up @@ -738,7 +739,7 @@
thisPropertyWasOverridden: true,
});
done();
(callback as any)(null, null, null!);

Check warning on line 742 in core/common/test/service-object.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

const serviceObject = new ServiceObject(CONFIG) as FakeServiceObject;
Expand Down
2 changes: 1 addition & 1 deletion core/common/test/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import * as assert from 'assert';
import {describe, it, before, beforeEach, after} from 'mocha';
import * as extend from 'extend';
import * as proxyquire from 'proxyquire';
import {proxyquire} from 'google-test-utils';
import {Request} from 'teeny-request';
import {AuthClient, GoogleAuth, OAuth2Client} from 'google-auth-library';

Expand Down
3 changes: 2 additions & 1 deletion core/common/test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
OAuth2Client,
} from 'google-auth-library';
import * as nock from 'nock';
import * as proxyquire from 'proxyquire';
import {proxyquire} from 'google-test-utils';
// eslint-disable-next-line import/namespace
import * as r from 'teeny-request';
import * as retryRequest from 'retry-request';
import * as sinon from 'sinon';
Expand Down
6 changes: 6 additions & 0 deletions core/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ export {
isReadOnlyPropertyError,
requiresStrictArrayPrototypeEquality,
} from './runtime';
export {
type Proxyquire,
type ProxyquireStubs,
createProxyquire,
proxyquire,
} from './proxyquire';
251 changes: 251 additions & 0 deletions core/test-utils/src/proxyquire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as Module from 'module';
import * as path from 'path';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ProxyquireStubs = Record<string, any>;

export interface Proxyquire {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<T = any>(request: string, stubs?: ProxyquireStubs): T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
load<T = any>(request: string, stubs?: ProxyquireStubs): T;
noCallThru(): Proxyquire;
callThru(): Proxyquire;
noPreserveCache(): Proxyquire;
preserveCache(): Proxyquire;
}

type ModuleConstructor = {
new (id: string, parent?: NodeModule): NodeModule;
prototype: {require: NodeRequire};
_cache?: NodeJS.Dict<NodeModule>;
_resolveFilename: (request: string, parent: NodeModule) => string;
_nodeModulePaths: (from: string) => string[];
createRequire?: (filename: string) => NodeRequire;
};

const rawMod = ((Module as unknown as {Module?: ModuleConstructor}).Module ??
Module) as unknown as ModuleConstructor;
const {hasOwnProperty} = Object.prototype;
const isDirectMod = hasOwnProperty.call(rawMod.prototype, 'require');
const mod: ModuleConstructor = isDirectMod
? rawMod
: (Object.getPrototypeOf(rawMod) as ModuleConstructor);
const modProto: {require: NodeRequire} = isDirectMod
? rawMod.prototype
: Object.getPrototypeOf(rawMod.prototype);
const isObj = (o: unknown): o is Record<PropertyKey, unknown> =>
Boolean(o && (typeof o === 'object' || typeof o === 'function'));
const hasOwn = (o: unknown, k: string): boolean =>
isObj(o) && hasOwnProperty.call(o, k);
const isGlobal = (s: unknown): boolean =>
hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal');

const resolveFrom = (base: string, req: string): string => {
try {
return require.resolve(req, {paths: [path.dirname(base)]});
} catch {
return req[0] === '.' ? path.resolve(path.dirname(base), req) : req;
}
};

function getCallerFile(): string | undefined {
const origPrepare = Error.prepareStackTrace;
try {
const err = new Error();
Error.prepareStackTrace = (_, stack) => stack;
const stack = err.stack as unknown as NodeJS.CallSite[] | undefined;
if (Array.isArray(stack)) {
for (const frame of stack) {
const file = frame.getFileName?.();
if (
file &&
file !== __filename &&
!file.startsWith('node:') &&
!file.startsWith('internal/')
) {
return file;
}
}
}
} finally {
Error.prepareStackTrace = origPrepare;
}
return undefined;
}

function resolveModule(target: NodeModule | string | undefined): NodeModule {
if (target && typeof target === 'object') {
return target;
}
const filename =
typeof target === 'string' ? target : (getCallerFile() ?? __filename);
const cached = require.cache[filename];
if (cached) {
return cached;
}
const fallback = new mod(filename);
fallback.filename = filename;
fallback.paths = mod._nodeModulePaths(path.dirname(filename));
return fallback;
}

interface CallerOptions {
skipCallThru: boolean;
keepCache: boolean;
}

/**
* Creates a runtime-agnostic `proxyquire` function compatible with both
* Node.js and Bun (`JS_RUNTIME=bun`).
*
* @param caller Optional calling `NodeModule` or `__filename` used to resolve
* relative module paths. When omitted, the caller's filename is detected
* automatically from the call stack.
*/
export function createProxyquire(caller?: NodeModule | string): Proxyquire {
const defaultOpts: CallerOptions = {skipCallThru: false, keepCache: true};
const callerOpts = new Map<string, CallerOptions>();

const getOpts = (callerFilename: string): CallerOptions => {
if (caller !== undefined) {
return defaultOpts;
}
let opts = callerOpts.get(callerFilename);
if (!opts) {
opts = {...defaultOpts};
callerOpts.set(callerFilename, opts);
}
return opts;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fn = function proxyquire<T = any>(
request: string,
stubs: ProxyquireStubs = {},
): T {
const parent = resolveModule(caller);
const opts = getOpts(parent.filename);
const sut = mod._resolveFilename(request, parent);
const resolved: ProxyquireStubs = {};
let global = false;
for (const [k, v] of Object.entries(stubs)) {
if (isGlobal(v)) global = true;
resolved[resolveFrom(sut, k)] = v;
}
const cache = mod._cache ?? require.cache;
const keys = global
? Object.keys(cache).filter(id => !id.endsWith('.node'))
: [sut];
const saved = keys.map(id => [id, cache[id]] as const);
for (const id of keys) delete cache[id];
const prev = modProto.require;
const req = function (this: NodeModule, id: string) {
const isSut = this?.filename === sut;
if (this?.filename && (isSut || global)) {
const hit = (isSut || !id.startsWith('.')) && hasOwn(stubs, id);
const rId = hit ? '' : resolveFrom(this.filename, id);
if (hit || hasOwn(resolved, rId)) {
const stub = hit ? stubs[id] : resolved[rId];
if (isSut || isGlobal(stub)) {
if (stub === null) {
const e = new Error(`Cannot find module '${id}'`);
throw Object.assign(e, {code: 'MODULE_NOT_FOUND'});
}
const skip = hasOwn(stub, '@noCallThru')
? Boolean(stub['@noCallThru'])
: opts.skipCallThru;
if (!skip && isObj(stub)) {
try {
const real = prev.call(this, id);
if (isObj(real)) {
return new Proxy(stub, {
get(target, prop) {
if (prop in target) {
return Reflect.get(target, prop);
}
return Reflect.get(real, prop);
},
has(target, prop) {
return prop in target || prop in real;
},
ownKeys(target) {
return Array.from(
new Set([
...Reflect.ownKeys(target),
...Reflect.ownKeys(real),
]),
);
},
getOwnPropertyDescriptor(target, prop) {
const desc =
Reflect.getOwnPropertyDescriptor(target, prop) ||
Reflect.getOwnPropertyDescriptor(real, prop);
return desc ? {...desc, configurable: true} : undefined;
},
});
}
} catch {
// ignore when underlying module cannot be loaded
}
}
return stub;
}
}
}
return prev.call(this, id);
};
modProto.require = Object.assign(req, prev);
try {
return typeof parent.require === 'function' && parent instanceof mod
? prev.call(parent, request)
: mod.createRequire
? mod.createRequire(parent.filename)(request)
: prev.call(parent, request);
} finally {
modProto.require = prev;
for (const id of global ? Object.keys(cache) : [sut]) {
if (!id.endsWith('.node')) delete cache[id];
}
for (const [id, m] of saved) {
if (m && (opts.keepCache || id !== sut)) cache[id] = m;
}
}
};

const updateOpts = (patch: Partial<CallerOptions>): Proxyquire => {
const parent = resolveModule(caller);
Object.assign(getOpts(parent.filename), patch);
return instance;
};

const instance: Proxyquire = Object.assign(fn, {
load: fn,
noCallThru: () => updateOpts({skipCallThru: true}),
callThru: () => updateOpts({skipCallThru: false}),
noPreserveCache: () => updateOpts({keepCache: false}),
preserveCache: () => updateOpts({keepCache: true}),
});

return instance;
}

/**
* Shared `proxyquire` instance that resolves relative module requests and
* option flags (`noCallThru`, `noPreserveCache`, etc.) per calling test file.
*/
export const proxyquire: Proxyquire = createProxyquire();
Loading
Loading