diff --git a/.mocharc.yml b/.mocharc.yml index 638403e0b02..7ec96218bf3 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -1,7 +1,7 @@ require: - ts-node/register - source-map-support/register - - src/test/helpers/mocha-bootstrap.ts + - src/test/helpers/mocha-bootstrap.js file: - src/test/helpers/global-mock-auth.ts timeout: 2000 diff --git a/src/accountExporter.spec.ts b/src/accountExporter.spec.ts index 4d25b00b92e..876e21602aa 100644 --- a/src/accountExporter.spec.ts +++ b/src/accountExporter.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import * as os from "os"; import * as sinon from "sinon"; diff --git a/src/accountImporter.spec.ts b/src/accountImporter.spec.ts index 2965278499e..953529bb508 100644 --- a/src/accountImporter.spec.ts +++ b/src/accountImporter.spec.ts @@ -1,4 +1,4 @@ -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import { expect } from "chai"; import { googleOrigin } from "./api"; diff --git a/src/apiv2.spec.ts b/src/apiv2.spec.ts index b33041fca02..1120b2cb4ed 100644 --- a/src/apiv2.spec.ts +++ b/src/apiv2.spec.ts @@ -1,8 +1,9 @@ import { createServer, Server } from "http"; import { expect } from "chai"; -import * as nock from "nock"; -import AbortController from "abort-controller"; +import * as sinon from "sinon"; import * as FormData from "form-data"; +import * as auth from "./auth"; +import nock from "./test/helpers/nock"; const proxySetup = require("proxy"); import { Client, CLI_OAUTH_PROJECT_NUMBER } from "./apiv2"; @@ -10,6 +11,18 @@ import { FirebaseError } from "./error"; import { streamToString, stringToStream } from "./utils"; describe("apiv2", () => { + let authStub: sinon.SinonStub | undefined; + before(() => { + if (typeof (auth.getAccessToken as any).restore !== "function") { + authStub = sinon.stub(auth, "getAccessToken").resolves({ access_token: "owner" } as any); + } + }); + after(() => { + if (authStub) { + authStub.restore(); + } + }); + beforeEach(() => { // The api module has package variables that we don't want sticking around. delete require.cache[require.resolve("./apiv2")]; @@ -150,17 +163,39 @@ describe("apiv2", () => { }); it("should resend a multipart body when retrying after a premature close", async () => { - const sentBodies: string[] = []; - const capture = (b: unknown): boolean => { - sentBodies.push(typeof b === "string" ? b : JSON.stringify(b)); + let body1: string | undefined; + let body2: string | undefined; + + const bodyToStr = (b: unknown): string => { + if (b instanceof Uint8Array || Buffer.isBuffer(b)) { + return Buffer.from(b).toString("utf8"); + } else if (typeof b === "string") { + return b; + } else { + return JSON.stringify(b); + } + }; + + const capture1 = (b: unknown): boolean => { + if (body1 === undefined) { + body1 = bodyToStr(b); + } return true; }; - nock("https://example.com").post("/upload", capture).once().replyWithError({ + + const capture2 = (b: unknown): boolean => { + if (body2 === undefined) { + body2 = bodyToStr(b); + } + return true; + }; + + nock("https://example.com").post("/upload", capture1).once().replyWithError({ message: "Invalid response body while trying to fetch https://example.com/upload: Premature close", code: "ERR_STREAM_PREMATURE_CLOSE", }); - nock("https://example.com").post("/upload", capture).once().reply(200, { ok: true }); + nock("https://example.com").post("/upload", capture2).once().reply(200, { ok: true }); const form = new FormData(); form.append("code", "secret-code-123"); @@ -177,9 +212,10 @@ describe("apiv2", () => { }); expect(r.status).to.equal(200); // Both the original attempt and the retry must carry the full body. - expect(sentBodies).to.have.length(2); - expect(sentBodies[0]).to.contain("secret-code-123"); - expect(sentBodies[1]).to.contain("secret-code-123"); + expect(body1).to.not.be.undefined; + expect(body2).to.not.be.undefined; + expect(body1).to.contain("secret-code-123"); + expect(body2).to.contain("secret-code-123"); expect(nock.isDone()).to.be.true; }); @@ -584,7 +620,11 @@ describe("apiv2", () => { new Promise((resolve) => proxyServer.close(resolve)), new Promise((resolve) => targetServer.close(resolve)), ]); - process.env.HTTP_PROXY = oldProxy; + if (oldProxy === undefined) { + delete process.env.HTTP_PROXY; + } else { + process.env.HTTP_PROXY = oldProxy; + } }); it("should be able to make a basic GET request", async () => { diff --git a/src/appdistribution/client.spec.ts b/src/appdistribution/client.spec.ts index 86d26cfa48b..742a88f21fc 100644 --- a/src/appdistribution/client.spec.ts +++ b/src/appdistribution/client.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { join } from "path"; import * as fs from "fs-extra"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { rmSync } from "node:fs"; import * as sinon from "sinon"; import * as tmp from "tmp"; diff --git a/src/apptesting/invokeTests.spec.ts b/src/apptesting/invokeTests.spec.ts index 55027bc3e8e..af65f61c8d0 100644 --- a/src/apptesting/invokeTests.spec.ts +++ b/src/apptesting/invokeTests.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { appTestingOrigin } from "../api"; import { invokeTests, pollInvocationStatus } from "./invokeTests"; import { FirebaseError } from "../error"; diff --git a/src/command.spec.ts b/src/command.spec.ts index 11b6b7f36d4..2c25bc7b5a6 100644 --- a/src/command.spec.ts +++ b/src/command.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import * as sinon from "sinon"; import * as rc from "./rc"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import { configstore } from "./configstore"; import { Command, validateProjectId } from "./command"; diff --git a/src/commands/crashlytics-sourcemap-upload.spec.ts b/src/commands/crashlytics-sourcemap-upload.spec.ts index e19f0793944..9e0eded332c 100644 --- a/src/commands/crashlytics-sourcemap-upload.spec.ts +++ b/src/commands/crashlytics-sourcemap-upload.spec.ts @@ -57,7 +57,7 @@ describe("crashlytics:sourcemap:upload", () => { execSyncStub.withArgs("git rev-parse HEAD").returns(Buffer.from("a".repeat(40))); clientPatchStub = sandbox.stub(Client.prototype, "patch").resolves({ status: 200, - response: {} as unknown as import("node-fetch").Response, + response: {} as unknown as Response, body: {}, }); }); diff --git a/src/crashlytics/events.spec.ts b/src/crashlytics/events.spec.ts index 0a6dce511fd..406534b50cc 100644 --- a/src/crashlytics/events.spec.ts +++ b/src/crashlytics/events.spec.ts @@ -1,5 +1,5 @@ import * as chai from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as chaiAsPromised from "chai-as-promised"; import { listEvents, batchGetEvents } from "./events"; diff --git a/src/crashlytics/issues.spec.ts b/src/crashlytics/issues.spec.ts index c62dcbef515..abaabb9a6fe 100644 --- a/src/crashlytics/issues.spec.ts +++ b/src/crashlytics/issues.spec.ts @@ -1,5 +1,5 @@ import * as chai from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as chaiAsPromised from "chai-as-promised"; import { getIssue, updateIssue } from "./issues"; diff --git a/src/crashlytics/notes.spec.ts b/src/crashlytics/notes.spec.ts index 38cb66c54a1..f94bf42e34b 100644 --- a/src/crashlytics/notes.spec.ts +++ b/src/crashlytics/notes.spec.ts @@ -1,5 +1,5 @@ import * as chai from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as chaiAsPromised from "chai-as-promised"; import { createNote, deleteNote, listNotes } from "./notes"; diff --git a/src/crashlytics/reports.spec.ts b/src/crashlytics/reports.spec.ts index a6c06280cb0..e5762f4ff94 100644 --- a/src/crashlytics/reports.spec.ts +++ b/src/crashlytics/reports.spec.ts @@ -1,5 +1,5 @@ import * as chai from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as chaiAsPromised from "chai-as-promised"; import { CrashlyticsReport, getReport, simplifyReport } from "./reports"; diff --git a/src/database/import.spec.ts b/src/database/import.spec.ts index 332fb4c609d..b0534dbabad 100644 --- a/src/database/import.spec.ts +++ b/src/database/import.spec.ts @@ -1,17 +1,29 @@ -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as stream from "stream"; +import * as sinon from "sinon"; +import * as auth from "../auth"; import * as utils from "../utils"; import { expect } from "chai"; import DatabaseImporter from "./import"; import { FirebaseError } from "../error"; -import { FetchError } from "node-fetch"; const dbUrl = new URL("https://test-db.firebaseio.com/foo"); const payloadSize = 1024 * 1024 * 10; const concurrencyLimit = 5; describe("DatabaseImporter", () => { + let authStub: sinon.SinonStub | undefined; + before(() => { + if (typeof (auth.getAccessToken as any).restore !== "function") { + authStub = sinon.stub(auth, "getAccessToken").resolves({ access_token: "owner" } as any); + } + }); + after(() => { + if (authStub) { + authStub.restore(); + } + }); const DATA = { a: 100, b: [true, "bar", { f: { g: 0, h: 1 }, i: "baz" }], c: { d: false } }; let DATA_STREAM: stream.Readable; @@ -164,7 +176,7 @@ describe("DatabaseImporter", () => { }); it("retries non-fatal connection timeout error", async () => { - const timeoutErr = new FetchError("connect ETIMEDOUT", "system"); + const timeoutErr = new Error("connect ETIMEDOUT") as any; timeoutErr.code = "ETIMEDOUT"; nock("https://test-db.firebaseio.com").get("/foo.json?shallow=true").reply(200); diff --git a/src/database/listRemote.spec.ts b/src/database/listRemote.spec.ts index ded087ef8c5..a3b11efd646 100644 --- a/src/database/listRemote.spec.ts +++ b/src/database/listRemote.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as utils from "../utils"; import { realtimeOrigin } from "../api"; diff --git a/src/database/removeRemote.spec.ts b/src/database/removeRemote.spec.ts index 2bf38afd438..0db45d00628 100644 --- a/src/database/removeRemote.spec.ts +++ b/src/database/removeRemote.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as utils from "../utils"; import { RTDBRemoveRemote } from "./removeRemote"; diff --git a/src/dataconnect/dataplaneClient.spec.ts b/src/dataconnect/dataplaneClient.spec.ts index 580dc9cfaa3..04374712f9e 100644 --- a/src/dataconnect/dataplaneClient.spec.ts +++ b/src/dataconnect/dataplaneClient.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as chai from "chai"; import { dataconnectDataplaneClient, diff --git a/src/deploy/dataconnect/deploy.spec.ts b/src/deploy/dataconnect/deploy.spec.ts index 8a4298aa7d8..648696bd6a9 100644 --- a/src/deploy/dataconnect/deploy.spec.ts +++ b/src/deploy/dataconnect/deploy.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import * as deploy from "./deploy"; import * as utils from "../../utils"; import * as projectUtils from "../../projectUtils"; diff --git a/src/deploy/dataconnect/prepare.spec.ts b/src/deploy/dataconnect/prepare.spec.ts index bc6980cf0d4..6dbb61cade1 100644 --- a/src/deploy/dataconnect/prepare.spec.ts +++ b/src/deploy/dataconnect/prepare.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import * as prepare from "./prepare"; import * as load from "../../dataconnect/load"; import * as utils from "../../utils"; diff --git a/src/deploy/dataconnect/release.spec.ts b/src/deploy/dataconnect/release.spec.ts index b3d7feb2c34..c71163d004e 100644 --- a/src/deploy/dataconnect/release.spec.ts +++ b/src/deploy/dataconnect/release.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import * as release from "./release"; import * as utils from "../../utils"; import * as projectUtils from "../../projectUtils"; diff --git a/src/deploy/functions/ensure.spec.ts b/src/deploy/functions/ensure.spec.ts index 1efd428eb47..81d17dba0ee 100644 --- a/src/deploy/functions/ensure.spec.ts +++ b/src/deploy/functions/ensure.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { FirebaseError } from "../../error"; import { logger } from "../../logger"; diff --git a/src/deploy/functions/runtimes/discovery/index.spec.ts b/src/deploy/functions/runtimes/discovery/index.spec.ts index 3e617adb8fa..ab45a412130 100644 --- a/src/deploy/functions/runtimes/discovery/index.spec.ts +++ b/src/deploy/functions/runtimes/discovery/index.spec.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import * as fs from "fs/promises"; import * as yaml from "yaml"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../../../test/helpers/nock"; import * as api from "../../../../api"; import { FirebaseError } from "../../../../error"; diff --git a/src/deploy/remoteconfig/remoteconfig.spec.ts b/src/deploy/remoteconfig/remoteconfig.spec.ts index dc56e504938..ec7bc64077e 100644 --- a/src/deploy/remoteconfig/remoteconfig.spec.ts +++ b/src/deploy/remoteconfig/remoteconfig.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import * as sinon from "sinon"; import { remoteConfigApiOrigin } from "../../api"; diff --git a/src/downloadUtils.spec.ts b/src/downloadUtils.spec.ts index 6d4bb57bd01..5f46cffb549 100644 --- a/src/downloadUtils.spec.ts +++ b/src/downloadUtils.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { readFileSync } from "fs-extra"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import { gunzipSync, gzipSync } from "zlib"; import { downloadToTmp } from "./downloadUtils"; diff --git a/src/emulator/auth/cloudFunctions.spec.ts b/src/emulator/auth/cloudFunctions.spec.ts index 254a2e608a1..6625dfa8186 100644 --- a/src/emulator/auth/cloudFunctions.spec.ts +++ b/src/emulator/auth/cloudFunctions.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { AuthCloudFunction } from "./cloudFunctions"; import { EmulatorRegistry } from "../registry"; diff --git a/src/emulator/auth/emailLink.spec.ts b/src/emulator/auth/emailLink.spec.ts index 3a9bcb70f70..38dc8bff650 100644 --- a/src/emulator/auth/emailLink.spec.ts +++ b/src/emulator/auth/emailLink.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; import { FirebaseJwtPayload, parseBlockingFunctionJwt } from "./operations"; import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; diff --git a/src/emulator/auth/idp.spec.ts b/src/emulator/auth/idp.spec.ts index b02b270160a..abf0725765a 100644 --- a/src/emulator/auth/idp.spec.ts +++ b/src/emulator/auth/idp.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; import { FirebaseJwtPayload } from "./operations"; import { PROVIDER_PASSWORD, SIGNIN_METHOD_EMAIL_LINK } from "./state"; diff --git a/src/emulator/auth/mfa.spec.ts b/src/emulator/auth/mfa.spec.ts index f35e58320be..5f0a5ecaca6 100644 --- a/src/emulator/auth/mfa.spec.ts +++ b/src/emulator/auth/mfa.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; import { diff --git a/src/emulator/auth/password.spec.ts b/src/emulator/auth/password.spec.ts index 94a27e276ab..88813396ba2 100644 --- a/src/emulator/auth/password.spec.ts +++ b/src/emulator/auth/password.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; import { FirebaseJwtPayload } from "./operations"; import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; diff --git a/src/emulator/auth/phone.spec.ts b/src/emulator/auth/phone.spec.ts index 48050035367..5441c19cbad 100644 --- a/src/emulator/auth/phone.spec.ts +++ b/src/emulator/auth/phone.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; import { FirebaseJwtPayload } from "./operations"; import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; diff --git a/src/emulator/auth/signUp.spec.ts b/src/emulator/auth/signUp.spec.ts index ca62e4b9079..15b94d4d980 100644 --- a/src/emulator/auth/signUp.spec.ts +++ b/src/emulator/auth/signUp.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; import { FirebaseJwtPayload } from "./operations"; import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; diff --git a/src/emulator/functionsRuntimeWorker.spec.ts b/src/emulator/functionsRuntimeWorker.spec.ts index ec7347778a9..03dcdebccab 100644 --- a/src/emulator/functionsRuntimeWorker.spec.ts +++ b/src/emulator/functionsRuntimeWorker.spec.ts @@ -1,5 +1,7 @@ import * as httpMocks from "node-mocks-http"; -import * as nock from "nock"; +import * as sinon from "sinon"; +import * as http from "http"; +import { PassThrough } from "stream"; import { expect } from "chai"; import { FunctionsRuntimeInstance, IPCConn } from "./functionsEmulator"; import { EventEmitter } from "events"; @@ -90,9 +92,47 @@ function mockTrigger(id: string): EmulatedTriggerDefinition { } describe("FunctionsRuntimeWorker", () => { + let requestStub: sinon.SinonStub; + + beforeEach(() => { + requestStub = sinon.stub(http, "request"); + }); + + afterEach(() => { + requestStub.restore(); + }); + + function mockSuccessfulRequest(statusCode: number = 200, body: string = "") { + requestStub.callsFake((options: any, callback: any) => { + const mockRes = new PassThrough(); + (mockRes as any).statusCode = statusCode; + (mockRes as any).headers = {}; + + const mockReq = new PassThrough(); + if (callback) { + process.nextTick(() => { + callback(mockRes); + mockRes.push(body); + mockRes.push(null); + }); + } + return mockReq as any; + }); + } + + function mockFailedRequest(err: Error) { + requestStub.callsFake(() => { + const mockReq = new PassThrough(); + process.nextTick(() => { + mockReq.emit("error", err); + }); + return mockReq as any; + }); + } + describe("RuntimeWorker", () => { it("goes from created --> idle --> busy --> idle in normal operation", async () => { - const scope = nock("http://localhost").get("/").reply(200); + mockSuccessfulRequest(200); const worker = new RuntimeWorker("trigger", new MockRuntimeInstance(), {}); const counter = new WorkerStateCounter(worker); @@ -102,7 +142,6 @@ describe("FunctionsRuntimeWorker", () => { { method: "GET", path: "/" }, httpMocks.createResponse({ eventEmitter: EventEmitter }), ); - scope.done(); expect(counter.counts.CREATED).to.eql(1); expect(counter.counts.BUSY).to.eql(1); @@ -111,7 +150,7 @@ describe("FunctionsRuntimeWorker", () => { }); it("goes from created --> idle --> busy --> finished when there's an error", async () => { - const scope = nock("http://localhost").get("/").replyWithError("boom"); + mockFailedRequest(new Error("boom")); const worker = new RuntimeWorker("trigger", new MockRuntimeInstance(), {}); const counter = new WorkerStateCounter(worker); @@ -121,7 +160,6 @@ describe("FunctionsRuntimeWorker", () => { { method: "GET", path: "/" }, httpMocks.createResponse({ eventEmitter: EventEmitter }), ); - scope.done(); expect(counter.counts.CREATED).to.eql(1); expect(counter.counts.IDLE).to.eql(1); @@ -131,7 +169,7 @@ describe("FunctionsRuntimeWorker", () => { }); it("goes from created --> busy --> finishing --> finished when marked", async () => { - const scope = nock("http://localhost").get("/").replyWithError("boom"); + mockFailedRequest(new Error("boom")); const worker = new RuntimeWorker("trigger", new MockRuntimeInstance(), {}); const counter = new WorkerStateCounter(worker); @@ -142,7 +180,6 @@ describe("FunctionsRuntimeWorker", () => { worker.state = RuntimeWorkerState.FINISHING; }); await worker.request({ method: "GET", path: "/" }, resp); - scope.done(); expect(counter.counts.CREATED).to.eql(1); expect(counter.counts.IDLE).to.eql(1); @@ -155,7 +192,7 @@ describe("FunctionsRuntimeWorker", () => { describe("RuntimeWorkerPool", () => { it("properly manages a single worker", async () => { - const scope = nock("http://localhost").get("/").reply(200); + mockSuccessfulRequest(200); const pool = new RuntimeWorkerPool(); const triggerId = "region-trigger1"; @@ -176,7 +213,6 @@ describe("FunctionsRuntimeWorker", () => { expect(pool.getIdleWorker(triggerId)).to.be.undefined; }); await worker.request({ method: "GET", path: "/" }, resp); - scope.done(); // Completed handling request. Worker should be IDLE again. expect(pool.getIdleWorker(triggerId)).to.eql(worker); @@ -190,7 +226,7 @@ describe("FunctionsRuntimeWorker", () => { expect(pool.getIdleWorker(triggerId)).to.be.undefined; // Add a worker to the pool that's destined to fail. - const scope = nock("http://localhost").get("/").replyWithError("boom"); + mockFailedRequest(new Error("boom")); const worker = pool.addWorker(mockTrigger(triggerId), new MockRuntimeInstance(), {}); worker.readyForWork(); expect(pool.getIdleWorker(triggerId)).to.eql(worker); @@ -200,7 +236,6 @@ describe("FunctionsRuntimeWorker", () => { { method: "GET", path: "/" }, httpMocks.createResponse({ eventEmitter: EventEmitter }), ); - scope.done(); // Confirm there are no idle workers. expect(pool.getIdleWorker(triggerId)).to.be.undefined; @@ -219,13 +254,12 @@ describe("FunctionsRuntimeWorker", () => { const idleWorkerCounter = new WorkerStateCounter(idleWorker); // Add a worker to the pool that's destined to fail. - const scope = nock("http://localhost").get("/").reply(200); + mockSuccessfulRequest(200); const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); resp.on("end", () => { pool.exit(); }); await busyWorker.request({ method: "GET", path: "/" }, resp); - scope.done(); expect(busyWorkerCounter.counts.IDLE).to.eql(1); expect(busyWorkerCounter.counts.BUSY).to.eql(1); @@ -250,13 +284,12 @@ describe("FunctionsRuntimeWorker", () => { const idleWorkerCounter = new WorkerStateCounter(idleWorker); // Add a worker to the pool that's destined to fail. - const scope = nock("http://localhost").get("/").reply(200); + mockSuccessfulRequest(200); const resp = httpMocks.createResponse({ eventEmitter: EventEmitter }); resp.on("end", () => { pool.refresh(); }); await busyWorker.request({ method: "GET", path: "/" }, resp); - scope.done(); expect(busyWorkerCounter.counts.BUSY).to.eql(1); expect(busyWorkerCounter.counts.FINISHING).to.eql(1); @@ -268,7 +301,7 @@ describe("FunctionsRuntimeWorker", () => { }); it("gives assigns all triggers to the same worker in sequential mode", async () => { - const scope = nock("http://localhost").get("/").reply(200); + mockSuccessfulRequest(200); const triggerId1 = "region-abc"; const triggerId2 = "region-def"; @@ -283,7 +316,6 @@ describe("FunctionsRuntimeWorker", () => { expect(pool.readyForWork(triggerId2)).to.be.false; }); await worker.request({ method: "GET", path: "/" }, resp); - scope.done(); expect(pool.readyForWork(triggerId1)).to.be.true; expect(pool.readyForWork(triggerId2)).to.be.true; diff --git a/src/emulator/taskQueue.spec.ts b/src/emulator/taskQueue.spec.ts index fbb696f58a5..00736525692 100644 --- a/src/emulator/taskQueue.spec.ts +++ b/src/emulator/taskQueue.spec.ts @@ -1,7 +1,7 @@ import * as _ from "lodash"; import * as sinon from "sinon"; -import * as nodeFetch from "node-fetch"; import { expect } from "chai"; +import nock from "../test/helpers/nock"; import { EmulatedTask, EmulatedTaskMetadata, Queue, TaskQueue, TaskStatus } from "./taskQueue"; import { RateLimits, RetryConfig, Task, TaskQueueConfig } from "./tasksEmulator"; @@ -155,7 +155,7 @@ describe("Task Queue", () => { const mockTask: Task = { name: "", httpRequest: { - url: "", + url: "http://website.com/", oidcToken: { serviceAccountEmail: "test-user@email.com" }, body: { test: "test" }, headers: {}, @@ -180,23 +180,16 @@ describe("Task Queue", () => { let TEST_TASK: EmulatedTask; const NOW = 1000 * 60; - const stubs: sinon.SinonStub[] = []; - - before(() => { - sinon.stub(Date, "now").returns(NOW); - }); - - after(() => { - sinon.restore(); - }); + let dateStub: sinon.SinonStub; beforeEach(() => { TEST_TASK = _.cloneDeep(mockEmulatedTask); TEST_TASK.metadata.currentBackoff = 0; + dateStub = sinon.stub(Date, "now").returns(NOW); }); afterEach(() => { - stubs.forEach((s) => s.restore()); + dateStub.restore(); }); // Handle Retry Tests @@ -284,56 +277,50 @@ describe("Task Queue", () => { }); describe("Run Task", () => { + afterEach(() => { + nock.cleanAll(); + }); + it("should call the task url", () => { const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); - const response = new nodeFetch.Response(undefined, { status: 200 }); - const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); - stubs.push(fetchStub); + nock("http://website.com").post("/").reply(200); taskQueue.setDispatch([TEST_TASK]); const res = taskQueue.runTask(0).then(() => { - expect(fetchStub).to.have.been.calledOnce.and.calledWith(TEST_TASK.task.httpRequest.url); + expect(nock.isDone()).to.be.true; }); return res; }); it("Should wait until the backoff time has elapsed", () => { const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); - const response = new nodeFetch.Response(undefined, { status: 200 }); - TEST_TASK.metadata.lastRunTime = NOW - 1000; TEST_TASK.metadata.currentBackoff = 3; - const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); - stubs.push(fetchStub); + nock("http://website.com").post("/").reply(200); taskQueue.setDispatch([TEST_TASK]); const res = taskQueue.runTask(0).then(() => { - expect(fetchStub).to.not.have.been.called; + expect(nock.isDone()).to.be.false; }); return res; }); it("Should run if the backoff time has elapsed", () => { const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); - const response = new nodeFetch.Response(undefined, { status: 200 }); TEST_TASK.metadata.lastRunTime = NOW - 3 * 1000; TEST_TASK.metadata.currentBackoff = 2; - const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); - stubs.push(fetchStub); + nock("http://website.com").post("/").reply(200); taskQueue.setDispatch([TEST_TASK]); const res = taskQueue.runTask(0).then(() => { - expect(fetchStub).to.have.been.calledOnce.and.calledWith(TEST_TASK.task.httpRequest.url); + expect(nock.isDone()).to.be.true; }); return res; }); it("should properly update metadata on success", () => { const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); - const response = new nodeFetch.Response(undefined, { status: 200 }); - - const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); - stubs.push(fetchStub); + nock("http://website.com").post("/").reply(200); taskQueue.setDispatch([TEST_TASK]); const res = taskQueue.runTask(0).then(() => { expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.FINISHED); @@ -343,10 +330,7 @@ describe("Task Queue", () => { it("should properly update metadata on failure", () => { const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); - const response = new nodeFetch.Response(undefined, { status: 500 }); - - const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); - stubs.push(fetchStub); + nock("http://website.com").post("/").reply(500); taskQueue.setDispatch([TEST_TASK]); const res = taskQueue.runTask(0).then(() => { expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.RETRY); diff --git a/src/ensureApiEnabled.spec.ts b/src/ensureApiEnabled.spec.ts index 7bfa482f3d8..64df6fe2582 100644 --- a/src/ensureApiEnabled.spec.ts +++ b/src/ensureApiEnabled.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import * as sinon from "sinon"; import { configstore } from "./configstore"; import { check, ensure, POLL_SETTINGS } from "./ensureApiEnabled"; diff --git a/src/extensions/extensionsApi.spec.ts b/src/extensions/extensionsApi.spec.ts index 0f11b4dd9ca..405ad00d5f6 100644 --- a/src/extensions/extensionsApi.spec.ts +++ b/src/extensions/extensionsApi.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; import { FirebaseError } from "../error"; diff --git a/src/extensions/provisioningHelper.spec.ts b/src/extensions/provisioningHelper.spec.ts index df36f2dc7f8..9062fa95602 100644 --- a/src/extensions/provisioningHelper.spec.ts +++ b/src/extensions/provisioningHelper.spec.ts @@ -1,4 +1,4 @@ -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { expect } from "chai"; import * as api from "../api"; diff --git a/src/extensions/publisherApi.spec.ts b/src/extensions/publisherApi.spec.ts index 6d7a4f510bb..ff725d78b46 100644 --- a/src/extensions/publisherApi.spec.ts +++ b/src/extensions/publisherApi.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; import * as refs from "./refs"; diff --git a/src/extensions/secretUtils.spec.ts b/src/extensions/secretUtils.spec.ts index 72bb7629448..0b15428ac83 100644 --- a/src/extensions/secretUtils.spec.ts +++ b/src/extensions/secretUtils.spec.ts @@ -1,4 +1,4 @@ -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { expect } from "chai"; import * as api from "../api"; diff --git a/src/extensions/tos.spec.ts b/src/extensions/tos.spec.ts index 72c630380d3..0b76e6f7a87 100644 --- a/src/extensions/tos.spec.ts +++ b/src/extensions/tos.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; import * as tos from "./tos"; diff --git a/src/extensions/updateHelper.spec.ts b/src/extensions/updateHelper.spec.ts index 8be0ad45ecb..6cb3c767ee0 100644 --- a/src/extensions/updateHelper.spec.ts +++ b/src/extensions/updateHelper.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as sinon from "sinon"; import { FirebaseError } from "../error"; diff --git a/src/fetchWebSetup.spec.ts b/src/fetchWebSetup.spec.ts index acb6fe3c878..1f07d63df01 100644 --- a/src/fetchWebSetup.spec.ts +++ b/src/fetchWebSetup.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import * as sinon from "sinon"; import { configstore } from "./configstore"; diff --git a/src/gcp/artifactregistry.spec.ts b/src/gcp/artifactregistry.spec.ts index 36f24af1d86..c8e9353b925 100644 --- a/src/gcp/artifactregistry.spec.ts +++ b/src/gcp/artifactregistry.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as sinon from "sinon"; import * as artifactRegistry from "./artifactregistry"; import { artifactRegistryDomain } from "../api"; diff --git a/src/gcp/auth.spec.ts b/src/gcp/auth.spec.ts index 7998ea18680..28bcce33d9d 100644 --- a/src/gcp/auth.spec.ts +++ b/src/gcp/auth.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as auth from "./auth"; import { identityOrigin } from "../api"; diff --git a/src/gcp/cloudbilling.spec.ts b/src/gcp/cloudbilling.spec.ts index 1ca2ef0eb19..872eb48e254 100644 --- a/src/gcp/cloudbilling.spec.ts +++ b/src/gcp/cloudbilling.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as sinon from "sinon"; import * as cloudbilling from "./cloudbilling"; import { cloudbillingOrigin } from "../api"; diff --git a/src/gcp/cloudbuild.spec.ts b/src/gcp/cloudbuild.spec.ts index 0af01d5ef52..a2be251d3ab 100644 --- a/src/gcp/cloudbuild.spec.ts +++ b/src/gcp/cloudbuild.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as cloudbuild from "./cloudbuild"; import { cloudbuildOrigin } from "../api"; diff --git a/src/gcp/cloudfunctions.spec.ts b/src/gcp/cloudfunctions.spec.ts index 2e202aa99ae..24cefd0fa0b 100644 --- a/src/gcp/cloudfunctions.spec.ts +++ b/src/gcp/cloudfunctions.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { functionsOrigin } from "../api"; diff --git a/src/gcp/cloudfunctionsv2.spec.ts b/src/gcp/cloudfunctionsv2.spec.ts index 8eef3a09b18..5ce7f74f9fd 100644 --- a/src/gcp/cloudfunctionsv2.spec.ts +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as cloudfunctionsv2 from "./cloudfunctionsv2"; import * as backend from "../deploy/functions/backend"; diff --git a/src/gcp/cloudlogging.spec.ts b/src/gcp/cloudlogging.spec.ts index f5cce38d2e8..bfd314c65f2 100644 --- a/src/gcp/cloudlogging.spec.ts +++ b/src/gcp/cloudlogging.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as cloudlogging from "./cloudlogging"; import { FirebaseError } from "../error"; diff --git a/src/gcp/cloudmonitoring.spec.ts b/src/gcp/cloudmonitoring.spec.ts index 70fd987a3bd..6638c4bb7a3 100644 --- a/src/gcp/cloudmonitoring.spec.ts +++ b/src/gcp/cloudmonitoring.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; import { FirebaseError } from "../error"; import { Aligner, CmQuery, queryTimeSeries, TimeSeriesView } from "./cloudmonitoring"; diff --git a/src/gcp/cloudscheduler.spec.ts b/src/gcp/cloudscheduler.spec.ts index 391d2557776..85e2900d29c 100644 --- a/src/gcp/cloudscheduler.spec.ts +++ b/src/gcp/cloudscheduler.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { FirebaseError } from "../error"; import * as api from "../api"; diff --git a/src/gcp/cloudsql/cloudsqladmin.spec.ts b/src/gcp/cloudsql/cloudsqladmin.spec.ts index 0fa134f7f5d..e2ce1ce0101 100644 --- a/src/gcp/cloudsql/cloudsqladmin.spec.ts +++ b/src/gcp/cloudsql/cloudsqladmin.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import * as sinon from "sinon"; import * as sqladmin from "../../gcp/cloudsql/cloudsqladmin"; diff --git a/src/gcp/firedata.spec.ts b/src/gcp/firedata.spec.ts index ab0f9bdac83..19df2cd5b6c 100644 --- a/src/gcp/firedata.spec.ts +++ b/src/gcp/firedata.spec.ts @@ -1,4 +1,4 @@ -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { APPHOSTING_TOS_ID, APP_CHECK_TOS_ID, diff --git a/src/gcp/iam.spec.ts b/src/gcp/iam.spec.ts index 6539d5b588e..56e7d2c5ea3 100644 --- a/src/gcp/iam.spec.ts +++ b/src/gcp/iam.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { resourceManagerOrigin } from "../api"; import * as iam from "./iam"; diff --git a/src/gcp/resourceManager.spec.ts b/src/gcp/resourceManager.spec.ts index f26b459d018..1145e70d73e 100644 --- a/src/gcp/resourceManager.spec.ts +++ b/src/gcp/resourceManager.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { addServiceAccountToRoles, serviceAccountHasRoles } from "./resourceManager"; import { Policy } from "./iam"; diff --git a/src/gemini/fdcExperience.spec.ts b/src/gemini/fdcExperience.spec.ts index d3faa0a4721..4fd97a9a974 100644 --- a/src/gemini/fdcExperience.spec.ts +++ b/src/gemini/fdcExperience.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { extractCodeBlock, generateSchema, generateOperation } from "./fdcExperience"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { dataconnectOrigin } from "../api"; import { Schema } from "./types"; diff --git a/src/hosting/api.spec.ts b/src/hosting/api.spec.ts index 009a1c2c78c..1267a761078 100644 --- a/src/hosting/api.spec.ts +++ b/src/hosting/api.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { identityOrigin, hostingApiOrigin } from "../api"; import { FirebaseError } from "../error"; diff --git a/src/hosting/cloudRunProxy.spec.ts b/src/hosting/cloudRunProxy.spec.ts index aa88a8bc8bc..72f210c4eed 100644 --- a/src/hosting/cloudRunProxy.spec.ts +++ b/src/hosting/cloudRunProxy.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as express from "express"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as sinon from "sinon"; import * as supertest from "supertest"; @@ -155,7 +155,7 @@ describe("cloudRunProxy", () => { }); it("should cache calls to look up Cloud Run service URLs", async () => { - const multiCallOrigin = "https://multiLookup-hash-uc.a.run.app"; + const multiCallOrigin = "https://multilookup-hash-uc.a.run.app"; const multiNock = nock(cloudRunApiOrigin()) .get("/v1/projects/project-foo/locations/us-central1/services/multiLookup") .reply(200, { status: { url: multiCallOrigin } }); diff --git a/src/hosting/functionsProxy.spec.ts b/src/hosting/functionsProxy.spec.ts index 82a4ac40674..09ab87cc555 100644 --- a/src/hosting/functionsProxy.spec.ts +++ b/src/hosting/functionsProxy.spec.ts @@ -1,7 +1,7 @@ import { cloneDeep } from "lodash"; import { expect } from "chai"; import * as express from "express"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as sinon from "sinon"; import * as supertest from "supertest"; diff --git a/src/hosting/initMiddleware.spec.ts b/src/hosting/initMiddleware.spec.ts index 89d92411a21..3135e055985 100644 --- a/src/hosting/initMiddleware.spec.ts +++ b/src/hosting/initMiddleware.spec.ts @@ -1,13 +1,13 @@ -import { createGunzip, createGzip } from "zlib"; +import { createGunzip, gzipSync } from "zlib"; import { expect } from "chai"; import * as express from "express"; import * as http from "http"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as portfinder from "portfinder"; import * as supertest from "supertest"; import { initMiddleware } from "./initMiddleware"; -import { streamToString, stringToStream } from "../utils"; +import { streamToString } from "../utils"; import { TemplateServerResponse } from "./implicitInit"; const templateServerRes: TemplateServerResponse = { @@ -133,8 +133,6 @@ describe("initMiddleware", () => { // automatically handle compressed data, so a real request against a real // server is the easiest way to confirm this behavior. const content = "this should be compressed"; - const contentStream = stringToStream(content); - const compressedStream = contentStream?.pipe(createGzip()); const app = express(); app.use(initMiddleware(templateServerRes)); @@ -154,7 +152,9 @@ describe("initMiddleware", () => { it("should return compressed data if it is returned compressed", async () => { nock("https://www.gstatic.com") .get("/firebasejs/v2.2.2/sample-sdk.js") - .reply(200, () => compressedStream, { "content-encoding": "gzip" }); + .reply(200, () => gzipSync(content), { + "content-encoding": "gzip", + }); const res = await new Promise((resolve, reject) => { const req = http.request( diff --git a/src/management/apps.spec.ts b/src/management/apps.spec.ts index 933bff11495..a4b000fad06 100644 --- a/src/management/apps.spec.ts +++ b/src/management/apps.spec.ts @@ -2,7 +2,7 @@ import * as mockfs from "mock-fs"; import { expect } from "chai"; import * as sinon from "sinon"; import * as fs from "fs"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; import * as appUtils from "../appUtils"; diff --git a/src/management/database.spec.ts b/src/management/database.spec.ts index a37ea6c32d1..b619d3b3366 100644 --- a/src/management/database.spec.ts +++ b/src/management/database.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; diff --git a/src/management/projects.spec.ts b/src/management/projects.spec.ts index eeef6f70960..8c1cb78eeaf 100644 --- a/src/management/projects.spec.ts +++ b/src/management/projects.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as api from "../api"; import * as projectManager from "./projects"; diff --git a/src/management/provisioning/provision.spec.ts b/src/management/provisioning/provision.spec.ts index a0d2bc9cac2..572e1622054 100644 --- a/src/management/provisioning/provision.spec.ts +++ b/src/management/provisioning/provision.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../test/helpers/nock"; import { firebaseApiOrigin } from "../../api"; import * as pollUtils from "../../operation-poller"; import { diff --git a/src/mcp/tools/index.spec.ts b/src/mcp/tools/index.spec.ts index a21575a87fc..075765f7723 100644 --- a/src/mcp/tools/index.spec.ts +++ b/src/mcp/tools/index.spec.ts @@ -23,6 +23,14 @@ describe("availableTools", () => { isBillingEnabled: true, }; + let listToolsStub: sinon.SinonStub; + beforeEach(() => { + listToolsStub = sinon.stub(OneMcpServer.prototype, "listTools").resolves([]); + }); + afterEach(() => { + listToolsStub.restore(); + }); + it("should return specific tools when enabledTools is provided", async () => { const tools = await availableTools(mockContext, [], [], ["firebase_login"]); diff --git a/src/mcp/tools/remoteconfig/update_template.spec.ts b/src/mcp/tools/remoteconfig/update_template.spec.ts index 1a6c9bd0566..16ce16582dd 100644 --- a/src/mcp/tools/remoteconfig/update_template.spec.ts +++ b/src/mcp/tools/remoteconfig/update_template.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "../../../test/helpers/nock"; import * as api from "../../../api"; import { RemoteConfigTemplate } from "../../../remoteconfig/interfaces"; import { update_template } from "./update_template"; diff --git a/src/operation-poller.spec.ts b/src/operation-poller.spec.ts index 6db9a7f1198..d7e963f095f 100644 --- a/src/operation-poller.spec.ts +++ b/src/operation-poller.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import * as sinon from "sinon"; import { FirebaseError } from "./error"; diff --git a/src/remoteconfig/deleteExperiment.spec.ts b/src/remoteconfig/deleteExperiment.spec.ts index 663963f22e7..dcff2df1843 100644 --- a/src/remoteconfig/deleteExperiment.spec.ts +++ b/src/remoteconfig/deleteExperiment.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as clc from "colorette"; import { remoteConfigApiOrigin } from "../api"; diff --git a/src/remoteconfig/deleteRollout.spec.ts b/src/remoteconfig/deleteRollout.spec.ts index 43ee7fff28e..b844e592e4d 100644 --- a/src/remoteconfig/deleteRollout.spec.ts +++ b/src/remoteconfig/deleteRollout.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { remoteConfigApiOrigin } from "../api"; import { FirebaseError } from "../error"; import { NAMESPACE_FIREBASE } from "./interfaces"; diff --git a/src/remoteconfig/get.spec.ts b/src/remoteconfig/get.spec.ts index b7056fb4b37..3d9e304763e 100644 --- a/src/remoteconfig/get.spec.ts +++ b/src/remoteconfig/get.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { remoteConfigApiOrigin } from "../api"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as remoteconfig from "./get"; import { RemoteConfigTemplate } from "./interfaces"; diff --git a/src/remoteconfig/getExperiment.spec.ts b/src/remoteconfig/getExperiment.spec.ts index 06bbf9578a3..06b6ed6ed13 100644 --- a/src/remoteconfig/getExperiment.spec.ts +++ b/src/remoteconfig/getExperiment.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { remoteConfigApiOrigin } from "../api"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as Table from "cli-table3"; import * as util from "util"; diff --git a/src/remoteconfig/getRollout.spec.ts b/src/remoteconfig/getRollout.spec.ts index 62fb91b7f76..5534954fa9c 100644 --- a/src/remoteconfig/getRollout.spec.ts +++ b/src/remoteconfig/getRollout.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { remoteConfigApiOrigin } from "../api"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as Table from "cli-table3"; import * as util from "util"; diff --git a/src/remoteconfig/listExperiments.spec.ts b/src/remoteconfig/listExperiments.spec.ts index 2c87e0185c4..c806599ebf4 100644 --- a/src/remoteconfig/listExperiments.spec.ts +++ b/src/remoteconfig/listExperiments.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as Table from "cli-table3"; import { remoteConfigApiOrigin } from "../api"; diff --git a/src/remoteconfig/listRollouts.spec.ts b/src/remoteconfig/listRollouts.spec.ts index 80f052ae649..12b4156e640 100644 --- a/src/remoteconfig/listRollouts.spec.ts +++ b/src/remoteconfig/listRollouts.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { remoteConfigApiOrigin } from "../api"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as Table from "cli-table3"; import { listRollouts, parseRolloutList } from "./listRollouts"; diff --git a/src/remoteconfig/rollback.spec.ts b/src/remoteconfig/rollback.spec.ts index 8be10710cc6..f1be3c1d73b 100644 --- a/src/remoteconfig/rollback.spec.ts +++ b/src/remoteconfig/rollback.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { remoteConfigApiOrigin } from "../api"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import { RemoteConfigTemplate } from "./interfaces"; import * as remoteconfig from "./rollback"; diff --git a/src/remoteconfig/versionslist.spec.ts b/src/remoteconfig/versionslist.spec.ts index 88e7005db60..8235cfc6384 100644 --- a/src/remoteconfig/versionslist.spec.ts +++ b/src/remoteconfig/versionslist.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { remoteConfigApiOrigin } from "../api"; -import * as nock from "nock"; +import nock from "../test/helpers/nock"; import * as remoteconfig from "./versionslist"; import { ListVersionsResult, Version } from "./interfaces"; diff --git a/src/requireTosAcceptance.spec.ts b/src/requireTosAcceptance.spec.ts index 33724bfddbc..d8c763a3975 100644 --- a/src/requireTosAcceptance.spec.ts +++ b/src/requireTosAcceptance.spec.ts @@ -1,4 +1,4 @@ -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import * as sinon from "sinon"; import { APPHOSTING_TOS_ID, APP_CHECK_TOS_ID } from "./gcp/firedata"; import { requireTosAcceptance } from "./requireTosAcceptance"; diff --git a/src/rtdb.spec.ts b/src/rtdb.spec.ts index c6e0207d4b2..c52ddf8c66b 100644 --- a/src/rtdb.spec.ts +++ b/src/rtdb.spec.ts @@ -8,7 +8,6 @@ import * as rtdb from "./rtdb"; import * as management from "./management/database"; import * as utils from "./utils"; import { FirebaseError } from "./error"; -import { Response } from "node-fetch"; const expect = chai.expect; chai.use(sinonChai); diff --git a/src/test/helpers/mocha-bootstrap.js b/src/test/helpers/mocha-bootstrap.js new file mode 100644 index 00000000000..acc7e4a4e9c --- /dev/null +++ b/src/test/helpers/mocha-bootstrap.js @@ -0,0 +1,27 @@ +const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); +const sinonChai = require("sinon-chai"); +const nock = require("nock"); +const nodeFetch = require("node-fetch"); + +// Route global fetch to node-fetch during Mocha tests to support standard nock matching +global.fetch = nodeFetch; +global.Headers = nodeFetch.Headers; +global.Request = nodeFetch.Request; +global.Response = nodeFetch.Response; + +if (typeof nodeFetch.Headers.prototype.getSetCookie !== "function") { + nodeFetch.Headers.prototype.getSetCookie = function () { + return this.raw()["set-cookie"] || []; + }; +} + +// Force nock to execute its side-effects (patching http/https) immediately on load +void nock; + +chai.use(chaiAsPromised); +chai.use(sinonChai); + +process.on("unhandledRejection", (error) => { + throw error; +}); diff --git a/src/test/helpers/mocha-bootstrap.ts b/src/test/helpers/mocha-bootstrap.ts deleted file mode 100644 index c71d8e04a42..00000000000 --- a/src/test/helpers/mocha-bootstrap.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as chai from "chai"; -import * as chaiAsPromised from "chai-as-promised"; -import * as sinonChai from "sinon-chai"; - -chai.use(chaiAsPromised); -chai.use(sinonChai); - -process.on("unhandledRejection", (error) => { - throw error; -}); diff --git a/src/test/helpers/nock.ts b/src/test/helpers/nock.ts new file mode 100644 index 00000000000..3966e7a7de3 --- /dev/null +++ b/src/test/helpers/nock.ts @@ -0,0 +1,553 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { MockAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici"; + +let originalFetch: unknown = undefined; + +let mockAgent: MockAgent = new MockAgent(); +mockAgent.disableNetConnect(); +setGlobalDispatcher(mockAgent); + +function resetMockAgent() { + if (mockAgent) { + mockAgent.close().catch(() => {}); + } + mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + setGlobalDispatcher(mockAgent); +} + +type ReplyCallback = (this: any, uri: any, body: any, callback?: any) => any; +type BodyCallback = (body: any) => boolean; +type PathCallback = (uri: any) => boolean; + +class NockInterceptor { + private queryObj?: any; + private persistFlag = false; + private timesVal = 1; + private delayVal = 0; + private scope?: any; + + constructor( + private clientWrapper: NockClient, + private client: any, + private options: { path: any; method: string; body?: any; options?: any }, + ) { + clientWrapper.registerInterceptor(this); + if (clientWrapper.isPersistent()) { + this.persistFlag = true; + } + } + + query(q: any) { + this.queryObj = q; + return this; + } + + persist() { + this.persistFlag = true; + return this; + } + + times(t: number) { + this.timesVal = t; + return this; + } + + once() { + this.timesVal = 1; + return this; + } + + twice() { + this.timesVal = 2; + return this; + } + + delay(ms: number) { + this.delayVal = ms; + return this; + } + + matchHeader(_name: string, _value: any) { + void _name; + void _value; + return this; + } + + done() { + // No-op + } + + isDone() { + if (!this.scope) { + return true; + } + const symbols = Object.getOwnPropertySymbols(this.scope); + const kMockDispatch = symbols.find((s) => s.toString() === "Symbol(mock dispatch)"); + if (kMockDispatch) { + const dispatch = this.scope[kMockDispatch]; + return !!dispatch.consumed; + } + return true; + } + + private getInterceptOptions(): any { + let path = this.options.path; + let queryObj = this.queryObj; + + if (typeof path === "string" && path.includes("?")) { + try { + const url = new URL(path, "http://localhost"); + path = url.pathname; + const queryParams: Record = {}; + url.searchParams.forEach((val, key) => { + queryParams[key] = val; + }); + queryObj = queryParams; + } catch { + // Fallback to original path if parsing fails + } + } + + if ((queryObj === undefined || queryObj === true) && typeof path === "string") { + path = new RegExp("^" + escapeRegExp(path) + "(\\?|$)"); + } + + let interceptPath = path; + if (queryObj !== undefined && queryObj !== true) { + const expectedPath = path; + const expectedQuery = queryObj; + interceptPath = (incomingPath: string) => { + try { + const url = new URL(incomingPath, "http://localhost"); + if (expectedPath instanceof RegExp) { + if (!expectedPath.test(url.pathname)) { + return false; + } + } else if (typeof expectedPath === "string") { + if (url.pathname !== expectedPath) { + return false; + } + } else if (typeof expectedPath === "function") { + if (!expectedPath(url.pathname)) { + return false; + } + } + + const actualQuery: Record = {}; + url.searchParams.forEach((val, key) => { + if (key in actualQuery) { + const prev = actualQuery[key]; + if (Array.isArray(prev)) { + prev.push(val); + } else { + actualQuery[key] = [prev, val]; + } + } else { + actualQuery[key] = val; + } + }); + if (typeof expectedQuery === "function") { + return !!expectedQuery(actualQuery); + } + + const compareValues = (expected: any, actual: any): boolean => { + if (Array.isArray(expected)) { + const actualArr = Array.isArray(actual) ? actual : [actual]; + if (expected.length !== actualArr.length) { + return false; + } + const sortedExpected = [...expected].map(String).sort(); + const sortedActual = [...actualArr].map(String).sort(); + for (let i = 0; i < sortedExpected.length; i++) { + if (sortedExpected[i] !== sortedActual[i]) { + return false; + } + } + return true; + } else { + if (Array.isArray(actual)) { + return false; + } + return String(expected) === String(actual); + } + }; + + for (const key of Object.keys(expectedQuery)) { + const expectedVal = expectedQuery[key]; + const actualVal = actualQuery[key]; + if (!compareValues(expectedVal, actualVal)) { + return false; + } + } + + for (const key of Object.keys(actualQuery)) { + if (!(key in expectedQuery)) { + return false; + } + } + + return true; + } catch { + return false; + } + }; + } + + const interceptOptions: any = { + path: interceptPath, + method: this.options.method, + }; + const isAnyStream = (body: any) => { + return !!( + body && + (typeof body.pipe === "function" || + typeof body.on === "function" || + typeof body.getReader === "function" || + typeof body.pipeTo === "function" || + (typeof globalThis.ReadableStream !== "undefined" && + body instanceof globalThis.ReadableStream)) + ); + }; + const normalizeBody = (body: any) => { + if (body && typeof body.toString === "function" && Buffer.isBuffer(body)) { + return body.toString("utf8"); + } + return body; + }; + + if (this.options.body !== undefined) { + let interceptBody = this.options.body; + if (typeof interceptBody === "function") { + interceptBody = (body: any) => { + body = normalizeBody(body); + if (isAnyStream(body)) { + return true; + } + try { + const parsed = JSON.parse(body); + return !!(this.options.body as BodyCallback)(parsed); + } catch { + return !!(this.options.body as BodyCallback)(body); + } + }; + } else if (interceptBody !== null && typeof interceptBody === "object") { + if (isAnyStream(interceptBody)) { + interceptBody = (body: any) => { + void body; + return true; + }; + } else { + const expectedObj = interceptBody; + interceptBody = (body: any) => { + body = normalizeBody(body); + if (isAnyStream(body)) { + return true; + } + try { + const parsed = JSON.parse(body); + return deepEqual(parsed, expectedObj); + } catch { + return false; + } + }; + } + } else if (typeof interceptBody === "string") { + const expectedStr = interceptBody; + interceptBody = (body: any) => { + body = normalizeBody(body); + if (isAnyStream(body)) { + return true; + } + return body === expectedStr; + }; + } + interceptOptions.body = interceptBody; + } + + if (this.options.options?.reqheaders) { + interceptOptions.headers = this.options.options.reqheaders; + } + return interceptOptions; + } + + reply(callback: ReplyCallback): NockClient; + reply( + statusCode: number, + responseBody?: ReplyCallback | Record | string | number | boolean | any[] | null, + headers?: any, + ): NockClient; + reply(statusCode: any, responseBody?: any, headers?: any) { + const interceptOptions = this.getInterceptOptions(); + const interceptor = this.client.intercept(interceptOptions); + let scope: any; + + if (typeof statusCode === "function") { + scope = interceptor.reply((opts: any) => { + const context = { + req: { + headers: opts.headers || {}, + method: opts.method, + path: opts.path, + }, + }; + const parsedBody = parseBodyIfNeeded(opts.body); + const result = (statusCode as ReplyCallback).call(context, opts.path, parsedBody); + if (Array.isArray(result)) { + return { + statusCode: result[0], + data: result[1] === undefined ? "" : result[1], + responseOptions: { headers: result[2] }, + }; + } + return { + statusCode: 200, + data: result === undefined ? "" : result, + }; + }); + } else if (typeof responseBody === "function") { + scope = interceptor.reply((opts: any) => { + const parsedBody = parseBodyIfNeeded(opts.body); + const result = (responseBody as ReplyCallback)(opts.path, parsedBody); + if (Array.isArray(result)) { + return { + statusCode: result[0], + data: result[1] === undefined ? "" : result[1], + responseOptions: { headers: result[2] }, + }; + } + return { + statusCode, + data: result === undefined ? "" : result, + responseOptions: { headers }, + }; + }); + } else { + scope = interceptor.reply(statusCode, responseBody, { headers }); + } + + if (this.persistFlag) { + scope.persist(); + } else if (this.timesVal > 1) { + scope.times(this.timesVal); + } + if (this.delayVal > 0) { + scope.delay(this.delayVal); + } + + this.scope = scope; + return this.clientWrapper; + } + + replyWithError(error: any) { + const interceptOptions = this.getInterceptOptions(); + const interceptor = this.client.intercept(interceptOptions); + let err: Error; + if (error instanceof Error) { + err = error; + } else if (typeof error === "object" && error !== null) { + err = new Error(error.message || ""); + Object.assign(err, error); + } else { + err = new Error(String(error)); + } + const scope = interceptor.replyWithError(err); + + if (this.persistFlag) { + scope.persist(); + } else if (this.timesVal > 1) { + scope.times(this.timesVal); + } + if (this.delayVal > 0) { + scope.delay(this.delayVal); + } + + this.scope = scope; + return this.clientWrapper; + } +} + +class NockClient { + private client: any; + private persistFlag = false; + private interceptors: NockInterceptor[] = []; + private host: string; + + constructor(host: string) { + let urlString = host; + if (!host.startsWith("http://") && !host.startsWith("https://")) { + urlString = "https://" + host; + } + const parsed = new URL(urlString); + this.host = parsed.origin; + this.client = mockAgent.get(this.host); + } + + getHost() { + return this.host; + } + + registerInterceptor(interceptor: NockInterceptor) { + this.interceptors.push(interceptor); + } + + isPersistent() { + return this.persistFlag; + } + + persist() { + this.persistFlag = true; + return this; + } + + matchHeader(name: string, value: any) { + void name; + void value; + return this; + } + + done() { + // No-op + } + + isDone() { + if (this.interceptors.length === 0) { + return true; + } + return this.interceptors.every((i) => i.isDone()); + } + + get(path: PathCallback | string | RegExp, options?: any) { + return new NockInterceptor(this, this.client, { path, method: "GET", options }); + } + + post( + path: PathCallback | string | RegExp, + body?: BodyCallback | Record | string | any[] | null, + options?: any, + ) { + return new NockInterceptor(this, this.client, { path, method: "POST", body, options }); + } + + put( + path: PathCallback | string | RegExp, + body?: BodyCallback | Record | string | any[] | null, + options?: any, + ) { + return new NockInterceptor(this, this.client, { path, method: "PUT", body, options }); + } + + patch( + path: PathCallback | string | RegExp, + body?: BodyCallback | Record | string | any[] | null, + options?: any, + ) { + return new NockInterceptor(this, this.client, { path, method: "PATCH", body, options }); + } + + delete(path: PathCallback | string | RegExp, options?: any) { + return new NockInterceptor(this, this.client, { path, method: "DELETE", options }); + } +} + +function nock(host: string, options?: any): NockClient { + void options; + if (originalFetch === undefined) { + originalFetch = globalThis.fetch; + globalThis.fetch = undiciFetch as any; + } + return new NockClient(host); +} + +namespace nock { + export type Body = any; + export type ReplyFnContext = any; + export type ReplyFnResult = any; + + export function cleanAll() { + resetMockAgent(); + if (originalFetch !== undefined) { + globalThis.fetch = originalFetch; + originalFetch = undefined; + } + } + + export function isDone() { + try { + mockAgent.assertNoPendingInterceptors(); + return true; + } catch { + return false; + } + } + + export function disableNetConnect() { + mockAgent.disableNetConnect(); + } + + export function enableNetConnect() { + mockAgent.enableNetConnect(); + } + + export function pendingMocks() { + return []; + } +} + +export default nock; + +function deepEqual(a: any, b: any): boolean { + if (b instanceof RegExp) { + return b.test(String(a)); + } + if (a instanceof RegExp) { + return a.test(String(b)); + } + if (a === b) return true; + if (a && b && typeof a === "object" && typeof b === "object") { + if (a.constructor !== b.constructor) return false; + if (Array.isArray(a)) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) return false; + } + return true; + } + const keys = Object.keys(a); + if (keys.length !== Object.keys(b).length) return false; + for (const key of keys) { + if (!Object.prototype.hasOwnProperty.call(b, key)) return false; + if (!deepEqual(a[key], b[key])) return false; + } + return true; + } + return false; +} + +function parseBodyIfNeeded(body: any): any { + if (typeof body === "string") { + try { + return JSON.parse(body); + } catch { + return body; + } + } + if (body && (body instanceof Uint8Array || Buffer.isBuffer(body))) { + const str = new TextDecoder().decode(body); + try { + return JSON.parse(str); + } catch { + return str; + } + } + return body; +} + +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/src/track.spec.ts b/src/track.spec.ts index f0e3bfd9ae9..fcdbd69f1cc 100644 --- a/src/track.spec.ts +++ b/src/track.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; -import * as nock from "nock"; +import nock from "./test/helpers/nock"; import { configstore } from "./configstore"; import * as track from "./track"; import * as auth from "./auth"; diff --git a/tsconfig.json b/tsconfig.json index 0e1027d3feb..5ed0b524dd9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,5 +27,8 @@ ], "exclude": [ "src/dynamicImport.js" - ] + ], + "ts-node": { + "transpileOnly": true + } }