diff --git a/core/common/package.json b/core/common/package.json index 942fc0bde525..77fc610f483f 100644 --- a/core/common/package.json +++ b/core/common/package.json @@ -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 .", diff --git a/core/common/test/proxyquire.ts b/core/common/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/core/common/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/core/common/test/service-object.ts b/core/common/test/service-object.ts index 1d3ed35a5fee..e840224c6edc 100644 --- a/core/common/test/service-object.ts +++ b/core/common/test/service-object.ts @@ -20,7 +20,8 @@ import { import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; +// eslint-disable-next-line import/namespace import * as r from 'teeny-request'; import * as sinon from 'sinon'; diff --git a/core/common/test/service.ts b/core/common/test/service.ts index 768e7db4e4a6..74d6adb779c3 100644 --- a/core/common/test/service.ts +++ b/core/common/test/service.ts @@ -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 * as proxyquire from './proxyquire'; import {Request} from 'teeny-request'; import {AuthClient, GoogleAuth, OAuth2Client} from 'google-auth-library'; diff --git a/core/common/test/util.ts b/core/common/test/util.ts index 6c018afd4d3d..617a02e2c7ee 100644 --- a/core/common/test/util.ts +++ b/core/common/test/util.ts @@ -26,7 +26,8 @@ import { OAuth2Client, } from 'google-auth-library'; import * as nock from 'nock'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; +// eslint-disable-next-line import/namespace import * as r from 'teeny-request'; import * as retryRequest from 'retry-request'; import * as sinon from 'sinon'; diff --git a/core/packages/gax/package.json b/core/packages/gax/package.json index 7c637b1ba20a..96e47deed478 100644 --- a/core/packages/gax/package.json +++ b/core/packages/gax/package.json @@ -65,7 +65,7 @@ "scripts": { "docs": "jsdoc -c .jsdoc.js", "pretest": "pnpm run compile", - "test": "node ../../../bin/run-test.cjs --proxyquire-shim build/test/unit", + "test": "node ../../../bin/run-test.cjs build/test/unit", "lint": "gts check src samples test", "clean": "gts clean", "compile": "node ./scripts/copy-protos.js && tsc -p . && cp src/*.json build/src && cp -r test/fixtures build/test && cp -r protos build/", diff --git a/core/packages/gax/test/unit/grpc.ts b/core/packages/gax/test/unit/grpc.ts index 76f4207053b9..cf2662eeb2c1 100644 --- a/core/packages/gax/test/unit/grpc.ts +++ b/core/packages/gax/test/unit/grpc.ts @@ -20,9 +20,8 @@ import assert from 'assert'; import * as path from 'path'; -import proxyquire from 'proxyquire'; import * as sinon from 'sinon'; -import {mkdirSync, writeFileSync} from 'fs'; +import fs, {mkdirSync, writeFileSync} from 'fs'; import * as fsp from 'fs/promises'; import {afterEach, describe, it, beforeEach} from 'mocha'; @@ -613,28 +612,28 @@ describe('grpc', () => { const originPath = path.join('test', 'path', 'location'); const includePath = path.join('example', 'import.proto'); + afterEach(() => { + sinon.restore(); + }); + it('should throw an error if a file is not found', () => { - const findIncludePath = proxyquire('../../src/grpc', { - fs: { - existsSync: () => { - return false; - }, - }, - }).GoogleProtoFilesRoot._findIncludePath; - assert.throws(findIncludePath.bind(null, originPath, includePath)); + sinon.stub(fs, 'existsSync').returns(false); + assert.throws( + GoogleProtoFilesRoot._findIncludePath.bind( + null, + originPath, + includePath, + ), + ); }); it('should return the correct resolved import path', () => { const correctPath = path.join('test', 'example', 'import.proto'); - const findIncludePath = proxyquire('../../src/grpc', { - fs: { - existsSync(path: string) { - return path === correctPath; - }, - }, - }).GoogleProtoFilesRoot._findIncludePath; + sinon + .stub(fs, 'existsSync') + .callsFake((p: fs.PathLike) => p === correctPath); assert.strictEqual( - findIncludePath(originPath, includePath), + GoogleProtoFilesRoot._findIncludePath(originPath, includePath), correctPath, ); }); diff --git a/core/packages/nodejs-googleapis-common/package.json b/core/packages/nodejs-googleapis-common/package.json index 5ada806ae27d..a561c40589e5 100644 --- a/core/packages/nodejs-googleapis-common/package.json +++ b/core/packages/nodejs-googleapis-common/package.json @@ -17,7 +17,7 @@ "prebenchmark": "npm run compile", "benchmark": "node build/benchmark/bench.js", "compile": "tsc -p .", - "test": "node ../../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../../bin/run-test.cjs build/test", "system-test": "node ../../../bin/run-test.cjs build/system-test --timeout 600000", "presystem-test": "npm run compile", "fix": "gts fix", diff --git a/core/packages/nodejs-googleapis-common/test/test.authplus.ts b/core/packages/nodejs-googleapis-common/test/test.authplus.ts index 9a67b52adac6..a2c5b2aae6d5 100644 --- a/core/packages/nodejs-googleapis-common/test/test.authplus.ts +++ b/core/packages/nodejs-googleapis-common/test/test.authplus.ts @@ -13,23 +13,17 @@ // limitations under the License. import * as assert from 'assert'; -import {describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; -import * as AuthPlus from '../src/authplus'; +import {afterEach, describe, it} from 'mocha'; +import * as sinon from 'sinon'; +import {AuthPlus} from '../src/authplus'; describe(__filename, () => { - const AuthyPlus: typeof AuthPlus = proxyquire('../src/authplus', { - 'google-auth-library': { - GoogleAuth: class { - async getClient() { - return {}; - } - }, - }, + afterEach(() => { + sinon.restore(); }); it('should expose auth classes', () => { - const auth = new AuthyPlus.AuthPlus(); + const auth = new AuthPlus(); assert.ok(auth.JWT); assert.ok(auth.Compute); assert.ok(auth.OAuth2); @@ -40,8 +34,13 @@ describe(__filename, () => { }); it('should get a new client', async () => { - const auth = new AuthyPlus.AuthPlus(); + const auth = new AuthPlus(); + const getClientStub = sinon + .stub(auth.GoogleAuth.prototype, 'getClient') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .resolves({} as any); const client = await auth.getClient(); assert.deepStrictEqual(client, {}); + assert.strictEqual(getClientStub.calledOnce, true); }); }); diff --git a/core/packages/nodejs-googleapis-common/test/test.http2.ts b/core/packages/nodejs-googleapis-common/test/test.http2.ts index 1da040be4074..291deae4b60b 100644 --- a/core/packages/nodejs-googleapis-common/test/test.http2.ts +++ b/core/packages/nodejs-googleapis-common/test/test.http2.ts @@ -14,14 +14,13 @@ import * as assert from 'assert'; import * as coreHttp2 from 'http2'; -import {describe, it, before, beforeEach} from 'mocha'; +import {describe, it, before, beforeEach, after} from 'mocha'; import {gzipSync} from 'zlib'; -import * as proxyquire from 'proxyquire'; import * as sinon from 'sinon'; import {Duplex, Readable} from 'stream'; import {EventEmitter} from 'events'; import {GaxiosResponse} from 'gaxios'; -import * as http2Types from '../src/http2'; +import * as http2 from '../src/http2'; import {GoogleAuth} from '../src'; @@ -59,13 +58,15 @@ describe('http2', () => { return (connectStub || (() => {}))(host); } let connectStub: Function; - let http2: typeof http2Types; + let connectSinonStub: sinon.SinonStub; before(() => { - http2 = proxyquire('../src/http2', { - http2: { - connect: fakeConnect, - }, - }); + connectSinonStub = sinon + .stub(coreHttp2, 'connect') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake(fakeConnect as any); + }); + after(() => { + connectSinonStub.restore(); }); let requestStream: Duplex; @@ -153,7 +154,7 @@ describe('http2', () => { requestStream.push(Buffer.from('{}')); requestStream.push(null); await resPromise; - await new Promise(r => setTimeout(r, 500)); + await new Promise(resolve => setTimeout(resolve, 500)); assert.strictEqual(Object.keys(http2.sessions).length, 0); }); diff --git a/handwritten/bigquery/package.json b/handwritten/bigquery/package.json index a810796404c9..032cb571a879 100644 --- a/handwritten/bigquery/package.json +++ b/handwritten/bigquery/package.json @@ -36,7 +36,7 @@ "docs": "jsdoc -c .jsdoc.js", "lint": "gts check", "samples-test": "cd samples/ && pnpm link ../ && pnpm test && cd ../", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "presystem-test": "pnpm run compile", "clean": "gts clean", diff --git a/handwritten/bigquery/test/bigquery.ts b/handwritten/bigquery/test/bigquery.ts index e5bbf0e222a1..72d0743ac9f6 100644 --- a/handwritten/bigquery/test/bigquery.ts +++ b/handwritten/bigquery/test/bigquery.ts @@ -24,7 +24,7 @@ import * as assert from 'assert'; import {describe, it, after, afterEach, before, beforeEach} from 'mocha'; import * as Big from 'big.js'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as crypto from 'crypto'; diff --git a/handwritten/bigquery/test/dataset.ts b/handwritten/bigquery/test/dataset.ts index 26bb3ff8a831..cdf1a4e321dd 100644 --- a/handwritten/bigquery/test/dataset.ts +++ b/handwritten/bigquery/test/dataset.ts @@ -22,7 +22,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as _root from '../src'; import {toArray} from '../src/util'; diff --git a/handwritten/bigquery/test/job.ts b/handwritten/bigquery/test/job.ts index afba50a3fc7a..ece2bc1d340e 100644 --- a/handwritten/bigquery/test/job.ts +++ b/handwritten/bigquery/test/job.ts @@ -16,7 +16,7 @@ import {DecorateRequestOptions, util} from '@google-cloud/common'; import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, beforeEach, afterEach, before} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {BigQuery} from '../src/bigquery'; diff --git a/handwritten/bigquery/test/model.ts b/handwritten/bigquery/test/model.ts index 49c59a5cec4b..b07ab763e644 100644 --- a/handwritten/bigquery/test/model.ts +++ b/handwritten/bigquery/test/model.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as sinon from 'sinon'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as pfy from '@google-cloud/promisify'; import {EventEmitter} from 'events'; import {JobOptions} from '../src/job'; diff --git a/handwritten/bigquery/test/proxyquire.ts b/handwritten/bigquery/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/bigquery/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/bigquery/test/routine.ts b/handwritten/bigquery/test/routine.ts index d1cdb0966ac4..dac2db92b637 100644 --- a/handwritten/bigquery/test/routine.ts +++ b/handwritten/bigquery/test/routine.ts @@ -17,7 +17,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as _root from '../src'; diff --git a/handwritten/bigquery/test/rowQueue.ts b/handwritten/bigquery/test/rowQueue.ts index f53dbf011485..11b0f8c971c0 100644 --- a/handwritten/bigquery/test/rowQueue.ts +++ b/handwritten/bigquery/test/rowQueue.ts @@ -16,7 +16,7 @@ import {util} from '@google-cloud/common'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {Duplex, Stream} from 'stream'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as q from '../src/rowQueue'; import * as t from '../src/table'; diff --git a/handwritten/bigquery/test/table.ts b/handwritten/bigquery/test/table.ts index f3e59d7f3d9a..b17c506a65ed 100644 --- a/handwritten/bigquery/test/table.ts +++ b/handwritten/bigquery/test/table.ts @@ -26,7 +26,7 @@ import {describe, it, afterEach, beforeEach, before, after} from 'mocha'; import * as Big from 'big.js'; import {EventEmitter} from 'events'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as stream from 'stream'; import * as crypto from 'crypto'; diff --git a/handwritten/bigtable/package.json b/handwritten/bigtable/package.json index fab4e307fa9b..222d1421e862 100644 --- a/handwritten/bigtable/package.json +++ b/handwritten/bigtable/package.json @@ -42,7 +42,7 @@ "presystem-test": "pnpm run compile", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000 --retries 3", "pretest": "pnpm run compile", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "test:snap": "SNAPSHOT_UPDATE=1 pnpm test", "testproxy": "pnpm run compile && node build/testproxy/index.js", "clean": "gts clean", diff --git a/handwritten/bigtable/system-test/client-side-metrics-all-methods.ts b/handwritten/bigtable/system-test/client-side-metrics-all-methods.ts index d0d8d9ebe49b..42273eac501e 100644 --- a/handwritten/bigtable/system-test/client-side-metrics-all-methods.ts +++ b/handwritten/bigtable/system-test/client-side-metrics-all-methods.ts @@ -21,7 +21,7 @@ import { import {ResourceMetrics} from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import {GCPMetricsHandler} from '../src/client-side-metrics/gcp-metrics-handler'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import {Bigtable, RawFilter} from '../src'; import {Mutation} from '../src/mutation'; import {Row} from '../src/row'; diff --git a/handwritten/bigtable/system-test/client-side-metrics.ts b/handwritten/bigtable/system-test/client-side-metrics.ts index 7a029b37378e..4a1878131cc8 100644 --- a/handwritten/bigtable/system-test/client-side-metrics.ts +++ b/handwritten/bigtable/system-test/client-side-metrics.ts @@ -21,7 +21,7 @@ import { import {ResourceMetrics} from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import {GCPMetricsHandler} from '../src/client-side-metrics/gcp-metrics-handler'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import {Bigtable, BigtableOptions} from '../src'; import {Row} from '../src/row'; import { diff --git a/handwritten/bigtable/system-test/gcp-metrics-handler.ts b/handwritten/bigtable/system-test/gcp-metrics-handler.ts index 2608d6e191b1..bd847cbe2dfd 100644 --- a/handwritten/bigtable/system-test/gcp-metrics-handler.ts +++ b/handwritten/bigtable/system-test/gcp-metrics-handler.ts @@ -26,7 +26,7 @@ import {Bigtable} from '../src'; import {ResourceMetrics} from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import {ClientOptions} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; function getHandler(Exporter: typeof CloudMonitoringExporter) { const FakeCGPMetricsHandler = proxyquire( diff --git a/handwritten/bigtable/system-test/read-rows.ts b/handwritten/bigtable/system-test/read-rows.ts index 15ac57a41298..3317432e6c5e 100644 --- a/handwritten/bigtable/system-test/read-rows.ts +++ b/handwritten/bigtable/system-test/read-rows.ts @@ -26,7 +26,7 @@ import {EventEmitter} from 'events'; import {Test} from './testTypes'; import {ServiceError, GrpcClient, GoogleError, CallOptions} from 'google-gax'; import {PassThrough} from 'stream'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import {TabularApiSurface} from '../src/tabular-api-surface'; import * as mocha from 'mocha'; import {generateId} from './common'; @@ -141,7 +141,10 @@ describe('Bigtable/Table', () => { 'An error should have been thrown because the client is closed', ); } catch (err: any) { - assert.strictEqual(err.message, 'The client has already been closed.'); + assert.strictEqual( + err.message, + 'The client has already been closed.', + ); } } finally { const bigtableSecondClient = new Bigtable(); diff --git a/handwritten/bigtable/test/app-profile.ts b/handwritten/bigtable/test/app-profile.ts index 581c46f98065..33b2d1b34c9a 100644 --- a/handwritten/bigtable/test/app-profile.ts +++ b/handwritten/bigtable/test/app-profile.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {CallOptions} from 'google-gax'; let promisified = false; diff --git a/handwritten/bigtable/test/backup.ts b/handwritten/bigtable/test/backup.ts index 915bcde6bad2..fdf55e792e01 100644 --- a/handwritten/bigtable/test/backup.ts +++ b/handwritten/bigtable/test/backup.ts @@ -16,7 +16,7 @@ import {PreciseDate} from '@google-cloud/precise-date'; import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it, afterEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as pumpify from 'pumpify'; import {ServiceError} from 'google-gax'; diff --git a/handwritten/bigtable/test/base64keymap.ts b/handwritten/bigtable/test/base64keymap.ts index 7446126bff45..d9903a1afe75 100644 --- a/handwritten/bigtable/test/base64keymap.ts +++ b/handwritten/bigtable/test/base64keymap.ts @@ -15,7 +15,6 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import Long = require('long'); -import * as proxyquire from 'proxyquire'; import * as sn from 'sinon'; import {RowStateEnum} from '../src/chunktransformer.js'; diff --git a/handwritten/bigtable/test/chunktransformer.ts b/handwritten/bigtable/test/chunktransformer.ts index 88e0e313707a..fb7897914bc7 100644 --- a/handwritten/bigtable/test/chunktransformer.ts +++ b/handwritten/bigtable/test/chunktransformer.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import Long = require('long'); -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sn from 'sinon'; import {RowStateEnum} from '../src/chunktransformer.js'; diff --git a/handwritten/bigtable/test/cluster.ts b/handwritten/bigtable/test/cluster.ts index a1c963cd22f9..b49ad5298e49 100644 --- a/handwritten/bigtable/test/cluster.ts +++ b/handwritten/bigtable/test/cluster.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {PassThrough, Readable} from 'stream'; import {CallOptions} from 'google-gax'; import {PreciseDate} from '@google-cloud/precise-date'; diff --git a/handwritten/bigtable/test/executequery.ts b/handwritten/bigtable/test/executequery.ts index 4d0ea37f205b..fc1e2a469b0b 100644 --- a/handwritten/bigtable/test/executequery.ts +++ b/handwritten/bigtable/test/executequery.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import * as sinon from 'sinon'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {grpc} from 'google-gax'; import * as inst from '../src/instance'; import {Bigtable} from '../src'; diff --git a/handwritten/bigtable/test/family.ts b/handwritten/bigtable/test/family.ts index 4d6b0babc4e2..9017558d2185 100644 --- a/handwritten/bigtable/test/family.ts +++ b/handwritten/bigtable/test/family.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {protos} from '@google-cloud/bigtable-api'; import google = protos.google; diff --git a/handwritten/bigtable/test/filter.ts b/handwritten/bigtable/test/filter.ts index d2dc9fbaed7d..cd9ae392ef74 100644 --- a/handwritten/bigtable/test/filter.ts +++ b/handwritten/bigtable/test/filter.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {afterEach, before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as fr from '../src/filter'; import {Row} from '../src/row'; diff --git a/handwritten/bigtable/test/index.ts b/handwritten/bigtable/test/index.ts index 361d2527a300..b0995985e248 100644 --- a/handwritten/bigtable/test/index.ts +++ b/handwritten/bigtable/test/index.ts @@ -17,7 +17,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {afterEach, before, beforeEach, describe, it} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sn from 'sinon'; import {Cluster} from '../src/cluster.js'; @@ -1216,6 +1216,7 @@ describe('Bigtable', () => { }); describe('close', () => { it('should have failed request after close is called', done => { + // eslint-disable-next-line promise/catch-or-return, promise/always-return bigtable.close().then(() => { bigtable.getInstances((err: Error) => { if (err) { diff --git a/handwritten/bigtable/test/instance.ts b/handwritten/bigtable/test/instance.ts index 47798885b905..876cb156f77d 100644 --- a/handwritten/bigtable/test/instance.ts +++ b/handwritten/bigtable/test/instance.ts @@ -16,7 +16,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import * as sinon from 'sinon'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {ServiceError} from 'google-gax'; import * as snapshot from 'snap-shot-it'; import {Readable} from 'stream'; diff --git a/handwritten/bigtable/test/metric-service-client-credentials.ts b/handwritten/bigtable/test/metric-service-client-credentials.ts index b05caa470f61..bea333d2c105 100644 --- a/handwritten/bigtable/test/metric-service-client-credentials.ts +++ b/handwritten/bigtable/test/metric-service-client-credentials.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {ClientOptions, grpc} from 'google-gax'; import * as assert from 'assert'; import {MetricServiceClient} from '@google-cloud/monitoring'; diff --git a/handwritten/bigtable/test/metrics-collector/gcp-metrics-handler.ts b/handwritten/bigtable/test/metrics-collector/gcp-metrics-handler.ts index 6f54951b5551..5deebaf2bed0 100644 --- a/handwritten/bigtable/test/metrics-collector/gcp-metrics-handler.ts +++ b/handwritten/bigtable/test/metrics-collector/gcp-metrics-handler.ts @@ -30,7 +30,7 @@ import { } from '../../test-common/expected-otel-export-input'; import * as assert from 'assert'; import {replaceTimestamps} from '../../test-common/replace-timestamps'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; /** * Cleans a ResourceMetrics object by replacing client UUIDs with a placeholder. diff --git a/handwritten/bigtable/test/metrics-collector/metrics-collector.ts b/handwritten/bigtable/test/metrics-collector/metrics-collector.ts index b63bd2b0ba51..edaaa74bacd2 100644 --- a/handwritten/bigtable/test/metrics-collector/metrics-collector.ts +++ b/handwritten/bigtable/test/metrics-collector/metrics-collector.ts @@ -23,7 +23,7 @@ import { import {grpc} from 'google-gax'; import {expectedRequestsHandled} from '../../test-common/metrics-handler-fixture'; import * as gax from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import {GCPMetricsHandler} from '../../src/client-side-metrics/gcp-metrics-handler'; import {getProtoPath} from '../../src/utils/protos'; const protoPath = getProtoPath('google/bigtable/v2/response_params.proto'); diff --git a/handwritten/bigtable/test/proxyquire.ts b/handwritten/bigtable/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/bigtable/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/bigtable/test/row.ts b/handwritten/bigtable/test/row.ts index ca1f444cfb65..ab2cba41c8a8 100644 --- a/handwritten/bigtable/test/row.ts +++ b/handwritten/bigtable/test/row.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {afterEach, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Mutation} from '../src/mutation.js'; import * as rw from '../src/row'; diff --git a/handwritten/bigtable/test/table.ts b/handwritten/bigtable/test/table.ts index c10e89145d4e..5a7d54d67d07 100644 --- a/handwritten/bigtable/test/table.ts +++ b/handwritten/bigtable/test/table.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {afterEach, before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as pumpify from 'pumpify'; import * as sinon from 'sinon'; import {PassThrough, Writable, Duplex} from 'stream'; diff --git a/handwritten/datastore/package.json b/handwritten/datastore/package.json index a031adee220d..7271a5d9d22f 100644 --- a/handwritten/datastore/package.json +++ b/handwritten/datastore/package.json @@ -38,7 +38,7 @@ "presystem-test": "npm run compile", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "pretest": "npm run compile", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "precompile": "gts clean" }, "dependencies": { diff --git a/handwritten/datastore/system-test/transaction-spy.ts b/handwritten/datastore/system-test/transaction-spy.ts index 23ea7a69524c..fb4763aa0ea8 100644 --- a/handwritten/datastore/system-test/transaction-spy.ts +++ b/handwritten/datastore/system-test/transaction-spy.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import {DatastoreRequest} from '../src'; import {RequestCallback, RequestConfig} from '../src/request'; diff --git a/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts b/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts index a3a16ef7dc30..4c4809b69789 100644 --- a/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts +++ b/handwritten/datastore/test/gapic-mocks/client-initialization-testing.ts @@ -21,7 +21,7 @@ import { DatastoreOptions, } from '../../src'; import * as assert from 'assert'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import {Callback, CallOptions} from 'google-gax'; import * as protos from '../../src/protos'; import * as ds from '../../src'; diff --git a/handwritten/datastore/test/index-class.ts b/handwritten/datastore/test/index-class.ts index 3754e4672748..1f75cb436b38 100644 --- a/handwritten/datastore/test/index-class.ts +++ b/handwritten/datastore/test/index-class.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as ds from '../src'; diff --git a/handwritten/datastore/test/index.ts b/handwritten/datastore/test/index.ts index 03fedde213ce..8545834c59cb 100644 --- a/handwritten/datastore/test/index.ts +++ b/handwritten/datastore/test/index.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {before, beforeEach, after, afterEach, describe, it} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {PassThrough, Readable} from 'stream'; import * as ds from '../src'; diff --git a/handwritten/datastore/test/proxyquire.ts b/handwritten/datastore/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/datastore/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/datastore/test/request.ts b/handwritten/datastore/test/request.ts index 97366be73bbe..1db6a3699caf 100644 --- a/handwritten/datastore/test/request.ts +++ b/handwritten/datastore/test/request.ts @@ -18,7 +18,7 @@ import {after, afterEach, before, beforeEach, describe, it} from 'mocha'; import * as extend from 'extend'; import * as gax from 'google-gax'; import * as is from 'is'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PassThrough, Transform} from 'stream'; diff --git a/handwritten/datastore/test/transaction.ts b/handwritten/datastore/test/transaction.ts index 36366c0d7e02..c322c5f4592f 100644 --- a/handwritten/datastore/test/transaction.ts +++ b/handwritten/datastore/test/transaction.ts @@ -16,7 +16,7 @@ import * as pfy from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {afterEach, beforeEach, before, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import { Datastore, DatastoreOptions, diff --git a/handwritten/error-reporting/package.json b/handwritten/error-reporting/package.json index 99be138c9e81..145b0be715b3 100644 --- a/handwritten/error-reporting/package.json +++ b/handwritten/error-reporting/package.json @@ -23,7 +23,7 @@ "lint": "gts check", "presystem-test": "pnpm run compile", "system-test": "MOCHA_THROW_DEPRECATION=false node ../../bin/run-test.cjs build/system-test", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test/unit", + "test": "node ../../bin/run-test.cjs build/test/unit", "clean": "gts clean", "compile": "tsc -p .", "fix": "gts fix", diff --git a/handwritten/error-reporting/test/unit/google-apis/auth-client.ts b/handwritten/error-reporting/test/unit/google-apis/auth-client.ts index 712dc4fb95af..205e352301ba 100644 --- a/handwritten/error-reporting/test/unit/google-apis/auth-client.ts +++ b/handwritten/error-reporting/test/unit/google-apis/auth-client.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as proxyquire from 'proxyquire'; +import {util} from '@google-cloud/common'; import {describe, beforeEach, afterEach, it} from 'mocha'; import { @@ -20,6 +20,7 @@ import { ConfigurationOptions, Logger, } from '../../../src/configuration'; +import {RequestHandler} from '../../../src/google-apis/auth-client'; import {deepStrictEqual} from '../../util'; function verifyReportedMessage( @@ -28,28 +29,6 @@ function verifyReportedMessage( expectedLogs: {error?: string; info?: string; warn?: string}, done: () => void, ) { - class ServiceStub { - authClient: {}; - request: {}; - constructor() { - this.authClient = { - async getAccessToken() { - if (errToReturn) { - throw errToReturn; - } - return 'some-token'; - }, - }; - this.request = () => {}; - } - } - - const RequestHandler = proxyquire('../../../src/google-apis/auth-client', { - '@google-cloud/common': { - Service: ServiceStub, - }, - }).RequestHandler; - const logs: {error?: string; info?: string; warn?: string} = {}; const logger = { error(text: string) { @@ -72,8 +51,27 @@ function verifyReportedMessage( }, } as {} as Logger; const config2 = new Configuration(config1, logger); - // tslint:disable-next-line:no-unused-expression - new RequestHandler(config2, logger); + const origMakeAuthFactory = util.makeAuthenticatedRequestFactory; + const origRequest = RequestHandler.prototype.request; + util.makeAuthenticatedRequestFactory = (() => ({ + authClient: { + async getAccessToken() { + if (errToReturn) { + throw errToReturn; + } + return 'some-token'; + }, + }, + getCredentials: {}, + })) as unknown as typeof util.makeAuthenticatedRequestFactory; + RequestHandler.prototype.request = (() => {}) as typeof origRequest; + try { + // tslint:disable-next-line:no-unused-expression + new RequestHandler(config2, logger); + } finally { + util.makeAuthenticatedRequestFactory = origMakeAuthFactory; + RequestHandler.prototype.request = origRequest; + } setImmediate(() => { deepStrictEqual(logs, expectedLogs); done(); diff --git a/handwritten/google-cloud-dns/package.json b/handwritten/google-cloud-dns/package.json index 9a8744bc31b2..a58a630f0a2c 100644 --- a/handwritten/google-cloud-dns/package.json +++ b/handwritten/google-cloud-dns/package.json @@ -33,7 +33,7 @@ "scripts": { "docs": "jsdoc -c .jsdoc.js", "lint": "gts check", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "samples-test": "pnpm run compile && cd samples/ && pnpm link ../ && pnpm i && pnpm test", "presystem-test": "pnpm run compile", "system-test": "pnpm run compile && node ../../bin/run-test.cjs build/system-test", diff --git a/handwritten/google-cloud-dns/test/change.ts b/handwritten/google-cloud-dns/test/change.ts index 4d37cfd21cd6..7aaa61f9c0fc 100644 --- a/handwritten/google-cloud-dns/test/change.ts +++ b/handwritten/google-cloud-dns/test/change.ts @@ -20,7 +20,7 @@ import { import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Change} from '../src/change'; let promisified = false; @@ -119,7 +119,7 @@ describe('Change', () => { assert.strictEqual(change, null); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -145,7 +145,7 @@ describe('Change', () => { assert.strictEqual(change_, change); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); diff --git a/handwritten/google-cloud-dns/test/index.ts b/handwritten/google-cloud-dns/test/index.ts index 0c3fd27da05e..1d89ead7255a 100644 --- a/handwritten/google-cloud-dns/test/index.ts +++ b/handwritten/google-cloud-dns/test/index.ts @@ -22,7 +22,7 @@ import * as promisify from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {CoreOptions, OptionsWithUri, Response} from 'request'; import {Zone} from '../src'; @@ -135,7 +135,7 @@ describe('DNS', () => { assert.deepStrictEqual( calledWith.packageJson, // eslint-disable-next-line @typescript-eslint/no-var-requires - require('../../package.json') + require('../../package.json'), ); }); @@ -226,7 +226,7 @@ describe('DNS', () => { assert.strictEqual(zone, null); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -265,7 +265,7 @@ describe('DNS', () => { assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); @@ -318,7 +318,7 @@ describe('DNS', () => { err: Error, zones: Zone[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.strictEqual(err, error); assert.strictEqual(zones, null); @@ -326,7 +326,7 @@ describe('DNS', () => { assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -377,7 +377,7 @@ describe('DNS', () => { nextQuery, Object.assign({}, query, { pageToken: apiResponseWithNextPageToken.nextPageToken, - }) + }), ); done(); @@ -391,7 +391,7 @@ describe('DNS', () => { err: Error, zones: Zone[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.ifError(err); assert.strictEqual(zones[0], zone); @@ -399,7 +399,7 @@ describe('DNS', () => { assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); diff --git a/handwritten/google-cloud-dns/test/proxyquire.ts b/handwritten/google-cloud-dns/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/google-cloud-dns/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/google-cloud-dns/test/record.ts b/handwritten/google-cloud-dns/test/record.ts index e7174c43d546..ffbba1ce547e 100644 --- a/handwritten/google-cloud-dns/test/record.ts +++ b/handwritten/google-cloud-dns/test/record.ts @@ -15,7 +15,7 @@ import * as promisify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import {Record} from '../src'; diff --git a/handwritten/google-cloud-dns/test/zone.ts b/handwritten/google-cloud-dns/test/zone.ts index 07681f51e591..c2a639b6d668 100644 --- a/handwritten/google-cloud-dns/test/zone.ts +++ b/handwritten/google-cloud-dns/test/zone.ts @@ -17,7 +17,7 @@ import * as promisify from '@google-cloud/promisify'; import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {CoreOptions, OptionsWithUri, Response} from 'request'; import * as crypto from 'crypto'; @@ -187,7 +187,7 @@ describe('Zone', () => { zone.createChange = ( options: CreateChangeRequest, - callback: Function + callback: Function, ) => { assert.strictEqual(options.add, records); callback(); @@ -215,7 +215,7 @@ describe('Zone', () => { type: crypto.randomUUID(), rrdatas: [crypto.randomUUID(), crypto.randomUUID()], }, - recordJson + recordJson, ); return { @@ -312,7 +312,7 @@ describe('Zone', () => { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -342,7 +342,7 @@ describe('Zone', () => { assert.strictEqual(change_.metadata, apiResponse); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -390,7 +390,7 @@ describe('Zone', () => { zone.createChange = ( options: CreateChangeRequest, - callback: Function + callback: Function, ) => { assert.deepStrictEqual(options.delete, [recordsToDelete]); callback(); @@ -462,7 +462,7 @@ describe('Zone', () => { it('should delete non-NS and non-SOA records', done => { zone.deleteRecords = ( recordsToDelete: string[], - callback: Function + callback: Function, ) => { assert.deepStrictEqual(recordsToDelete, expectedRecordsToDelete); callback(); @@ -539,7 +539,7 @@ describe('Zone', () => { writeFileOverride = ( path_: string, content: string, - encoding: string + encoding: string, ) => { assert.strictEqual(path_, path); assert.strictEqual(content, expectedZonefileContents); @@ -559,7 +559,7 @@ describe('Zone', () => { path: string, content: string, encoding: string, - callback: Function + callback: Function, ) => { callback(error); }; @@ -579,7 +579,7 @@ describe('Zone', () => { path: string, content: string, encoding: string, - callback: Function + callback: Function, ) => { callback(); }; @@ -648,12 +648,12 @@ describe('Zone', () => { err: Error, changes: Change[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -703,14 +703,14 @@ describe('Zone', () => { err: Error, changes: Change[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.ifError(err); assert.strictEqual(changes[0], change); assert.strictEqual(changes[0].metadata, apiResponse.changes[0]); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -734,12 +734,12 @@ describe('Zone', () => { err: Error, changes: Change[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.strictEqual(err, error); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); @@ -795,13 +795,13 @@ describe('Zone', () => { err: Error, records: Record[], nextQuery: {}, - apiResponse_: Response + apiResponse_: Response, ) => { assert.ifError(err); assert.strictEqual(records[0], record); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); it('should not require a query', done => { @@ -866,7 +866,7 @@ describe('Zone', () => { readFileOverride = ( path: string, encoding: string, - callback: Function + callback: Function, ) => { callback(error); }; @@ -897,7 +897,7 @@ describe('Zone', () => { readFileOverride = ( path: string, encoding: string, - callback: Function + callback: Function, ) => { callback(); }; @@ -906,7 +906,7 @@ describe('Zone', () => { it('should add records', done => { zone.addRecords = ( recordsToCreate: FakeRecord[], - callback: Function + callback: Function, ) => { assert.strictEqual(recordsToCreate.length, 1); const recordToCreate = recordsToCreate[0]; @@ -998,7 +998,7 @@ describe('Zone', () => { it('should create a change', done => { zone.createChange = ( options: CreateChangeRequest, - callback: Function + callback: Function, ) => { assert.strictEqual(options.add, recordsToCreate); assert.strictEqual(options.delete, recordsToDelete); diff --git a/handwritten/logging-bunyan/package.json b/handwritten/logging-bunyan/package.json index 3358325c6cce..4bd8a2581106 100644 --- a/handwritten/logging-bunyan/package.json +++ b/handwritten/logging-bunyan/package.json @@ -42,7 +42,7 @@ "presystem-test": "pnpm run compile", "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", - "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs build/test", "clean": "gts clean", "compile": "tsc -p .", "postcompile": "cpy \"src/types/*\" build/src/types", diff --git a/handwritten/logging-bunyan/test/index.ts b/handwritten/logging-bunyan/test/index.ts index 9e91fe2665fe..0c82b56d35f5 100644 --- a/handwritten/logging-bunyan/test/index.ts +++ b/handwritten/logging-bunyan/test/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {inspect} from 'util'; import {LoggingBunyan} from '../src'; diff --git a/handwritten/logging-bunyan/test/middleware/test-express.ts b/handwritten/logging-bunyan/test/middleware/test-express.ts index 99920c4ca522..ba5f651c6e8d 100644 --- a/handwritten/logging-bunyan/test/middleware/test-express.ts +++ b/handwritten/logging-bunyan/test/middleware/test-express.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import {GCPEnv} from 'google-auth-library'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; // types-only import. Actual require is done through proxyquire below. import {MiddlewareOptions} from '../../src/middleware/express'; diff --git a/handwritten/logging-bunyan/test/proxyquire.ts b/handwritten/logging-bunyan/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/logging-bunyan/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/logging-winston/package.json b/handwritten/logging-winston/package.json index aaf7cd1b0af5..810054027251 100644 --- a/handwritten/logging-winston/package.json +++ b/handwritten/logging-winston/package.json @@ -42,7 +42,7 @@ "samples-test": "cd samples/ && npm link ../ && npm test && cd ../", "presystem-test": "pnpm run pretest", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", - "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs build/test", "clean": "gts clean", "compile": "tsc -p .", "fix": "gts fix", diff --git a/handwritten/logging-winston/test/common.ts b/handwritten/logging-winston/test/common.ts index 39401702c6ee..f3fb4011a6dc 100644 --- a/handwritten/logging-winston/test/common.ts +++ b/handwritten/logging-winston/test/common.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as nodeutil from 'util'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Options} from '../src'; import {Entry, Logging, LogSync, Log} from '@google-cloud/logging'; import * as instrumentation from '@google-cloud/logging/build/src/utils/instrumentation'; diff --git a/handwritten/logging-winston/test/index.ts b/handwritten/logging-winston/test/index.ts index 13412aa4e742..1af6b8881636 100644 --- a/handwritten/logging-winston/test/index.ts +++ b/handwritten/logging-winston/test/index.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as TransportStream from 'winston-transport'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import {Options} from '../src'; describe('logging-winston', () => { diff --git a/handwritten/logging-winston/test/middleware/express.ts b/handwritten/logging-winston/test/middleware/express.ts index 40a1853601dc..b8ec33cc8f9c 100644 --- a/handwritten/logging-winston/test/middleware/express.ts +++ b/handwritten/logging-winston/test/middleware/express.ts @@ -18,7 +18,7 @@ import {GCPEnv} from 'google-auth-library'; import {LogEntry} from 'winston'; import * as TransportStream from 'winston-transport'; import * as winston from 'winston'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import {Options} from '../../src'; // types-only import. Actual require is done through proxyquire below. diff --git a/handwritten/logging-winston/test/proxyquire.ts b/handwritten/logging-winston/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/logging-winston/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/logging-winston/test/stackdriver-trace-integration.ts b/handwritten/logging-winston/test/stackdriver-trace-integration.ts index ed0e668c4bbb..9e62c33575e0 100644 --- a/handwritten/logging-winston/test/stackdriver-trace-integration.ts +++ b/handwritten/logging-winston/test/stackdriver-trace-integration.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, after} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as winston from 'winston'; import * as loggingWinstonLibTypes from '../src/index'; diff --git a/handwritten/logging/package.json b/handwritten/logging/package.json index 63a3d3261513..06363bb21952 100644 --- a/handwritten/logging/package.json +++ b/handwritten/logging/package.json @@ -40,7 +40,7 @@ "presystem-test": "pnpm run compile", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "pretest": "pnpm run compile", - "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "cross-env NODE_OPTIONS='--no-deprecation' node ../../bin/run-test.cjs build/test", "precompile": "gts clean", "postpublish": "./.kokoro/publish-min.sh" }, diff --git a/handwritten/logging/test/entry.ts b/handwritten/logging/test/entry.ts index d17976edd138..d9d994a8e44d 100644 --- a/handwritten/logging/test/entry.ts +++ b/handwritten/logging/test/entry.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as entryTypes from '../src/entry'; import * as common from '../src/utils/common'; import * as http from 'http'; @@ -222,7 +222,7 @@ describe('Entry', () => { it('should pass removeCircular to objToStruct_', done => { fakeObjToStruct = ( obj: {}, - options: common.ObjectToStructConverterConfig, + options: common.ObjectToStructConverterConfig ) => { assert.strictEqual(options.removeCircular, true); done(); @@ -325,7 +325,7 @@ describe('Entry', () => { }); after(() => { - sdk.shutdown(); + return sdk.shutdown(); }); it('should detect open telemetry trace and span if open telemetry context present', () => { @@ -335,12 +335,12 @@ describe('Entry', () => { const json = entry.toJSON(); assert.strictEqual( json.trace, - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual(json.spanId, span.spanContext().spanId); assert.strictEqual( json.traceSampled, - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -360,12 +360,12 @@ describe('Entry', () => { const json = entry.toJSON(); assert.strictEqual( json.trace, - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual(json.spanId, span.spanContext().spanId); assert.strictEqual( json.traceSampled, - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -413,7 +413,7 @@ describe('Entry', () => { entry.data = 'this is a log'; const json = entry.toStructuredJSON(); assert( - withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)), + withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)) ); delete json.timestamp; const expectedJSON = { @@ -441,7 +441,7 @@ describe('Entry', () => { entry.metadata.timestamp = new Date(); const json = entry.toStructuredJSON(); assert( - withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)), + withinExpectedTimeBoundaries(nanosAndSecondsToDate(json.timestamp!)) ); }); @@ -503,7 +503,7 @@ describe('Entry', () => { }); after(() => { - sdk.shutdown(); + return sdk.shutdown(); }); it('should detect open telemetry trace and span if open telemetry context present', () => { @@ -513,15 +513,15 @@ describe('Entry', () => { const json = entry.toStructuredJSON(); assert.strictEqual( json[entryTypes.TRACE_KEY], - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual( json[entryTypes.SPAN_ID_KEY], - span.spanContext().spanId, + span.spanContext().spanId ); assert.strictEqual( json[entryTypes.TRACE_SAMPLED_KEY], - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -541,15 +541,15 @@ describe('Entry', () => { const json = entry.toStructuredJSON(); assert.strictEqual( json[entryTypes.TRACE_KEY], - `projects//traces/${span.spanContext().traceId}`, + `projects//traces/${span.spanContext().traceId}` ); assert.strictEqual( json[entryTypes.SPAN_ID_KEY], - span.spanContext().spanId, + span.spanContext().spanId ); assert.strictEqual( json[entryTypes.TRACE_SAMPLED_KEY], - (span.spanContext().traceFlags & 1) !== 0, + (span.spanContext().traceFlags & 1) !== 0 ); }); }); @@ -571,7 +571,7 @@ describe('Entry', () => { assert.strictEqual(json[entryTypes.SPAN_ID_KEY], expected.spanId); assert.strictEqual( json[entryTypes.TRACE_SAMPLED_KEY], - expected.traceSampled, + expected.traceSampled ); }); }); diff --git a/handwritten/logging/test/index.ts b/handwritten/logging/test/index.ts index 73ae9f61581a..03663ed512dc 100644 --- a/handwritten/logging/test/index.ts +++ b/handwritten/logging/test/index.ts @@ -18,7 +18,7 @@ import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, beforeEach, before} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import { Logging as LOGGING, LoggingOptions, @@ -215,8 +215,8 @@ describe('Logging', () => { libVersion: version, scopes: EXPECTED_SCOPES, }, - options, - ), + options + ) ); return fakeGoogleAuthInstance; }; @@ -245,8 +245,8 @@ describe('Logging', () => { libVersion: version, scopes: EXPECTED_SCOPES, }, - options, - ), + options + ) ); }); @@ -270,7 +270,7 @@ describe('Logging', () => { it('should throw if a name is not provided', () => { const error = new Error('A sink name must be provided.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - (logging as any).createSink().then(noop, (err: Error) => { + return (logging as any).createSink().then(noop, (err: Error) => { assert.deepStrictEqual(err, error); }); }); @@ -278,7 +278,7 @@ describe('Logging', () => { it('should throw if a config object is not provided', () => { const error = new Error('A sink configuration object must be provided.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - (logging as any).createSink(SINK_NAME).then(noop, (err: Error) => { + return (logging as any).createSink(SINK_NAME).then(noop, (err: Error) => { assert.deepStrictEqual(err, error); }); }); @@ -354,7 +354,7 @@ describe('Logging', () => { logging.configService.createSink = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { const expectedParent = 'projects/' + logging.projectId; assert.strictEqual(reqOpts.parent, expectedParent); @@ -376,11 +376,11 @@ describe('Logging', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, // eslint-disable-next-line @typescript-eslint/no-unused-vars - gaxOpts: {}, + gaxOpts: {} ) => { assert.strictEqual( reqOpts.uniqueWriterIdentity, - config.uniqueWriterIdentity, + config.uniqueWriterIdentity ); assert.strictEqual(reqOpts.sink.uniqueWriterIdentity, undefined); return [{}]; @@ -399,7 +399,7 @@ describe('Logging', () => { logging.configService.createSink = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.strictEqual(reqOpts.sink.gaxOptions, undefined); assert.strictEqual(gaxOpts, config.gaxOptions); @@ -424,7 +424,7 @@ describe('Logging', () => { throw error; }; - logging + return logging .createSink(SINK_NAME, {} as CreateSinkRequest) .then(noop, (err: Error) => assert.deepStrictEqual(err, error)); }); @@ -455,7 +455,7 @@ describe('Logging', () => { const [sink_, apiResponse_] = await logging.createSink( SINK_NAME, - {} as CreateSinkRequest, + {} as CreateSinkRequest ); assert.strictEqual(sink_, sink); assert.strictEqual(sink_.metadata, apiResponse); @@ -487,7 +487,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { filter: reqOpts?.filter, @@ -512,7 +512,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any - reqOpts: any, + reqOpts: any ) => { assert.deepStrictEqual(reqOpts.resourceNames, resourceNames); return [[]]; @@ -526,7 +526,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual( reqOpts, @@ -534,7 +534,7 @@ describe('Logging', () => { filter: 'timestamp > "2020-11-11T15:01:23.045123456Z"', orderBy: 'timestamp desc', resourceNames: ['projects/' + logging.projectId], - }), + }) ); assert.deepStrictEqual(gaxOpts, { @@ -552,7 +552,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual( reqOpts, @@ -560,7 +560,7 @@ describe('Logging', () => { filter: reqOpts?.filter, orderBy: 'timestamp desc', resourceNames: ['projects/' + logging.projectId], - }), + }) ); assert.deepStrictEqual(gaxOpts, { autoPaginate: undefined, @@ -614,7 +614,7 @@ describe('Logging', () => { logging.loggingService.listLogEntries = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any reqOpts: any, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { a: 'b', @@ -643,7 +643,9 @@ describe('Logging', () => { }); it('should reject promise with error', () => { - logging.getEntries().then(noop, err => assert.strictEqual(err, error)); + return logging + .getEntries() + .then(noop, err => assert.strictEqual(err, error)); }); }); @@ -692,7 +694,7 @@ describe('Logging', () => { it('should make request once reading', done => { logging.loggingService.listLogEntriesStream = ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { resourceNames: ['projects/' + logging.projectId], @@ -721,7 +723,7 @@ describe('Logging', () => { logging = new LOGGING({projectId: PROJECT_ID}); logging.loggingService.listLogEntriesStream = ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { resourceNames: ['projects/' + logging.projectId], @@ -758,12 +760,12 @@ describe('Logging', () => { { filter: 'custom filter', }, - OPTIONS, + OPTIONS ); logging = new LOGGING({projectId: PROJECT_ID}); logging.loggingService.listLogEntriesStream = ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { resourceNames: ['projects/' + logging.projectId], @@ -908,7 +910,7 @@ describe('Logging', () => { logging.loggingService.listLogs = async () => { throw error; }; - logging + return logging .getLogs(OPTIONS) .then(noop, err => assert.strictEqual(err, error)); }); @@ -1079,7 +1081,7 @@ describe('Logging', () => { logging.configService.listSinks = async () => { throw error; }; - logging + return logging .getSinks(OPTIONS) .then(noop, err => assert.strictEqual(err, error)); }); @@ -1561,7 +1563,7 @@ describe('Logging', () => { }); it('should return error', () => { - logging + return logging .setAclForBucket_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1610,7 +1612,7 @@ describe('Logging', () => { }); it('should reject with error', () => { - logging + return logging .setAclForDataset_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1658,7 +1660,7 @@ describe('Logging', () => { }); it('should reject with error', () => { - logging + return logging .setAclForDataset_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1717,7 +1719,7 @@ describe('Logging', () => { }); it('should throw error', () => { - logging + return logging .setAclForTopic_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); @@ -1764,7 +1766,7 @@ describe('Logging', () => { }); it('should throw error', () => { - logging + return logging .setAclForTopic_(CONFIG) .then(noop, err => assert.deepStrictEqual(err, error)); }); diff --git a/handwritten/logging/test/log.ts b/handwritten/logging/test/log.ts index e535e739b694..3efb9dfbf2ae 100644 --- a/handwritten/logging/test/log.ts +++ b/handwritten/logging/test/log.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Entry, Logging} from '../src'; import {Log as LOG, LogOptions, WriteOptions} from '../src/log'; @@ -123,8 +123,8 @@ describe('Log', () => { assert( callbackifyFake.callbackifyAll.calledWithExactly( Log, - sinon.match({exclude: ['entry', 'getEntriesStream']}), - ), + sinon.match({exclude: ['entry', 'getEntriesStream']}) + ) ); }); @@ -140,7 +140,7 @@ describe('Log', () => { const log = new Log(LOGGING, LOG_NAME); assert.strictEqual( log.formattedName_, - logCommon.formatLogName('{{project-id}}', LOG_NAME), + logCommon.formatLogName('{{project-id}}', LOG_NAME) ); }); @@ -172,8 +172,8 @@ describe('Log', () => { logName: log.formattedName_, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -186,8 +186,8 @@ describe('Log', () => { logName: log.formattedName_, }, undefined, - log.defaultWriteDeleteCallback, - ), + log.defaultWriteDeleteCallback + ) ); log.defaultWriteDeleteCallback = undefined; }); @@ -195,7 +195,7 @@ describe('Log', () => { it('should accept gaxOptions', async () => { await log.delete({}); assert( - log.logging.loggingService.deleteLog.calledWith(sinon.match.any, {}), + log.logging.loggingService.deleteLog.calledWith(sinon.match.any, {}) ); }); }); @@ -235,7 +235,7 @@ describe('Log', () => { assert( log.logging.getEntries.calledWithExactly({ filter: `logName="${LOG_NAME_FORMATTED}"`, - }), + }) ); }); @@ -273,7 +273,7 @@ describe('Log', () => { assert( log.logging.getEntriesStream.calledWithExactly({ log: LOG_NAME_ENCODED, - }), + }) ); }); @@ -292,9 +292,9 @@ describe('Log', () => { { log: LOG_NAME_ENCODED, }, - options, - ), - ), + options + ) + ) ); }); }); @@ -312,7 +312,7 @@ describe('Log', () => { assert( log.logging.tailEntries.calledWithExactly({ log: LOG_NAME_ENCODED, - }), + }) ); }); @@ -331,9 +331,9 @@ describe('Log', () => { { log: LOG_NAME_ENCODED, }, - options, - ), - ), + options + ) + ) ); }); }); @@ -388,8 +388,8 @@ describe('Log', () => { }, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -424,8 +424,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ resource: reusableDetectedResource, - }), - ), + }) + ) ); }); @@ -454,8 +454,8 @@ describe('Log', () => { resource: EXPECTED_RESOURCE, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -470,8 +470,8 @@ describe('Log', () => { resource: FAKE_RESOURCE, }, undefined, - undefined, - ), + undefined + ) ); }); @@ -487,8 +487,8 @@ describe('Log', () => { resource: FAKE_RESOURCE, }, undefined, - log.defaultWriteDeleteCallback, - ), + log.defaultWriteDeleteCallback + ) ); log.defaultWriteDeleteCallback = undefined; }); @@ -503,8 +503,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ entries: 'decorated entries', - }), - ), + }) + ) ); }); @@ -517,8 +517,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ entries: arrifiedEntries, - }), - ), + }) + ) ); }); @@ -530,8 +530,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ entries: ENTRIES, - }), - ), + }) + ) ); }); @@ -541,8 +541,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ partialSuccess: true, - }), - ), + }) + ) ); }); @@ -553,8 +553,8 @@ describe('Log', () => { sinon.match({ dryRun: true, partialSuccess: false, - }), - ), + }) + ) ); }); @@ -567,8 +567,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ partialSuccess: true, - }), - ), + }) + ) ); instrumentation.setInstrumentationStatus(true); }); @@ -582,8 +582,8 @@ describe('Log', () => { log.logging.loggingService.writeLogEntries.calledOnceWith( sinon.match({ partialSuccess: true, - }), - ), + }) + ) ); }); @@ -597,8 +597,8 @@ describe('Log', () => { { maxRetries: 1, }, - sinon.match.any, - ), + sinon.match.any + ) ); log.logging.loggingService.writeLogEntries.reset(); await log.write(ENTRIES, {gaxOptions: {maxRetries: 10}}); @@ -608,8 +608,8 @@ describe('Log', () => { { maxRetries: 10, }, - sinon.match.any, - ), + sinon.match.any + ) ); }); }); @@ -656,7 +656,7 @@ describe('Log', () => { .returns({} as EntryJson); log.decorateEntries([entry]); assert( - localJSONStub.calledWithExactly({removeCircular: true}, PROJECT_ID), + localJSONStub.calledWithExactly({removeCircular: true}, PROJECT_ID) ); }); @@ -749,11 +749,11 @@ describe('Log', () => { assert.ok(log.jsonFieldsToTruncate.length > 1); assert.ok(log.jsonFieldsToTruncate[0] === TRUNCATE_FIELD); const notExists = log.jsonFieldsToTruncate.filter( - (str: string) => str === INVALID_TRUNCATE_FIELD, + (str: string) => str === INVALID_TRUNCATE_FIELD ); assert.strictEqual(notExists.length, 0); const existOnce = log.jsonFieldsToTruncate.filter( - (str: string) => str === TRUNCATE_FIELD, + (str: string) => str === TRUNCATE_FIELD ); assert.strictEqual(existOnce.length, 1); }); diff --git a/handwritten/logging/test/middleware/express/test-make-middleware.ts b/handwritten/logging/test/middleware/express/test-make-middleware.ts index 47740b2ad306..5444e2fc698e 100644 --- a/handwritten/logging/test/middleware/express/test-make-middleware.ts +++ b/handwritten/logging/test/middleware/express/test-make-middleware.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach} from 'mocha'; import * as EventEmitter from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../../proxyquire'; const FAKE_PROJECT_ID = 'project-🦄'; @@ -45,7 +45,7 @@ describe('middleware/express/make-middleware', () => { '../../../src/middleware/express/make-middleware', { '../../../src/utils/context': FAKE_CONTEXT, - }, + } ); it('should return a function accepting 3 arguments', () => { @@ -133,7 +133,7 @@ describe('middleware/express/make-middleware', () => { const middleware = makeMiddleware( FAKE_PROJECT_ID, () => {}, - emitRequestLog, + emitRequestLog ); middleware(fakeRequest, fakeResponse, () => {}); diff --git a/handwritten/logging/test/proxyquire.ts b/handwritten/logging/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/logging/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/logging/test/sink.ts b/handwritten/logging/test/sink.ts index 598e63c70bf7..f5a6c747bb2e 100644 --- a/handwritten/logging/test/sink.ts +++ b/handwritten/logging/test/sink.ts @@ -16,7 +16,7 @@ import * as callbackify from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Sink as SINK} from '../src/sink'; import {Logging, CreateSinkRequest, LogSink} from '../src/index'; @@ -75,7 +75,7 @@ describe('Sink', () => { it('should localize the formatted name', () => { assert.strictEqual( sink.formattedName_, - 'projects/' + LOGGING.projectId + '/sinks/' + SINK_NAME, + 'projects/' + LOGGING.projectId + '/sinks/' + SINK_NAME ); }); }); @@ -98,7 +98,7 @@ describe('Sink', () => { sink.logging.auth.getProjectId = async () => PROJECT_ID; sink.logging.configService.deleteSink = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { sinkName: sink.formattedName_, @@ -113,7 +113,7 @@ describe('Sink', () => { const gaxOptions = {}; sink.logging.configService.deleteSink = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(gaxOpts, gaxOptions); }; @@ -201,9 +201,9 @@ describe('Sink', () => { sink.getMetadata = async () => { throw error; }; - sink.setMetadata(METADATA).then( - () => {}, - err => assert.strictEqual(err, error), + return sink.setMetadata(METADATA).then( + () => null, + err => assert.strictEqual(err, error) ); }); @@ -213,7 +213,7 @@ describe('Sink', () => { sink.getMetadata = async () => [currentMetadata] as any; sink.logging.configService.updateSink = async ( reqOpts: {}, - gaxOpts: {}, + gaxOpts: {} ) => { assert.deepStrictEqual(reqOpts, { sinkName: sink.formattedName_, diff --git a/handwritten/logging/test/utils/metadata.ts b/handwritten/logging/test/utils/metadata.ts index 9425f8ff5436..0e32e59f2853 100644 --- a/handwritten/logging/test/utils/metadata.ts +++ b/handwritten/logging/test/utils/metadata.ts @@ -17,7 +17,7 @@ import {describe, it, beforeEach, before, after, afterEach} from 'mocha'; import BigNumber from 'bignumber.js'; import * as extend from 'extend'; import {GCPEnv} from 'google-auth-library'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; let instanceOverride: {} | null; const fakeGcpMetadata = { @@ -298,7 +298,7 @@ describe('metadata', () => { }; await assert.rejects( metadata.getGKEDescriptor(), - (err: Error) => err === FAKE_ERROR, + (err: Error) => err === FAKE_ERROR ); }); @@ -306,7 +306,7 @@ describe('metadata', () => { readFileShouldError = true; await assert.doesNotReject(metadata.getGKEDescriptor(), (err: Error) => - err.message.includes(FAKE_READFILE_ERROR_MESSAGE), + err.message.includes(FAKE_READFILE_ERROR_MESSAGE) ); }); }); diff --git a/handwritten/pubsub/package.json b/handwritten/pubsub/package.json index b995155c1015..27c7a7aa8ff5 100644 --- a/handwritten/pubsub/package.json +++ b/handwritten/pubsub/package.json @@ -35,7 +35,7 @@ "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 600000", "samples-test": "pnpm run link-samples && pnpm test && cd ../", "link-samples": "cd samples/ && pnpm link ../ && pnpm install", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test", + "test": "node ../../bin/run-test.cjs build/test", "lint": "gts check", "predocs": "pnpm run compile", "docs": "jsdoc -c .jsdoc.js", diff --git a/handwritten/pubsub/test/iam.ts b/handwritten/pubsub/test/iam.ts index a1f73f8bf35d..6232a51e37ac 100644 --- a/handwritten/pubsub/test/iam.ts +++ b/handwritten/pubsub/test/iam.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as iamTypes from '../src/iam'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/lease-manager.ts b/handwritten/pubsub/test/lease-manager.ts index d7fc2d8a2cfd..a5088a5d1813 100644 --- a/handwritten/pubsub/test/lease-manager.ts +++ b/handwritten/pubsub/test/lease-manager.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as defer from 'p-defer'; @@ -208,10 +208,7 @@ describe('LeaseManager', () => { subscriber.on('message', () => { assert.strictEqual(fakeLog!.called, true); - assert.strictEqual( - fakeLog!.fields!.severity, - 'INFO', - ); + assert.strictEqual(fakeLog!.fields!.severity, 'INFO'); assert.strictEqual(fakeLog!.args![1] as string, 'a'); assert.strictEqual(fakeLog!.args![2] as string, 'b'); done(); @@ -241,10 +238,7 @@ describe('LeaseManager', () => { await deferred.promise; assert.strictEqual(fakeLog.called, true); - assert.strictEqual( - fakeLog.fields!.severity, - 'ERROR', - ); + assert.strictEqual(fakeLog.fields!.severity, 'ERROR'); assert.strictEqual( (fakeLog.args![0] as string).includes('exception'), true, @@ -402,10 +396,7 @@ describe('LeaseManager', () => { // make sure the expired messages were forgotten assert.strictEqual(removeStub.callCount, badMessages.length); - assert.strictEqual( - fakeLog.fields!.severity, - 'WARNING', - ); + assert.strictEqual(fakeLog.fields!.severity, 'WARNING'); assert.strictEqual(fakeLog.called, true); badMessages.forEach((fakeMessage, i) => { diff --git a/handwritten/pubsub/test/message-stream.ts b/handwritten/pubsub/test/message-stream.ts index e9a0a5a1fad2..0d921e038ba8 100644 --- a/handwritten/pubsub/test/message-stream.ts +++ b/handwritten/pubsub/test/message-stream.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {grpc} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Duplex, PassThrough} from 'stream'; import * as defer from 'p-defer'; diff --git a/handwritten/pubsub/test/proxyquire.ts b/handwritten/pubsub/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/pubsub/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/pubsub/test/publisher/index.ts b/handwritten/pubsub/test/publisher/index.ts index 6930b3baa67e..f4fa95908ba2 100644 --- a/handwritten/pubsub/test/publisher/index.ts +++ b/handwritten/pubsub/test/publisher/index.ts @@ -18,7 +18,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import * as sinon from 'sinon'; import * as opentelemetry from '@opentelemetry/api'; import {Topic} from '../../src'; diff --git a/handwritten/pubsub/test/publisher/message-queues.ts b/handwritten/pubsub/test/publisher/message-queues.ts index c6d0b3309987..343906dd6833 100644 --- a/handwritten/pubsub/test/publisher/message-queues.ts +++ b/handwritten/pubsub/test/publisher/message-queues.ts @@ -18,7 +18,7 @@ import {loggingUtils, ServiceError} from 'google-gax'; import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import * as sinon from 'sinon'; import {RequestConfig, RequestCallback} from '../../src/pubsub'; @@ -187,10 +187,7 @@ describe('Message Queues', () => { fakeLog.remove(); assert.strictEqual(fakeLog.called, true); - assert.strictEqual( - fakeLog.fields!.severity, - 'INFO', - ); + assert.strictEqual(fakeLog.fields!.severity, 'INFO'); assert.strictEqual(fakeLog.args![1] as string, 'test'); }); @@ -394,6 +391,7 @@ describe('Message Queues', () => { queue.batch = new FakeMessageBatch(); queue.batch.messages = fakeMessages; queue.batch.callbacks = spies; + // eslint-disable-next-line promise/always-return void queue.publishDrain().then(() => { process.nextTick(() => { assert.strictEqual(stub.callCount, 2); @@ -429,6 +427,7 @@ describe('Message Queues', () => { sandbox.stub(queue, '_publish').callsFake(async () => {}); queue.on('drain', spy); + // eslint-disable-next-line promise/always-return void queue.publish('test').then(() => { process.nextTick(() => { assert.strictEqual(spy.callCount, 1); diff --git a/handwritten/pubsub/test/pubsub.ts b/handwritten/pubsub/test/pubsub.ts index b2ecbfa7d29f..06f4547a09eb 100644 --- a/handwritten/pubsub/test/pubsub.ts +++ b/handwritten/pubsub/test/pubsub.ts @@ -17,7 +17,7 @@ import arrify = require('arrify'); import * as assert from 'assert'; import {describe, it, before, beforeEach, after, afterEach} from 'mocha'; import * as gax from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import defer = require('p-defer'); @@ -196,7 +196,7 @@ describe('PubSub', () => { './snapshot': {Snapshot: FakeSnapshot}, './subscription': {Subscription}, './topic': {Topic: FakeTopic}, - './index': {v1:v1Override}, + './index': {v1: v1Override}, './util': fakeUtil, }).PubSub; }); diff --git a/handwritten/pubsub/test/snapshot.ts b/handwritten/pubsub/test/snapshot.ts index 5e5da3efec7a..cd781fe09e8d 100644 --- a/handwritten/pubsub/test/snapshot.ts +++ b/handwritten/pubsub/test/snapshot.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {ServiceError} from 'google-gax'; import {describe, it, beforeEach, before, after, afterEach} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/subscriber.ts b/handwritten/pubsub/test/subscriber.ts index a0c4b0336b96..a6051a7aa459 100644 --- a/handwritten/pubsub/test/subscriber.ts +++ b/handwritten/pubsub/test/subscriber.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; import {common as protobuf} from 'protobufjs'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PassThrough} from 'stream'; import * as opentelemetry from '@opentelemetry/api'; diff --git a/handwritten/pubsub/test/subscription.ts b/handwritten/pubsub/test/subscription.ts index 4855ec9217a5..5577c6b0e794 100644 --- a/handwritten/pubsub/test/subscription.ts +++ b/handwritten/pubsub/test/subscription.ts @@ -16,7 +16,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {EventEmitter} from 'events'; import {ServiceError} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {PubSub, RequestConfig} from '../src/pubsub'; diff --git a/handwritten/pubsub/test/topic.ts b/handwritten/pubsub/test/topic.ts index c918b21c64e9..4e2f736e48b1 100644 --- a/handwritten/pubsub/test/topic.ts +++ b/handwritten/pubsub/test/topic.ts @@ -15,7 +15,7 @@ import * as assert from 'assert'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {CallOptions, ServiceError} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {google} from '../protos/protos'; diff --git a/handwritten/spanner/observability-test/batch-transaction.ts b/handwritten/spanner/observability-test/batch-transaction.ts index f3738c67ecef..4105c3d95993 100644 --- a/handwritten/spanner/observability-test/batch-transaction.ts +++ b/handwritten/spanner/observability-test/batch-transaction.ts @@ -21,7 +21,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, after, beforeEach, afterEach, describe, it} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import * as sinon from 'sinon'; const { AlwaysOnSampler, diff --git a/handwritten/spanner/observability-test/database.ts b/handwritten/spanner/observability-test/database.ts index 1c9410242d33..7d4ab0b796e6 100644 --- a/handwritten/spanner/observability-test/database.ts +++ b/handwritten/spanner/observability-test/database.ts @@ -30,7 +30,7 @@ import { } from '../src/transaction'; import {util} from '@google-cloud/common'; import {Transform} from 'stream'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import * as sinon from 'sinon'; const {SpanStatusCode} = require('@opentelemetry/api'); const { diff --git a/handwritten/spanner/observability-test/helper.ts b/handwritten/spanner/observability-test/helper.ts index fc610a4f912f..45be0d2c891a 100644 --- a/handwritten/spanner/observability-test/helper.ts +++ b/handwritten/spanner/observability-test/helper.ts @@ -43,6 +43,7 @@ export const cacheSessionEvents = ['Cache hit: has usable multiplexed session']; * and referring to context causes type/value collision errors. */ export function setGlobalContextManager(manager: ContextManager) { + context.disable(); if (typeof manager.enable === 'function') { manager.enable(); } diff --git a/handwritten/spanner/observability-test/spanner.ts b/handwritten/spanner/observability-test/spanner.ts index a262314b2f79..8d9854f8fb52 100644 --- a/handwritten/spanner/observability-test/spanner.ts +++ b/handwritten/spanner/observability-test/spanner.ts @@ -188,6 +188,7 @@ describe('EndToEnd', async () => { const database = instance.database('database'); beforeEach(async () => { + setGlobalContextManager(contextManager); // To deflake expectations of session creation, let's // issue out a warm-up request request that'll ensure // that the MultiplexedSession is created deterministically. diff --git a/handwritten/spanner/observability-test/table.ts b/handwritten/spanner/observability-test/table.ts index 36257b34e875..3506f10e4eeb 100644 --- a/handwritten/spanner/observability-test/table.ts +++ b/handwritten/spanner/observability-test/table.ts @@ -18,7 +18,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import * as sinon from 'sinon'; import * as through from 'through2'; diff --git a/handwritten/spanner/observability-test/transaction.ts b/handwritten/spanner/observability-test/transaction.ts index 126b52141b8c..351427dd0b9b 100644 --- a/handwritten/spanner/observability-test/transaction.ts +++ b/handwritten/spanner/observability-test/transaction.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../test/proxyquire'; import * as sinon from 'sinon'; import {codec} from '../src/codec'; diff --git a/handwritten/spanner/package.json b/handwritten/spanner/package.json index e07b4ae6b71c..e26166d92aa7 100644 --- a/handwritten/spanner/package.json +++ b/handwritten/spanner/package.json @@ -35,9 +35,9 @@ "predocs": "pnpm run compile", "lint": "gts check", "system-test": "node ../../bin/run-test.cjs build/system-test --timeout 1600000", - "observability-test": "node ../../bin/run-test.cjs --proxyquire-shim build/observability-test --timeout 1600000", + "observability-test": "node ../../bin/run-test.cjs build/observability-test --timeout 1600000", "cleanup": "node ../../bin/run-test.cjs scripts/cleanup.js --timeout 30000", - "test": "node ../../bin/run-test.cjs --proxyquire-shim build/test build/test/common build/observability-test", + "test": "node ../../bin/run-test.cjs build/test build/test/common build/observability-test", "ycsb": "node ./benchmark/ycsb.js run -P ./benchmark/workloada -p table=usertable -p cloudspanner.instance=ycsb-instance -p operationcount=100 -p cloudspanner.database=ycsb", "fix": "gts fix", "clean": "gts clean", diff --git a/handwritten/spanner/test/backup.ts b/handwritten/spanner/test/backup.ts index f52acc976b1b..e8de46ecbed3 100644 --- a/handwritten/spanner/test/backup.ts +++ b/handwritten/spanner/test/backup.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {EventEmitter} from 'events'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {util} from '@google-cloud/common'; import * as pfy from '@google-cloud/promisify'; diff --git a/handwritten/spanner/test/batch-transaction.ts b/handwritten/spanner/test/batch-transaction.ts index 990bee6bb8bd..f8cd9043ae7a 100644 --- a/handwritten/spanner/test/batch-transaction.ts +++ b/handwritten/spanner/test/batch-transaction.ts @@ -20,7 +20,7 @@ import {util} from '@google-cloud/common'; import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Session, Database, Spanner, protos} from '../src'; diff --git a/handwritten/spanner/test/common/service-object.ts b/handwritten/spanner/test/common/service-object.ts index 7e5544eb8e95..489863c87d5a 100644 --- a/handwritten/spanner/test/common/service-object.ts +++ b/handwritten/spanner/test/common/service-object.ts @@ -19,7 +19,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; let promisified = false; const fakePfy = Object.assign({}, pfy, { diff --git a/handwritten/spanner/test/common/service.ts b/handwritten/spanner/test/common/service.ts index 11ea01a9820a..c073c31cd095 100644 --- a/handwritten/spanner/test/common/service.ts +++ b/handwritten/spanner/test/common/service.ts @@ -20,7 +20,7 @@ import {before, beforeEach, after, afterEach, describe, it} from 'mocha'; import * as duplexify from 'duplexify'; import * as extend from 'extend'; import {grpc, GrpcClient} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from '../proxyquire'; import * as retryRequest from 'retry-request'; import * as sn from 'sinon'; import {PassThrough} from 'stream'; diff --git a/handwritten/spanner/test/database.ts b/handwritten/spanner/test/database.ts index ea94628136e6..eebe303775c2 100644 --- a/handwritten/spanner/test/database.ts +++ b/handwritten/spanner/test/database.ts @@ -21,7 +21,7 @@ import {before, beforeEach, afterEach, describe, it} from 'mocha'; import {EventEmitter} from 'events'; import * as extend from 'extend'; import {ApiError, util} from '@google-cloud/common'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Transform, Duplex} from 'stream'; import * as through from 'through2'; diff --git a/handwritten/spanner/test/index.ts b/handwritten/spanner/test/index.ts index 384eb8b5c440..3bbcbc34f054 100644 --- a/handwritten/spanner/test/index.ts +++ b/handwritten/spanner/test/index.ts @@ -20,7 +20,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import * as extend from 'extend'; import * as path from 'path'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as through from 'through2'; import {util} from '@google-cloud/common'; import {PreciseDate} from '@google-cloud/precise-date'; diff --git a/handwritten/spanner/test/instance-config.ts b/handwritten/spanner/test/instance-config.ts index dd050be447f0..528ca1e875e2 100644 --- a/handwritten/spanner/test/instance-config.ts +++ b/handwritten/spanner/test/instance-config.ts @@ -18,7 +18,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import {grpc} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as pfy from '@google-cloud/promisify'; import * as sinon from 'sinon'; diff --git a/handwritten/spanner/test/instance.ts b/handwritten/spanner/test/instance.ts index f816e2fa0d15..ef6d21788c6c 100644 --- a/handwritten/spanner/test/instance.ts +++ b/handwritten/spanner/test/instance.ts @@ -20,7 +20,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import {ApiError} from '@google-cloud/common'; import {grpc} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as pfy from '@google-cloud/promisify'; import * as sinon from 'sinon'; import snakeCase = require('lodash.snakecase'); diff --git a/handwritten/spanner/test/partial-result-stream.ts b/handwritten/spanner/test/partial-result-stream.ts index 4d8560be7593..beceec7e1521 100644 --- a/handwritten/spanner/test/partial-result-stream.ts +++ b/handwritten/spanner/test/partial-result-stream.ts @@ -19,7 +19,7 @@ import {before, beforeEach, afterEach, describe, it} from 'mocha'; // eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires const concat = require('concat-stream'); -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Transform, finished} from 'stream'; import * as through from 'through2'; diff --git a/handwritten/spanner/test/proxyquire.ts b/handwritten/spanner/test/proxyquire.ts new file mode 100644 index 000000000000..44ff9f689c12 --- /dev/null +++ b/handwritten/spanner/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export = proxyquire; diff --git a/handwritten/spanner/test/session.ts b/handwritten/spanner/test/session.ts index 60e2de6845a6..fb5cfb734054 100644 --- a/handwritten/spanner/test/session.ts +++ b/handwritten/spanner/test/session.ts @@ -19,7 +19,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import { CLOUD_RESOURCE_HEADER, LEADER_AWARE_ROUTING_HEADER, diff --git a/handwritten/spanner/test/table.ts b/handwritten/spanner/test/table.ts index 199d456215ba..cd4d2a744532 100644 --- a/handwritten/spanner/test/table.ts +++ b/handwritten/spanner/test/table.ts @@ -17,7 +17,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {Transform} from 'stream'; import * as through from 'through2'; diff --git a/handwritten/spanner/test/transaction-runner.ts b/handwritten/spanner/test/transaction-runner.ts index 9a2bd818b313..ee5c8764bcbc 100644 --- a/handwritten/spanner/test/transaction-runner.ts +++ b/handwritten/spanner/test/transaction-runner.ts @@ -18,7 +18,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import {EventEmitter} from 'events'; import {grpc} from 'google-gax'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import * as through from 'through2'; import {RunTransactionOptions} from '../src/transaction-runner'; @@ -329,6 +329,7 @@ describe('TransactionRunner', () => { runner .run() + // eslint-disable-next-line promise/always-return .then(() => { done(new Error('missing expected DEADLINE_EXCEEDED error')); }) diff --git a/handwritten/spanner/test/transaction.ts b/handwritten/spanner/test/transaction.ts index 5e5e13ef7590..c3ae51eff5ac 100644 --- a/handwritten/spanner/test/transaction.ts +++ b/handwritten/spanner/test/transaction.ts @@ -19,7 +19,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import {EventEmitter} from 'events'; import {common as p} from 'protobufjs'; -import * as proxyquire from 'proxyquire'; +import * as proxyquire from './proxyquire'; import * as sinon from 'sinon'; import {codec} from '../src/codec'; diff --git a/handwritten/storage/package.json b/handwritten/storage/package.json index 461391690a21..679530c84430 100644 --- a/handwritten/storage/package.json +++ b/handwritten/storage/package.json @@ -71,7 +71,7 @@ "pretest": "pnpm run compile --sourceMap", "system-test:esm": "mkdir -p $HOME/.config && node ../../bin/run-test.cjs --no-parallel build/esm/system-test --timeout 600000 --exit", "system-test": "mkdir -p $HOME/.config && node ../../bin/run-test.cjs --no-parallel build/cjs/system-test --timeout 600000 --exit", - "test": "cross-env NODE_OPTIONS=\"--require ./scripts/preload-yargs.cjs --no-deprecation\" node ../../bin/run-test.cjs --proxyquire-shim build/cjs/test" + "test": "cross-env NODE_OPTIONS=\"--require ./scripts/preload-yargs.cjs --no-deprecation\" node ../../bin/run-test.cjs build/cjs/test" }, "dependencies": { "@google-cloud/paginator": "^7.0.1", diff --git a/handwritten/storage/test/acl.ts b/handwritten/storage/test/acl.ts index 5c1d73e25ae0..6d35f9398488 100644 --- a/handwritten/storage/test/acl.ts +++ b/handwritten/storage/test/acl.ts @@ -15,7 +15,7 @@ import {DecorateRequestOptions, util} from '../src/nodejs-common/index.js'; import assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; import {Storage} from '../src/storage.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/handwritten/storage/test/bucket.ts b/handwritten/storage/test/bucket.ts index 09928da7aab4..dc39164881c3 100644 --- a/handwritten/storage/test/bucket.ts +++ b/handwritten/storage/test/bucket.ts @@ -26,7 +26,7 @@ import {describe, it, before, beforeEach, after, afterEach} from 'mocha'; import mime from 'mime'; import pLimit from 'p-limit'; import * as path from 'path'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; import * as stream from 'stream'; import {Bucket, Channel, Notification, CRC32C} from '../src/index.js'; diff --git a/handwritten/storage/test/channel.ts b/handwritten/storage/test/channel.ts index e70272f20453..715f8adc1321 100644 --- a/handwritten/storage/test/channel.ts +++ b/handwritten/storage/test/channel.ts @@ -24,7 +24,7 @@ import { } from '../src/nodejs-common/index.js'; import assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; let promisified = false; const fakePromisify = { diff --git a/handwritten/storage/test/file.ts b/handwritten/storage/test/file.ts index 938e8e317bba..04dd3bb069ef 100644 --- a/handwritten/storage/test/file.ts +++ b/handwritten/storage/test/file.ts @@ -36,7 +36,7 @@ import * as crypto from 'crypto'; import duplexify from 'duplexify'; import * as fs from 'fs'; import * as path from 'path'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; import * as resumableUpload from '../src/resumable-upload.js'; import * as sinon from 'sinon'; import * as tmp from 'tmp'; diff --git a/handwritten/storage/test/headers.ts b/handwritten/storage/test/headers.ts index b786ae54d4e0..4b3080293308 100644 --- a/handwritten/storage/test/headers.ts +++ b/handwritten/storage/test/headers.ts @@ -14,7 +14,7 @@ import * as assert from 'assert'; import {describe, it} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; const error = Error('not implemented'); diff --git a/handwritten/storage/test/hmacKey.ts b/handwritten/storage/test/hmacKey.ts index b67da92d7233..01d8d73c4167 100644 --- a/handwritten/storage/test/hmacKey.ts +++ b/handwritten/storage/test/hmacKey.ts @@ -13,7 +13,7 @@ // limitations under the License. import * as sinon from 'sinon'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; import assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; import {util, ServiceObject} from '../src/nodejs-common/index.js'; diff --git a/handwritten/storage/test/iam.ts b/handwritten/storage/test/iam.ts index a037e77b0a46..9907c2981075 100644 --- a/handwritten/storage/test/iam.ts +++ b/handwritten/storage/test/iam.ts @@ -15,7 +15,7 @@ import {DecorateRequestOptions, util} from '../src/nodejs-common/index.js'; import assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; import {IAMExceptionMessages} from '../src/iam.js'; describe('storage/iam', () => { diff --git a/handwritten/storage/test/index.ts b/handwritten/storage/test/index.ts index f615cbeb1ffa..a3d3f9fc0b04 100644 --- a/handwritten/storage/test/index.ts +++ b/handwritten/storage/test/index.ts @@ -22,7 +22,7 @@ import { import {PromisifyAllOptions} from '@google-cloud/promisify'; import assert from 'assert'; import {describe, it, before, beforeEach, after, afterEach} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import {Bucket, CRC32C_DEFAULT_VALIDATOR_GENERATOR} from '../src/index.js'; import {GetFilesOptions} from '../src/bucket.js'; diff --git a/handwritten/storage/test/nodejs-common/service-object.ts b/handwritten/storage/test/nodejs-common/service-object.ts index ac22a62dbdcf..58b7128964bb 100644 --- a/handwritten/storage/test/nodejs-common/service-object.ts +++ b/handwritten/storage/test/nodejs-common/service-object.ts @@ -21,7 +21,7 @@ import { } from '@google-cloud/promisify'; import assert from 'assert'; import {describe, it, beforeEach, afterEach} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from '../proxyquire.js'; import type { OptionsWithUri, Request as TeenyRequest, diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts index f785a1315793..e153b25a7de8 100644 --- a/handwritten/storage/test/nodejs-common/service.ts +++ b/handwritten/storage/test/nodejs-common/service.ts @@ -16,7 +16,7 @@ import assert from 'assert'; import {describe, it, before, beforeEach, after} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from '../proxyquire.js'; import {Request} from 'teeny-request'; import {AuthClient, GoogleAuth, OAuth2Client} from 'google-auth-library'; diff --git a/handwritten/storage/test/nodejs-common/util.ts b/handwritten/storage/test/nodejs-common/util.ts index d7ebc515c3fb..4b43852fd4f7 100644 --- a/handwritten/storage/test/nodejs-common/util.ts +++ b/handwritten/storage/test/nodejs-common/util.ts @@ -27,7 +27,7 @@ import { OAuth2Client, } from 'google-auth-library'; import * as nock from 'nock'; -import proxyquire from 'proxyquire'; +import proxyquire from '../proxyquire.js'; import retryRequest from 'retry-request'; import * as sinon from 'sinon'; import * as stream from 'stream'; diff --git a/handwritten/storage/test/notification.ts b/handwritten/storage/test/notification.ts index fe396dcb512a..2635c34acf04 100644 --- a/handwritten/storage/test/notification.ts +++ b/handwritten/storage/test/notification.ts @@ -21,7 +21,7 @@ import { } from '../src/nodejs-common/index.js'; import assert from 'assert'; import {describe, it, before, beforeEach} from 'mocha'; -import proxyquire from 'proxyquire'; +import proxyquire from './proxyquire.js'; import {Bucket} from '../src/index.js'; diff --git a/handwritten/storage/test/proxyquire.ts b/handwritten/storage/test/proxyquire.ts new file mode 100644 index 000000000000..42cdea201440 --- /dev/null +++ b/handwritten/storage/test/proxyquire.ts @@ -0,0 +1,115 @@ +// 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 Module from 'module'; +import path from 'path'; + +delete require.cache[require.resolve(__filename)]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Stubs = Record; +const mod = Module as unknown as { + prototype: {require: NodeRequire}; + _resolveFilename: (request: string, parent: NodeModule) => string; +}; +const {hasOwnProperty} = Object.prototype; +const hasOwn = (o: unknown, k: string) => + Boolean(o && typeof o === 'object' && hasOwnProperty.call(o, k)); +const isGlobal = (s: unknown) => + hasOwn(s, '@global') || hasOwn(s, '@runtimeGlobal'); +const resolveFrom = (base: string, req: string) => { + try { + return require.resolve(req, {paths: [path.dirname(base)]}); + } catch { + return req[0] === '.' ? path.resolve(path.dirname(base), req) : req; + } +}; + +const parent = module.parent || module; +let skipCallThru = false; +let keepCache = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function proxyquire(request: string, stubs: Stubs = {}): T { + const sut = mod._resolveFilename(request, parent); + const resolved: Stubs = {}; + let global = false; + for (const [k, v] of Object.entries(stubs)) { + if (isGlobal(v)) global = true; + resolved[resolveFrom(sut, k)] = v; + } + const 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 = mod.prototype.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') + ? stub['@noCallThru'] + : skipCallThru; + if (!skip && stub && ['object', 'function'].includes(typeof stub)) { + try { + const real = prev.call(this, id); + if (real && ['object', 'function'].includes(typeof real)) { + for (const k of Object.keys(real)) { + if (!(k in stub)) stub[k] = real[k]; + } + } + } catch { + // ignore + } + } + return stub; + } + } + } + return prev.call(this, id); + }; + mod.prototype.require = Object.assign(req, prev); + try { + return prev.call(parent, request); + } finally { + mod.prototype.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 && (keepCache || id !== sut)) cache[id] = m; + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace proxyquire { + export const load = proxyquire; + export const noCallThru = () => ((skipCallThru = true), proxyquire); + export const callThru = () => ((skipCallThru = false), proxyquire); + export const noPreserveCache = () => ((keepCache = false), proxyquire); + export const preserveCache = () => ((keepCache = true), proxyquire); +} + +export default proxyquire; diff --git a/packages/google-cloud-translate/package.json b/packages/google-cloud-translate/package.json index 328f2a0179e5..b7edd2c81e2d 100644 --- a/packages/google-cloud-translate/package.json +++ b/packages/google-cloud-translate/package.json @@ -44,7 +44,7 @@ "postpack": "minifyProtoJson", "samples-test": "cd samples/ && npm link ../ && npm i && npm test", "system-test": "node ../../bin/run-test.cjs --config ../../.mocharc.cjs --no-parallel build/system-test", - "test": "node ../../bin/run-test.cjs --proxyquire-shim --config ../../.mocharc.cjs build/test" + "test": "node ../../bin/run-test.cjs --config ../../.mocharc.cjs build/test" }, "dependencies": { "@google-cloud/common": "^8.0.1", diff --git a/packages/google-cloud-translate/test/index.ts b/packages/google-cloud-translate/test/index.ts index 8e6d714e057a..3e03bbd748cf 100644 --- a/packages/google-cloud-translate/test/index.ts +++ b/packages/google-cloud-translate/test/index.ts @@ -21,7 +21,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import {after, afterEach, before, beforeEach, describe, it} from 'mocha'; import * as extend from 'extend'; -import * as proxyquire from 'proxyquire'; +import * as Module from 'module'; import * as r from 'request'; import * as orig from '../src'; @@ -44,7 +44,7 @@ const fakeUtil = extend({}, util, { makeRequest( opts: DecorateRequestOptions, cfg: MakeRequestConfig, - cb: BodyResponseCallback + cb: BodyResponseCallback, ) { if (makeRequestOverride) { return makeRequestOverride(opts, cfg, cb); @@ -74,13 +74,30 @@ describe('Translate v2', () => { let translate: any; before(() => { - Translate = proxyquire('../src/v2', { + const stubs: Record = { '@google-cloud/common': { util: fakeUtil, Service: FakeService, }, '@google-cloud/promisify': fakePromisify, - }).Translate; + }; + const mod = Module as unknown as {prototype: {require: NodeRequire}}; + const origRequire = mod.prototype.require; + const v2Path = require.resolve('../src/v2'); + const cached = require.cache[v2Path]; + delete require.cache[v2Path]; + mod.prototype.require = Object.assign(function ( + this: NodeModule, + id: string, + ) { + return id in stubs ? stubs[id] : origRequire.call(this, id); + }, origRequire); + try { + Translate = require('../src/v2').Translate; + } finally { + mod.prototype.require = origRequire; + if (cached) require.cache[v2Path] = cached; + } }); beforeEach(() => { @@ -266,7 +283,7 @@ describe('Translate v2', () => { assert.strictEqual(apiResponse_, apiResponse); assert.deepStrictEqual(apiResponse_, originalApiResponse); done(); - } + }, ); }); }); @@ -314,7 +331,7 @@ describe('Translate v2', () => { assert.strictEqual(languages, null); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -359,7 +376,7 @@ describe('Translate v2', () => { assert.deepStrictEqual(languages, expectedResults); assert.strictEqual(apiResponse_, apiResponse); done(); - } + }, ); }); }); @@ -407,7 +424,7 @@ describe('Translate v2', () => { from: SOURCE_LANG_CODE, to: TARGET_LANG_CODE, }, - assert.ifError + assert.ifError, ); }); }); @@ -496,7 +513,7 @@ describe('Translate v2', () => { assert.strictEqual(translations, null); assert.strictEqual(resp, apiResponse); done(); - } + }, ); }); }); @@ -531,7 +548,7 @@ describe('Translate v2', () => { assert.deepStrictEqual(translations, expectedResults); assert.strictEqual(resp, apiResponse); done(); - } + }, ); }); @@ -552,7 +569,7 @@ describe('Translate v2', () => { assert.ifError(err); assert.deepStrictEqual(translations, [expectedResults]); done(); - } + }, ); }); }); @@ -584,7 +601,7 @@ describe('Translate v2', () => { FakeService.prototype.request = ( reqOpts: r.Options, - callback: Function + callback: Function, ) => { assert.strictEqual(reqOpts, fakeOptions); callback(); @@ -640,7 +657,7 @@ describe('Translate v2', () => { makeRequestOverride = ( reqOpts: r.Options, options: {}, - callback: Function + callback: Function, ) => { assert.deepStrictEqual(reqOpts, expectedReqOpts); assert.strictEqual(options, translate.options);