diff --git a/src/commands/apps-android-sha-create.spec.ts b/src/commands/apps-android-sha-create.spec.ts new file mode 100644 index 00000000000..7533909fce0 --- /dev/null +++ b/src/commands/apps-android-sha-create.spec.ts @@ -0,0 +1,81 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Command } from "../command"; +import * as projectUtils from "../projectUtils"; +import * as apps from "../management/apps"; +import { ShaCertificateType } from "../management/apps"; +import * as utils from "../utils"; +import { command, getCertHashType } from "./apps-android-sha-create"; +import * as auth from "../requireAuth"; + +describe("apps:android:sha:create", () => { + let sandbox: sinon.SinonSandbox; + let needProjectIdStub: sinon.SinonStub; + let createAppAndroidShaStub: sinon.SinonStub; + let promiseWithSpinnerStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(auth, "requireAuth").resolves(); + needProjectIdStub = sandbox.stub(projectUtils, "needProjectId").returns("test-project-id"); + createAppAndroidShaStub = sandbox + .stub(apps, "createAppAndroidSha") + .resolves({ name: "test-sha", shaHash: "test-hash", certType: ShaCertificateType.SHA_1 }); + promiseWithSpinnerStub = sandbox.stub(utils, "promiseWithSpinner").callThrough(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should be a Command", () => { + expect(command).to.be.an.instanceOf(Command); + }); + + describe("action", () => { + it("should create a SHA certificate", async () => { + const shaHash = "A1:B2:C3:D4:E5:F6:A1:B2:C3:D4:E5:F6:A1:B2:C3:D4:E5:F6:A1:B2"; // SHA-1 + const options = { + user: { email: "test@example.com" }, + tokens: { access_token: "an_access_token" }, + }; + await command.runner()("test-app-id", shaHash, options); + + expect(needProjectIdStub).to.have.been.calledOnce; + expect(createAppAndroidShaStub).to.have.been.calledOnceWith( + "test-project-id", + "test-app-id", + { + shaHash: shaHash, + certType: ShaCertificateType.SHA_1, + }, + ); + const spinnerText = promiseWithSpinnerStub.getCall(0).args[1]; + expect(spinnerText).to.include("Creating Android SHA certificate"); + }); + }); + + describe("getCertHashType", () => { + it("should return SHA_1 for a 40-character hash", () => { + const shaHash = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"; + expect(getCertHashType(shaHash)).to.equal(ShaCertificateType.SHA_1); + }); + + it("should return SHA_256 for a 64-character hash", () => { + const shaHash = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"; + expect(getCertHashType(shaHash)).to.equal(ShaCertificateType.SHA_256); + }); + + it("should return UNSPECIFIED for other hash lengths", () => { + const shaHash = "A1B2C3D4E5F6"; + expect(getCertHashType(shaHash)).to.equal( + ShaCertificateType.SHA_CERTIFICATE_TYPE_UNSPECIFIED, + ); + }); + + it("should handle colons in the hash", () => { + const shaHash = "A1:B2:C3:D4:E5:F6:A1:B2:C3:D4:E5:F6:A1:B2:C3:D4:E5:F6:A1:B2"; + expect(getCertHashType(shaHash)).to.equal(ShaCertificateType.SHA_1); + }); + }); +}); diff --git a/src/commands/apps-android-sha-create.ts b/src/commands/apps-android-sha-create.ts index 7a7609ea9ee..8ddd2359010 100644 --- a/src/commands/apps-android-sha-create.ts +++ b/src/commands/apps-android-sha-create.ts @@ -6,7 +6,7 @@ import { AppAndroidShaData, createAppAndroidSha, ShaCertificateType } from "../m import { requireAuth } from "../requireAuth"; import { promiseWithSpinner } from "../utils"; -function getCertHashType(shaHash: string): string { +export function getCertHashType(shaHash: string): string { shaHash = shaHash.replace(/:/g, ""); const shaHashCount = shaHash.length; if (shaHashCount === 40) return ShaCertificateType.SHA_1.toString(); diff --git a/src/commands/apps-android-sha-delete.spec.ts b/src/commands/apps-android-sha-delete.spec.ts new file mode 100644 index 00000000000..5b06d555038 --- /dev/null +++ b/src/commands/apps-android-sha-delete.spec.ts @@ -0,0 +1,50 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Command } from "../command"; +import * as projectUtils from "../projectUtils"; +import * as apps from "../management/apps"; +import * as utils from "../utils"; +import { command } from "./apps-android-sha-delete"; +import * as auth from "../requireAuth"; + +describe("apps:android:sha:delete", () => { + let sandbox: sinon.SinonSandbox; + let needProjectIdStub: sinon.SinonStub; + let deleteAppAndroidShaStub: sinon.SinonStub; + let promiseWithSpinnerStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(auth, "requireAuth").resolves(); + needProjectIdStub = sandbox.stub(projectUtils, "needProjectId").returns("test-project-id"); + deleteAppAndroidShaStub = sandbox.stub(apps, "deleteAppAndroidSha").resolves(); + promiseWithSpinnerStub = sandbox.stub(utils, "promiseWithSpinner").callThrough(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should be a Command", () => { + expect(command).to.be.an.instanceOf(Command); + }); + + describe("action", () => { + it("should delete a SHA certificate", async () => { + const options = { + user: { email: "test@example.com" }, + tokens: { access_token: "an_access_token" }, + }; + await command.runner()("test-app-id", "test-sha-id", options); + + expect(needProjectIdStub).to.have.been.calledOnce; + expect(deleteAppAndroidShaStub).to.have.been.calledOnceWith( + "test-project-id", + "test-app-id", + "test-sha-id", + ); + const spinnerText = promiseWithSpinnerStub.getCall(0).args[1]; + expect(spinnerText).to.include("Deleting Android SHA certificate hash"); + }); + }); +}); diff --git a/src/commands/apps-android-sha-list.spec.ts b/src/commands/apps-android-sha-list.spec.ts new file mode 100644 index 00000000000..3b18c5c381d --- /dev/null +++ b/src/commands/apps-android-sha-list.spec.ts @@ -0,0 +1,121 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as Table from "cli-table3"; +import { Command } from "../command"; +import * as projectUtils from "../projectUtils"; +import * as apps from "../management/apps"; +import { AppAndroidShaData, ShaCertificateType } from "../management/apps"; +import { command, logCertificatesList, logCertificatesCount } from "./apps-android-sha-list"; +import * as auth from "../requireAuth"; +import { logger } from "../logger"; + +describe("apps:android:sha:list", () => { + let sandbox: sinon.SinonSandbox; + let listAppAndroidShaStub: sinon.SinonStub; + let loggerInfoStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(auth, "requireAuth").resolves(); + sandbox.stub(projectUtils, "needProjectId").returns("test-project-id"); + listAppAndroidShaStub = sandbox.stub(apps, "listAppAndroidSha"); + loggerInfoStub = sandbox.stub(logger, "info"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should be a Command", () => { + expect(command).to.be.an.instanceOf(Command); + }); + + describe("action", () => { + const options = { + user: { email: "test@example.com" }, + tokens: { access_token: "an_access_token" }, + }; + + it("should list SHA certificates", async () => { + const certificates: AppAndroidShaData[] = [ + { + name: "projects/p/androidApps/a/sha/s1", + shaHash: "h1", + certType: ShaCertificateType.SHA_1, + }, + { + name: "projects/p/androidApps/a/sha/s2", + shaHash: "h2", + certType: ShaCertificateType.SHA_256, + }, + ]; + listAppAndroidShaStub.resolves(certificates); + + await command.runner()("test-app-id", options); + + expect(listAppAndroidShaStub).to.have.been.calledOnceWith("test-project-id", "test-app-id"); + expect(loggerInfoStub).to.have.been.calledWith(sinon.match("s1")); + expect(loggerInfoStub).to.have.been.calledWith(sinon.match("s2")); + }); + + it('should display "No SHA certificate hashes found." if no certificates exist', async () => { + listAppAndroidShaStub.resolves([]); + + await command.runner()("test-app-id", options); + + expect(listAppAndroidShaStub).to.have.been.calledOnceWith("test-project-id", "test-app-id"); + expect(loggerInfoStub).to.have.been.calledWith("No SHA certificate hashes found."); + }); + }); + + describe("logCertificatesList", () => { + it("should print a table of certificates", () => { + const certificates: AppAndroidShaData[] = [ + { + name: "projects/p/androidApps/app1/sha/sha1", + shaHash: "hash1", + certType: ShaCertificateType.SHA_1, + }, + { + name: "projects/p/androidApps/app2/sha/sha2", + shaHash: "hash2", + certType: ShaCertificateType.SHA_256, + }, + ]; + const tableSpy = sandbox.spy(Table.prototype, "push"); + + logCertificatesList(certificates); + + expect(tableSpy.getCall(0).args[0]).to.deep.equal([ + "app1", + "sha1", + "hash1", + ShaCertificateType.SHA_1, + ]); + expect(tableSpy.getCall(1).args[0]).to.deep.equal([ + "app2", + "sha2", + "hash2", + ShaCertificateType.SHA_256, + ]); + }); + + it('should print "No SHA certificate hashes found." if no certificates exist', () => { + logCertificatesList([]); + expect(loggerInfoStub).to.have.been.calledWith("No SHA certificate hashes found."); + }); + }); + + describe("logCertificatesCount", () => { + it("should print the total number of certificates", () => { + logCertificatesCount(5); + expect(loggerInfoStub).to.have.been.calledWith(""); + expect(loggerInfoStub).to.have.been.calledWith("5 SHA hash(es) total."); + }); + + it("should not print if count is 0", () => { + logCertificatesCount(0); + expect(loggerInfoStub).to.not.have.been.called; + }); + }); +}); diff --git a/src/commands/apps-android-sha-list.ts b/src/commands/apps-android-sha-list.ts index 65e0f260f1c..d1928c782fb 100644 --- a/src/commands/apps-android-sha-list.ts +++ b/src/commands/apps-android-sha-list.ts @@ -6,7 +6,7 @@ import { requireAuth } from "../requireAuth"; import { logger } from "../logger"; import { promiseWithSpinner } from "../utils"; -function logCertificatesList(certificates: AppAndroidShaData[]): void { +export function logCertificatesList(certificates: AppAndroidShaData[]): void { if (certificates.length === 0) { logger.info("No SHA certificate hashes found."); return; @@ -27,7 +27,7 @@ function logCertificatesList(certificates: AppAndroidShaData[]): void { logger.info(table.toString()); } -function logCertificatesCount(count: number = 0): void { +export function logCertificatesCount(count: number = 0): void { if (count === 0) { return; } diff --git a/src/commands/apps-create.spec.ts b/src/commands/apps-create.spec.ts new file mode 100644 index 00000000000..847fd7d4411 --- /dev/null +++ b/src/commands/apps-create.spec.ts @@ -0,0 +1,160 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Command } from "../command"; +import * as projectUtils from "../projectUtils"; +import { FirebaseError } from "../error"; +import * as apps from "../management/apps"; +import { AppPlatform } from "../management/apps"; +import * as prompt from "../prompt"; +import { command, logPostAppCreationInformation } from "./apps-create"; +import * as auth from "../requireAuth"; +import { logger } from "../logger"; + +describe("apps:create", () => { + let sandbox: sinon.SinonSandbox; + let needProjectIdStub: sinon.SinonStub; + let getAppPlatformStub: sinon.SinonStub; + let sdkInitStub: sinon.SinonStub; + let selectStub: sinon.SinonStub; + let loggerInfoStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(auth, "requireAuth").resolves(); + needProjectIdStub = sandbox.stub(projectUtils, "needProjectId").returns("test-project-id"); + getAppPlatformStub = sandbox.stub(apps, "getAppPlatform"); + sdkInitStub = sandbox.stub(apps, "sdkInit").resolves({ + name: "test-name", + projectId: "test-project-id", + appId: "test-app-id", + platform: AppPlatform.WEB, + displayName: "test-display-name", + }); + selectStub = sandbox.stub(prompt, "select"); + loggerInfoStub = sandbox.stub(logger, "info"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should be a Command", () => { + expect(command).to.be.an.instanceOf(Command); + }); + + describe("action", () => { + const options = { + nonInteractive: false, + user: { email: "test@example.com" }, + tokens: { access_token: "an_access_token" }, + }; + + it("should throw if platform is not provided in non-interactive mode", async () => { + getAppPlatformStub.returns(AppPlatform.ANY); + const nonInteractiveOptions = { ...options, nonInteractive: true }; + await expect(command.runner()("", undefined, nonInteractiveOptions)).to.be.rejectedWith( + FirebaseError, + "App platform must be provided", + ); + expect(needProjectIdStub).to.have.been.calledOnce; + }); + + it("should prompt for platform if not provided in interactive mode", async () => { + getAppPlatformStub.withArgs("").returns(AppPlatform.ANY); + getAppPlatformStub.withArgs("IOS").returns(AppPlatform.IOS); + selectStub.resolves("IOS"); + await command.runner()("", "test-display-name", options); + expect(selectStub).to.have.been.calledOnce; + expect(sdkInitStub).to.have.been.calledOnceWith( + AppPlatform.IOS, + sinon.match({ + displayName: "test-display-name", + nonInteractive: false, + }), + ); + }); + + it("should create an iOS app", async () => { + getAppPlatformStub.returns(AppPlatform.IOS); + const iosOptions = { ...options, bundleId: "test-bundle-id" }; + await command.runner()("IOS", "test-display-name", iosOptions); + expect(sdkInitStub).to.have.been.calledOnceWith( + AppPlatform.IOS, + sinon.match({ + bundleId: "test-bundle-id", + displayName: "test-display-name", + }), + ); + }); + + it("should create an Android app", async () => { + getAppPlatformStub.returns(AppPlatform.ANDROID); + const androidOptions = { ...options, packageName: "test-package-name" }; + await command.runner()("ANDROID", "test-display-name", androidOptions); + expect(sdkInitStub).to.have.been.calledOnceWith( + AppPlatform.ANDROID, + sinon.match({ + packageName: "test-package-name", + displayName: "test-display-name", + }), + ); + }); + + it("should create a Web app", async () => { + getAppPlatformStub.returns(AppPlatform.WEB); + await command.runner()("WEB", "test-display-name", options); + expect(sdkInitStub).to.have.been.calledOnceWith( + AppPlatform.WEB, + sinon.match({ + displayName: "test-display-name", + }), + ); + }); + }); + + describe("logPostAppCreationInformation", () => { + it("should log basic app information", () => { + const appMetadata: apps.WebAppMetadata = { + name: "test-name", + projectId: "test-project-id", + appId: "test-app-id", + platform: AppPlatform.WEB, + displayName: "test-display-name", + }; + logPostAppCreationInformation(appMetadata, AppPlatform.WEB); + expect(loggerInfoStub).to.have.been.calledWith(sinon.match("App ID: test-app-id")); + }); + + it("should log iOS specific information", () => { + const appMetadata: apps.IosAppMetadata = { + name: "test-name", + projectId: "test-project-id", + appId: "test-app-id", + platform: AppPlatform.IOS, + displayName: "test-display-name", + bundleId: "test-bundle-id", + appStoreId: "test-app-store-id", + }; + logPostAppCreationInformation(appMetadata, AppPlatform.IOS); + expect(loggerInfoStub).to.have.been.calledWith(sinon.match("Bundle ID: test-bundle-id")); + expect(loggerInfoStub).to.have.been.calledWith( + sinon.match("App Store ID: test-app-store-id"), + ); + }); + + it("should log Android specific information", () => { + const appMetadata: apps.AndroidAppMetadata = { + name: "test-name", + projectId: "test-project-id", + appId: "test-app-id", + platform: AppPlatform.ANDROID, + displayName: "test-display-name", + packageName: "test-package-name", + }; + logPostAppCreationInformation(appMetadata, AppPlatform.ANDROID); + expect(loggerInfoStub).to.have.been.calledWith( + sinon.match("Package name: test-package-name"), + ); + }); + }); +}); diff --git a/src/commands/apps-create.ts b/src/commands/apps-create.ts index 452ec2cf093..5cfe4c086ea 100644 --- a/src/commands/apps-create.ts +++ b/src/commands/apps-create.ts @@ -18,7 +18,7 @@ import { logger } from "../logger"; import { Options } from "../options"; import { select } from "../prompt"; -function logPostAppCreationInformation( +export function logPostAppCreationInformation( appMetadata: IosAppMetadata | AndroidAppMetadata | WebAppMetadata, appPlatform: AppPlatform, ): void { diff --git a/src/commands/apps-list.spec.ts b/src/commands/apps-list.spec.ts new file mode 100644 index 00000000000..1b3b99cd343 --- /dev/null +++ b/src/commands/apps-list.spec.ts @@ -0,0 +1,111 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as clc from "colorette"; +import * as Table from "cli-table3"; +import { Command } from "../command"; +import * as projectUtils from "../projectUtils"; +import * as apps from "../management/apps"; +import { AppMetadata, AppPlatform } from "../management/apps"; +import { command, logAppsList, logAppCount } from "./apps-list"; +import * as auth from "../requireAuth"; + +const NOT_SPECIFIED = clc.yellow("[Not specified]"); + +describe("apps:list", () => { + let sandbox: sinon.SinonSandbox; + let listFirebaseAppsStub: sinon.SinonStub; + let getAppPlatformStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(auth, "requireAuth").resolves(); + sandbox.stub(projectUtils, "needProjectId").returns("test-project-id"); + listFirebaseAppsStub = sandbox.stub(apps, "listFirebaseApps"); + getAppPlatformStub = sandbox.stub(apps, "getAppPlatform"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should be a Command", () => { + expect(command).to.be.an.instanceOf(Command); + }); + + describe("action", () => { + it("should list all apps if no platform is provided", async () => { + const appsList: AppMetadata[] = [ + { name: "n1", projectId: "p1", appId: "1", displayName: "app1", platform: AppPlatform.IOS }, + { + name: "n2", + projectId: "p2", + appId: "2", + displayName: "app2", + platform: AppPlatform.ANDROID, + }, + ]; + listFirebaseAppsStub.resolves(appsList); + getAppPlatformStub.returns(AppPlatform.ANY); + + await command.runner()(undefined, {}); + + expect(listFirebaseAppsStub).to.have.been.calledOnceWith("test-project-id", AppPlatform.ANY); + }); + + it("should list apps for a specific platform", async () => { + const appsList: AppMetadata[] = [ + { name: "n1", projectId: "p1", appId: "1", displayName: "app1", platform: AppPlatform.IOS }, + ]; + listFirebaseAppsStub.resolves(appsList); + getAppPlatformStub.returns(AppPlatform.IOS); + + await command.runner()("IOS", {}); + + expect(listFirebaseAppsStub).to.have.been.calledOnceWith("test-project-id", AppPlatform.IOS); + }); + + it('should display "No apps found." if no apps exist', async () => { + listFirebaseAppsStub.resolves([]); + getAppPlatformStub.returns(AppPlatform.ANY); + + await command.runner()(undefined, {}); + + // No assertion needed here, we are just checking that it does not throw. + }); + }); + + describe("logAppsList", () => { + it("should print a table of apps", () => { + const appsList: AppMetadata[] = [ + { name: "n1", projectId: "p1", appId: "1", displayName: "app1", platform: AppPlatform.IOS }, + { + name: "n2", + projectId: "p2", + appId: "2", + displayName: "app2", + platform: AppPlatform.ANDROID, + }, + { name: "n3", projectId: "p3", appId: "3", platform: AppPlatform.WEB }, + ]; + const tableSpy = sandbox.spy(Table.prototype, "push"); + + logAppsList(appsList); + + expect(tableSpy.getCall(0).args[0]).to.deep.equal(["app1", "1", "IOS"]); + expect(tableSpy.getCall(1).args[0]).to.deep.equal(["app2", "2", "ANDROID"]); + expect(tableSpy.getCall(2).args[0]).to.deep.equal([NOT_SPECIFIED, "3", "WEB"]); + }); + }); + + describe("logAppCount", () => { + it("should print the total number of apps", () => { + logAppCount(5); + // No assertion needed here, we are just checking that it does not throw. + }); + + it("should not print if count is 0", () => { + logAppCount(0); + // No assertion needed here, we are just checking that it does not throw. + }); + }); +}); diff --git a/src/commands/apps-list.ts b/src/commands/apps-list.ts index c3f3fa4fd4a..6f0ffab2cf7 100644 --- a/src/commands/apps-list.ts +++ b/src/commands/apps-list.ts @@ -10,7 +10,7 @@ import { logger } from "../logger"; const NOT_SPECIFIED = clc.yellow("[Not specified]"); -function logAppsList(apps: AppMetadata[]): void { +export function logAppsList(apps: AppMetadata[]): void { if (apps.length === 0) { logger.info(clc.bold("No apps found.")); return; @@ -24,7 +24,7 @@ function logAppsList(apps: AppMetadata[]): void { logger.info(table.toString()); } -function logAppCount(count: number = 0): void { +export function logAppCount(count: number = 0): void { if (count === 0) { return; } diff --git a/src/commands/apps-sdkconfig.spec.ts b/src/commands/apps-sdkconfig.spec.ts new file mode 100644 index 00000000000..44a86b93800 --- /dev/null +++ b/src/commands/apps-sdkconfig.spec.ts @@ -0,0 +1,184 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fs from "fs-extra"; +import { Command } from "../command"; +import * as apps from "../management/apps"; +import { AppPlatform } from "../management/apps"; +import * as projectUtils from "../projectUtils"; +import * as projects from "../management/projects"; +import { FirebaseError } from "../error"; +import * as prompt from "../prompt"; +import { command } from "./apps-sdkconfig"; +import * as auth from "../requireAuth"; + +import { logger } from "../logger"; + +describe("apps:sdkconfig", () => { + let sandbox: sinon.SinonSandbox; + let needProjectIdStub: sinon.SinonStub; + let getAppConfigStub: sinon.SinonStub; + let getAppConfigFileStub: sinon.SinonStub; + let listFirebaseAppsStub: sinon.SinonStub; + let getOrPromptProjectStub: sinon.SinonStub; + let selectStub: sinon.SinonStub; + let confirmStub: sinon.SinonStub; + let writeFileSyncStub: sinon.SinonStub; + let existsSyncStub: sinon.SinonStub; + let loggerInfoStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(auth, "requireAuth").resolves(); + needProjectIdStub = sandbox.stub(projectUtils, "needProjectId").returns("test-project-id"); + getAppConfigStub = sandbox.stub(apps, "getAppConfig"); + getAppConfigFileStub = sandbox.stub(apps, "getAppConfigFile"); + listFirebaseAppsStub = sandbox.stub(apps, "listFirebaseApps"); + getOrPromptProjectStub = sandbox.stub(projects, "getOrPromptProject"); + selectStub = sandbox.stub(prompt, "select"); + confirmStub = sandbox.stub(prompt, "confirm"); + writeFileSyncStub = sandbox.stub(fs, "writeFileSync"); + existsSyncStub = sandbox.stub(fs, "existsSync"); + loggerInfoStub = sandbox.stub(logger, "info"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should be a Command", () => { + expect(command).to.be.an.instanceOf(Command); + }); + + describe("action", () => { + it("should get config for a specified app", async () => { + getAppConfigStub.resolves({} as any); + getAppConfigFileStub.returns({ fileContents: "test-config" } as any); + + await command.runner()("IOS", "test-app-id", {}); + + expect(getAppConfigStub).to.have.been.calledOnceWith("test-app-id", AppPlatform.IOS); + expect(loggerInfoStub).to.have.been.calledWith("test-config"); + }); + + it("should get config for the only app when no app id is provided", async () => { + listFirebaseAppsStub.resolves([ + { name: "n1", projectId: "p1", appId: "test-app-id", platform: AppPlatform.ANDROID }, + ]); + getAppConfigStub.resolves({ fileContents: "test-config" }); + getAppConfigFileStub.returns({ fileContents: "test-config" }); + + await command.runner()("ANDROID", "", {}); + + expect(listFirebaseAppsStub).to.have.been.calledOnceWith( + "test-project-id", + AppPlatform.ANDROID, + ); + expect(getAppConfigStub).to.have.been.calledOnceWith("test-app-id", AppPlatform.ANDROID); + }); + + it("should prompt for app if multiple apps exist", async () => { + const app1 = { + name: "n1", + projectId: "p1", + appId: "app1", + platform: AppPlatform.IOS, + displayName: "app1", + }; + const app2 = { + name: "n2", + projectId: "p2", + appId: "app2", + platform: AppPlatform.IOS, + displayName: "app2", + }; + listFirebaseAppsStub.resolves([app1, app2]); + selectStub.resolves(app1); + getAppConfigStub.resolves({ fileContents: "test-config" }); + getAppConfigFileStub.returns({ fileContents: "test-config" } as any); + + await command.runner()("IOS", "", { nonInteractive: false }); + + expect(selectStub).to.have.been.calledOnce; + expect(getAppConfigStub).to.have.been.calledOnceWith("app1", AppPlatform.IOS); + }); + + it("should throw if multiple apps exist in non-interactive mode", async () => { + const app1 = { name: "n1", projectId: "p1", appId: "app1", platform: AppPlatform.IOS }; + const app2 = { name: "n2", projectId: "p2", appId: "app2", platform: AppPlatform.IOS }; + listFirebaseAppsStub.resolves([app1, app2]); + + await expect(command.runner()("IOS", "", { nonInteractive: true })).to.be.rejectedWith( + FirebaseError, + "Project test-project-id has multiple apps, must specify an app id.", + ); + }); + + it("should throw if no apps exist", async () => { + listFirebaseAppsStub.resolves([]); + + await expect(command.runner()("IOS", "", {})).to.be.rejectedWith( + FirebaseError, + "There are no IOS apps associated with this Firebase project", + ); + }); + + it("should write config to a file", async () => { + getAppConfigStub.resolves({}); + getAppConfigFileStub.returns({ fileName: "test.json", fileContents: "test-config" }); + existsSyncStub.returns(false); + + await command.runner()("WEB", "test-app-id", { out: "out.json" }); + + expect(writeFileSyncStub).to.have.been.calledOnceWith("out.json", "test-config"); + expect(loggerInfoStub).to.have.been.calledWith("App configuration is written in out.json"); + }); + + it("should overwrite existing file if confirmed", async () => { + getAppConfigStub.resolves({}); + getAppConfigFileStub.returns({ fileName: "test.json", fileContents: "test-config" }); + existsSyncStub.returns(true); + confirmStub.resolves(true); + + await command.runner()("WEB", "test-app-id", { out: "out.json" }); + + expect(confirmStub).to.have.been.calledOnce; + expect(writeFileSyncStub).to.have.been.calledOnceWith("out.json", "test-config"); + }); + + it("should not overwrite existing file if not confirmed", async () => { + getAppConfigStub.resolves({}); + getAppConfigFileStub.returns({ fileName: "test.json", fileContents: "test-config" }); + existsSyncStub.returns(true); + confirmStub.resolves(false); + + await command.runner()("WEB", "test-app-id", { out: "out.json" }); + + expect(confirmStub).to.have.been.calledOnce; + expect(writeFileSyncStub).to.not.have.been.called; + }); + + it("should throw if file exists in non-interactive mode", async () => { + getAppConfigStub.resolves({}); + getAppConfigFileStub.returns({ fileName: "test.json", fileContents: "test-config" }); + existsSyncStub.returns(true); + + await expect( + command.runner()("WEB", "test-app-id", { out: "out.json", nonInteractive: true }), + ).to.be.rejectedWith(FirebaseError, "out.json already exists"); + }); + + it("should prompt for project if not available", async () => { + needProjectIdStub.returns(undefined); + getOrPromptProjectStub.resolves({ projectId: "test-project-id" }); + listFirebaseAppsStub.resolves([ + { name: "n1", projectId: "p1", appId: "test-app-id", platform: AppPlatform.ANDROID }, + ]); + getAppConfigStub.resolves({ fileContents: "test-config" }); + getAppConfigFileStub.returns({ fileContents: "test-config" }); + + await command.runner()("ANDROID", "", {}); + + expect(getOrPromptProjectStub).to.have.been.calledOnce; + }); + }); +}); diff --git a/src/management/apps.ts b/src/management/apps.ts index 8acd8455c7e..6c66c6fb725 100644 --- a/src/management/apps.ts +++ b/src/management/apps.ts @@ -309,7 +309,7 @@ export interface AppConfigurationData { export interface AppAndroidShaData { name: string; shaHash: string; - certType: ShaCertificateType.SHA_1; + certType: ShaCertificateType; } export enum AppPlatform {