From 02a20797e319ab04747b05c09be1f53e1d6ccfdd Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Wed, 28 Jan 2026 21:02:47 -0800 Subject: [PATCH 1/5] test: add unit tests for apphosting:backends:create --- .../apphosting-backends-create.spec.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/commands/apphosting-backends-create.spec.ts diff --git a/src/commands/apphosting-backends-create.spec.ts b/src/commands/apphosting-backends-create.spec.ts new file mode 100644 index 00000000000..69969d26a11 --- /dev/null +++ b/src/commands/apphosting-backends-create.spec.ts @@ -0,0 +1,120 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as nock from "nock"; +import { command } from "./apphosting-backends-create"; +import * as backend from "../apphosting/backend"; +import * as projectUtils from "../projectUtils"; +import * as requireAuthModule from "../requireAuth"; +import { FirebaseError } from "../error"; + +describe("apphosting:backends:create", () => { + const PROJECT_ID = "test-project"; + const WEB_APP_ID = "test-web-app"; + const BACKEND_ID = "test-backend"; + const REGION = "us-central1"; + const SERVICE_ACCOUNT = "test-sa"; + const ROOT_DIR = "."; + + let doSetupStub: sinon.SinonStub; + let needProjectIdStub: sinon.SinonStub; + let requireAuthStub: sinon.SinonStub; + + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + beforeEach(() => { + doSetupStub = sinon.stub(backend, "doSetup").resolves(); + needProjectIdStub = sinon.stub(projectUtils, "needProjectId").returns(PROJECT_ID); + requireAuthStub = sinon.stub(requireAuthModule, "requireAuth").resolves(); + + // Stub ensureApiEnabled calls + nock("https://serviceusage.googleapis.com") + .get(`/v1/projects/${PROJECT_ID}/services/firebaseapphosting.googleapis.com`) + .query(true) // match any query params + .reply(200, { state: "ENABLED" }); + + // Stub TOS acceptance check + nock("https://mobilesdk-pa.googleapis.com") + .get("/v1/accessmanagement/tos:getStatus") + .query(true) + .reply(200, { + perServiceStatus: [{ + tosId: "APP_HOSTING_TOS", + serviceStatus: { + status: "ACCEPTED" + } + }] + }); + }); + + afterEach(() => { + sinon.restore(); + nock.cleanAll(); + }); + + it("should throw error if non-interactive but missing required options", async () => { + const options = { nonInteractive: true }; + await expect(command.runner()(options)).to.be.rejectedWith( + FirebaseError, + "requires --backend and --primary-region" + ); + }); + + it("should throw error if non-interactive and just backend provided", async () => { + const options = { nonInteractive: true, backend: BACKEND_ID }; + await expect(command.runner()(options)).to.be.rejectedWith( + FirebaseError, + "requires --backend and --primary-region" + ); + }); + + it("should throw error if non-interactive and just region provided", async () => { + const options = { nonInteractive: true, primaryRegion: REGION }; + await expect(command.runner()(options)).to.be.rejectedWith( + FirebaseError, + "requires --backend and --primary-region" + ); + }); + + it("should call doSetup with correct arguments in interactive mode", async () => { + const options = {}; + await command.runner()(options); + + expect(doSetupStub).to.have.been.calledWith( + PROJECT_ID, + undefined, // nonInteractive + undefined, // webAppId + undefined, // backendId + undefined, // serviceAccount + undefined, // primaryRegion + undefined // rootDir + ); + }); + + it("should call doSetup with passed options in non-interactive mode", async () => { + const options = { + nonInteractive: true, + backend: BACKEND_ID, + primaryRegion: REGION, + app: WEB_APP_ID, + serviceAccount: SERVICE_ACCOUNT, + rootDir: ROOT_DIR, + }; + await command.runner()(options); + + expect(doSetupStub).to.have.been.calledWith( + PROJECT_ID, + true, + WEB_APP_ID, + BACKEND_ID, + SERVICE_ACCOUNT, + REGION, + ROOT_DIR + ); + }); +}); From acf0c54a624e278926719ce25e3cccf403fa522a Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 29 Jan 2026 14:02:57 -0800 Subject: [PATCH 2/5] formats --- .../apphosting-backends-create.spec.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/commands/apphosting-backends-create.spec.ts b/src/commands/apphosting-backends-create.spec.ts index 69969d26a11..2dc4ff70f72 100644 --- a/src/commands/apphosting-backends-create.spec.ts +++ b/src/commands/apphosting-backends-create.spec.ts @@ -16,8 +16,6 @@ describe("apphosting:backends:create", () => { const ROOT_DIR = "."; let doSetupStub: sinon.SinonStub; - let needProjectIdStub: sinon.SinonStub; - let requireAuthStub: sinon.SinonStub; before(() => { nock.disableNetConnect(); @@ -29,8 +27,8 @@ describe("apphosting:backends:create", () => { beforeEach(() => { doSetupStub = sinon.stub(backend, "doSetup").resolves(); - needProjectIdStub = sinon.stub(projectUtils, "needProjectId").returns(PROJECT_ID); - requireAuthStub = sinon.stub(requireAuthModule, "requireAuth").resolves(); + sinon.stub(projectUtils, "needProjectId").returns(PROJECT_ID); + sinon.stub(requireAuthModule, "requireAuth").resolves(); // Stub ensureApiEnabled calls nock("https://serviceusage.googleapis.com") @@ -43,12 +41,14 @@ describe("apphosting:backends:create", () => { .get("/v1/accessmanagement/tos:getStatus") .query(true) .reply(200, { - perServiceStatus: [{ - tosId: "APP_HOSTING_TOS", - serviceStatus: { - status: "ACCEPTED" - } - }] + perServiceStatus: [ + { + tosId: "APP_HOSTING_TOS", + serviceStatus: { + status: "ACCEPTED", + }, + }, + ], }); }); @@ -61,7 +61,7 @@ describe("apphosting:backends:create", () => { const options = { nonInteractive: true }; await expect(command.runner()(options)).to.be.rejectedWith( FirebaseError, - "requires --backend and --primary-region" + "requires --backend and --primary-region", ); }); @@ -69,7 +69,7 @@ describe("apphosting:backends:create", () => { const options = { nonInteractive: true, backend: BACKEND_ID }; await expect(command.runner()(options)).to.be.rejectedWith( FirebaseError, - "requires --backend and --primary-region" + "requires --backend and --primary-region", ); }); @@ -77,7 +77,7 @@ describe("apphosting:backends:create", () => { const options = { nonInteractive: true, primaryRegion: REGION }; await expect(command.runner()(options)).to.be.rejectedWith( FirebaseError, - "requires --backend and --primary-region" + "requires --backend and --primary-region", ); }); @@ -92,7 +92,7 @@ describe("apphosting:backends:create", () => { undefined, // backendId undefined, // serviceAccount undefined, // primaryRegion - undefined // rootDir + undefined, // rootDir ); }); @@ -114,7 +114,7 @@ describe("apphosting:backends:create", () => { BACKEND_ID, SERVICE_ACCOUNT, REGION, - ROOT_DIR + ROOT_DIR, ); }); }); From dbdcab2d408769ad2dfc4a8ccb9e98da204bd5f0 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 30 Jan 2026 11:10:07 -0800 Subject: [PATCH 3/5] PR fixes --- .../apphosting-backends-create.spec.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/commands/apphosting-backends-create.spec.ts b/src/commands/apphosting-backends-create.spec.ts index 2dc4ff70f72..ebf3a18f0a3 100644 --- a/src/commands/apphosting-backends-create.spec.ts +++ b/src/commands/apphosting-backends-create.spec.ts @@ -57,28 +57,26 @@ describe("apphosting:backends:create", () => { nock.cleanAll(); }); - it("should throw error if non-interactive but missing required options", async () => { - const options = { nonInteractive: true }; - await expect(command.runner()(options)).to.be.rejectedWith( - FirebaseError, - "requires --backend and --primary-region", - ); - }); - - it("should throw error if non-interactive and just backend provided", async () => { - const options = { nonInteractive: true, backend: BACKEND_ID }; - await expect(command.runner()(options)).to.be.rejectedWith( - FirebaseError, - "requires --backend and --primary-region", - ); - }); - - it("should throw error if non-interactive and just region provided", async () => { - const options = { nonInteractive: true, primaryRegion: REGION }; - await expect(command.runner()(options)).to.be.rejectedWith( - FirebaseError, - "requires --backend and --primary-region", - ); + [ + { + description: "missing required options", + options: { nonInteractive: true }, + }, + { + description: "just backend provided", + options: { nonInteractive: true, backend: BACKEND_ID }, + }, + { + description: "just region provided", + options: { nonInteractive: true, primaryRegion: REGION }, + }, + ].forEach(({ description, options }) => { + it(`should throw error if non-interactive and ${description}`, async () => { + await expect(command.runner()(options)).to.be.rejectedWith( + FirebaseError, + "requires --backend and --primary-region", + ); + }); }); it("should call doSetup with correct arguments in interactive mode", async () => { From e836db973aaad73f9a7650562501f32d6cc5155d Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 30 Jan 2026 14:10:04 -0800 Subject: [PATCH 4/5] Fix require auth stubbing --- src/commands/apphosting-backends-create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts index b09a02d585c..c3a264b79f5 100644 --- a/src/commands/apphosting-backends-create.ts +++ b/src/commands/apphosting-backends-create.ts @@ -28,7 +28,7 @@ export const command = new Command("apphosting:backends:create") "specify the primary region for the backend. Required with --non-interactive.", ) .option("--root-dir ", "specify the root directory for the backend.") - .before(requireAuth) + .before((options: Options) => requireAuth(options)) .before(ensureApiEnabled) .before(requireTosAcceptance(APPHOSTING_TOS_ID)) .action(async (options: Options) => { From 506443baf3ca02293a9b2c40d7a86446a5e29b84 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Tue, 3 Feb 2026 16:12:17 -0800 Subject: [PATCH 5/5] Fix leaky noninteractive --- src/commands/apphosting-backends-create.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/apphosting-backends-create.spec.ts b/src/commands/apphosting-backends-create.spec.ts index ebf3a18f0a3..936e4e81617 100644 --- a/src/commands/apphosting-backends-create.spec.ts +++ b/src/commands/apphosting-backends-create.spec.ts @@ -80,6 +80,9 @@ describe("apphosting:backends:create", () => { }); it("should call doSetup with correct arguments in interactive mode", async () => { + before(() => { + sinon.stub(process.stdin, "isTTY").value(true); + }); const options = {}; await command.runner()(options);