From 48314e541ceb7a11b1a197d5fe608da8bbf24587 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 11 Dec 2019 11:02:32 -0800 Subject: [PATCH 1/2] Using async project ID discovery API in Auth and FCM --- src/auth/auth-api-request.ts | 84 ++++++++++++++++++--------- src/messaging/messaging.ts | 71 +++++++++++++--------- test/unit/auth/auth.spec.ts | 12 +++- test/unit/messaging/messaging.spec.ts | 22 +++---- 4 files changed, 117 insertions(+), 72 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 49836721e0..d40ea53515 100755 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -88,7 +88,9 @@ const MAX_LIST_TENANT_PAGE_SIZE = 1000; /** Defines a base utility to help with resource URL construction. */ class AuthResourceUrlBuilder { + protected urlFormat: string; + private projectId: string; /** * The resource URL builder constructor. @@ -97,7 +99,7 @@ class AuthResourceUrlBuilder { * @param {string} version The endpoint API version. * @constructor */ - constructor(protected projectId: string | null, protected version: string = 'v1') { + constructor(protected app: FirebaseApp, protected version: string = 'v1') { this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; } @@ -107,17 +109,41 @@ class AuthResourceUrlBuilder { * @param {string=} api The backend API name. * @param {object=} params The optional additional parameters to substitute in the * URL path. - * @return {string} The corresponding resource URL. + * @return {Promise} The corresponding resource URL. */ - public getUrl(api?: string, params?: object): string { - const baseParams = { - version: this.version, - projectId: this.projectId, - api: api || '', - }; - const baseUrl = utils.formatString(this.urlFormat, baseParams); - // Substitute additional api related parameters. - return utils.formatString(baseUrl, params || {}); + public getUrl(api?: string, params?: object): Promise { + return this.getProjectId() + .then((projectId) => { + const baseParams = { + version: this.version, + projectId, + api: api || '', + }; + const baseUrl = utils.formatString(this.urlFormat, baseParams); + // Substitute additional api related parameters. + return utils.formatString(baseUrl, params || {}); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CREDENTIAL, + 'Failed to determine project ID for Auth. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.projectId = projectId; + return projectId; + }); } } @@ -132,8 +158,8 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { * @param {string} tenantId The tenant ID. * @constructor */ - constructor(protected projectId: string | null, protected version: string, protected tenantId: string) { - super(projectId, version); + constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) { + super(app, version); this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; } @@ -143,10 +169,13 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { * @param {string=} api The backend API name. * @param {object=} params The optional additional parameters to substitute in the * URL path. - * @return {string} The corresponding resource URL. + * @return {Promise} The corresponding resource URL. */ - public getUrl(api?: string, params?: object) { - return utils.formatString(super.getUrl(api, params), {tenantId: this.tenantId}); + public getUrl(api?: string, params?: object): Promise { + return super.getUrl(api, params) + .then((url) => { + return utils.formatString(url, {tenantId: this.tenantId}); + }); } } @@ -683,7 +712,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') * Class that provides the mechanism to send requests to the Firebase Auth backend endpoints. */ export abstract class AbstractAuthRequestHandler { - protected readonly projectId: string | null; + protected readonly httpClient: AuthorizedHttpClient; private authUrlBuilder: AuthResourceUrlBuilder; private projectConfigUrlBuilder: AuthResourceUrlBuilder; @@ -700,7 +729,7 @@ export abstract class AbstractAuthRequestHandler { * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. * @constructor */ - constructor(app: FirebaseApp) { + constructor(protected readonly app: FirebaseApp) { if (typeof app !== 'object' || app === null || !('options' in app)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -708,9 +737,6 @@ export abstract class AbstractAuthRequestHandler { ); } - // TODO(rsgowman): Trace utils.getProjectId() throughout and figure out where a null return - // value will cause troubles. (Such as AuthResourceUrlBuilder::getUrl()). - this.projectId = utils.getProjectId(app); this.httpClient = new AuthorizedHttpClient(app); } @@ -1357,15 +1383,15 @@ export abstract class AbstractAuthRequestHandler { protected invokeRequestHandler( urlBuilder: AuthResourceUrlBuilder, apiSettings: ApiSettings, requestData: object, additionalResourceParams?: object): Promise { - return Promise.resolve() - .then(() => { + return urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams) + .then((url) => { // Validate request. const requestValidator = apiSettings.getRequestValidator(); requestValidator(requestData); // Process request. const req: HttpRequestConfig = { method: apiSettings.getHttpMethod(), - url: urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams), + url, headers: FIREBASE_AUTH_HEADER, data: requestData, timeout: FIREBASE_AUTH_TIMEOUT, @@ -1512,21 +1538,21 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: FirebaseApp) { super(app); - this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(utils.getProjectId(app), 'v2beta1'); + this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2beta1'); } /** * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. */ protected newAuthUrlBuilder(): AuthResourceUrlBuilder { - return new AuthResourceUrlBuilder(this.projectId, 'v1'); + return new AuthResourceUrlBuilder(this.app, 'v1'); } /** * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. */ protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { - return new AuthResourceUrlBuilder(this.projectId, 'v2beta1'); + return new AuthResourceUrlBuilder(this.app, 'v2beta1'); } /** @@ -1662,14 +1688,14 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. */ protected newAuthUrlBuilder(): AuthResourceUrlBuilder { - return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v1', this.tenantId); + return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId); } /** * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. */ protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { - return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v2beta1', this.tenantId); + return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2beta1', this.tenantId); } /** diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 26e12922e3..7a8c691e4a 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -206,8 +206,8 @@ export class Messaging implements FirebaseServiceInterface { public INTERNAL: MessagingInternals = new MessagingInternals(); private urlPath: string; - private appInternal: FirebaseApp; - private messagingRequestHandler: FirebaseMessagingRequestHandler; + private readonly appInternal: FirebaseApp; + private readonly messagingRequestHandler: FirebaseMessagingRequestHandler; /** * @param {FirebaseApp} app The app for this Messaging service. @@ -221,18 +221,6 @@ export class Messaging implements FirebaseServiceInterface { ); } - const projectId: string | null = utils.getProjectId(app); - if (!validator.isNonEmptyString(projectId)) { - // Assert for an explicit project ID (either via AppOptions or the cert itself). - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, - 'Failed to determine project ID for Messaging. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', - ); - } - - this.urlPath = `/v1/projects/${projectId}/messages:send`; this.appInternal = app; this.messagingRequestHandler = new FirebaseMessagingRequestHandler(app); } @@ -261,13 +249,13 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } - return Promise.resolve() - .then(() => { + return this.getUrlPath() + .then((urlPath) => { const request: {message: Message, validate_only?: boolean} = {message: copy}; if (dryRun) { request.validate_only = true; } - return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, this.urlPath, request); + return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, urlPath, request); }) .then((response) => { return (response as any).name; @@ -312,18 +300,21 @@ export class Messaging implements FirebaseServiceInterface { MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } - const requests: SubRequest[] = copy.map((message) => { - validateMessage(message); - const request: {message: Message, validate_only?: boolean} = {message}; - if (dryRun) { - request.validate_only = true; - } - return { - url: `https://${FCM_SEND_HOST}${this.urlPath}`, - body: request, - }; - }); - return this.messagingRequestHandler.sendBatchRequest(requests); + return this.getUrlPath() + .then((urlPath) => { + const requests: SubRequest[] = copy.map((message) => { + validateMessage(message); + const request: {message: Message, validate_only?: boolean} = {message}; + if (dryRun) { + request.validate_only = true; + } + return { + url: `https://${FCM_SEND_HOST}${urlPath}`, + body: request, + }; + }); + return this.messagingRequestHandler.sendBatchRequest(requests); + }); } /** @@ -645,6 +636,28 @@ export class Messaging implements FirebaseServiceInterface { ); } + private getUrlPath(): Promise { + if (this.urlPath) { + return Promise.resolve(this.urlPath); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + // Assert for an explicit project ID (either via AppOptions or the cert itself). + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + 'Failed to determine project ID for Messaging. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.urlPath = `/v1/projects/${projectId}/messages:send`; + return this.urlPath; + }); + } + /** * Helper method which sends and handles topic subscription management requests. * diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 5fd283a777..03bdcd04bc 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -292,6 +292,15 @@ AUTH_CONFIGS.forEach((testConfig) => { }).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.'); }); + it('should reject given no project ID', () => { + const authWithoutProjectId = new Auth(mocks.mockCredentialApp()); + authWithoutProjectId.getUser('uid') + .should.eventually.be.rejectedWith( + 'Failed to determine project ID for Auth. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'); + }); + it('should not throw given a valid app', () => { expect(() => { return new Auth(mockApp); @@ -1626,8 +1635,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }) .catch((error) => { expect(error).to.have.property('code', 'auth/invalid-page-token'); - expect(validator.isNonEmptyString) - .to.have.been.calledOnce.and.calledWith(invalidToken); }); }); @@ -1968,7 +1975,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }) .catch((error) => { expect(error).to.have.property('code', 'auth/invalid-id-token'); - expect(validator.isNonEmptyString).to.have.been.calledOnce.and.calledWith(invalidIdToken); }); }); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 5d6e6bc725..52219c4312 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -366,13 +366,14 @@ describe('Messaging', () => { }).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.'); }); - it('should throw given app without project ID', () => { - expect(() => { - const appWithoutProhectId = mocks.mockCredentialApp(); - return new Messaging(appWithoutProhectId); - }).to.throw('Failed to determine project ID for Messaging. Initialize the SDK with service ' - + 'account credentials or set project ID as an app option. Alternatively set the ' - + 'GOOGLE_CLOUD_PROJECT environment variable.'); + it('should reject given app without project ID', () => { + const appWithoutProjectId = mocks.mockCredentialApp(); + const messagingWithoutProjectId = new Messaging(appWithoutProjectId); + messagingWithoutProjectId.send({topic: 'test'}) + .should.eventually.be.rejectedWith( + 'Failed to determine project ID for Messaging. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'); }); it('should not throw given a valid app', () => { @@ -597,11 +598,10 @@ describe('Messaging', () => { }).to.throw('messages list must not contain more than 500 items'); }); - it('should throw when a message is invalid', () => { + it('should reject when a message is invalid', () => { const invalidMessage: Message = {} as any; - expect(() => { - messaging.sendAll([validMessage, invalidMessage]); - }).to.throw('Exactly one of topic, token or condition is required'); + messaging.sendAll([validMessage, invalidMessage]) + .should.eventually.be.rejectedWith('Exactly one of topic, token or condition is required'); }); const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; From 936c1f38f575bf1208dc66d92fe2472486f60a89 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 11 Dec 2019 16:05:06 -0800 Subject: [PATCH 2/2] Async project ID discovery mechanism for ProjectManagement and InstanceId APIs --- src/instance-id/instance-id-request.ts | 36 +++- src/instance-id/instance-id.ts | 14 +- src/project-management/project-management.ts | 160 +++++++++++------- .../instance-id/instance-id-request.spec.ts | 12 +- test/unit/instance-id/instance-id.spec.ts | 8 +- .../project-management.spec.ts | 8 +- 6 files changed, 140 insertions(+), 98 deletions(-) diff --git a/src/instance-id/instance-id-request.ts b/src/instance-id/instance-id-request.ts index 37b5bf9c85..e4c95aee68 100644 --- a/src/instance-id/instance-id-request.ts +++ b/src/instance-id/instance-id-request.ts @@ -20,6 +20,7 @@ import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; +import * as utils from '../utils/index'; import * as validator from '../utils/validator'; /** Firebase IID backend host. */ @@ -49,17 +50,15 @@ export class FirebaseInstanceIdRequestHandler { private readonly host: string = FIREBASE_IID_HOST; private readonly timeout: number = FIREBASE_IID_TIMEOUT; private readonly httpClient: AuthorizedHttpClient; - private readonly path: string; + private path: string; /** * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. - * @param {string} projectId A Firebase project ID string. * * @constructor */ - constructor(app: FirebaseApp, projectId: string) { + constructor(private readonly app: FirebaseApp) { this.httpClient = new AuthorizedHttpClient(app); - this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`; } public deleteInstanceId(instanceId: string): Promise { @@ -79,11 +78,10 @@ export class FirebaseInstanceIdRequestHandler { * @return {Promise} A promise that resolves when the request is complete. */ private invokeRequestHandler(apiSettings: ApiSettings): Promise { - const path: string = this.path + apiSettings.getEndpoint(); - return Promise.resolve() - .then(() => { + return this.getPathPrefix() + .then((path) => { const req: HttpRequestConfig = { - url: `https://${this.host}${path}`, + url: `https://${this.host}${path}${apiSettings.getEndpoint()}`, method: apiSettings.getHttpMethod(), timeout: this.timeout, }; @@ -107,4 +105,26 @@ export class FirebaseInstanceIdRequestHandler { throw err; }); } + + private getPathPrefix(): Promise { + if (this.path) { + return Promise.resolve(this.path); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + // Assert for an explicit projct ID (either via AppOptions or the cert itself). + throw new FirebaseInstanceIdError( + InstanceIdClientErrorCode.INVALID_PROJECT_ID, + 'Failed to determine project ID for InstanceId. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`; + return this.path; + }); + } } diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 60bc52565b..a3ebf0a189 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -19,7 +19,6 @@ import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; import {FirebaseInstanceIdRequestHandler} from './instance-id-request'; -import * as utils from '../utils/index'; import * as validator from '../utils/validator'; /** @@ -55,19 +54,8 @@ export class InstanceId implements FirebaseServiceInterface { ); } - const projectId: string | null = utils.getProjectId(app); - if (!validator.isNonEmptyString(projectId)) { - // Assert for an explicit projct ID (either via AppOptions or the cert itself). - throw new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_PROJECT_ID, - 'Failed to determine project ID for InstanceId. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', - ); - } - this.app_ = app; - this.requestHandler = new FirebaseInstanceIdRequestHandler(app, projectId); + this.requestHandler = new FirebaseInstanceIdRequestHandler(app); } /** diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index 2c9c39794c..3ac3d2dd09 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -43,11 +43,11 @@ class ProjectManagementInternals implements FirebaseServiceInternalsInterface { * ProjectManagement service bound to the provided app. */ export class ProjectManagement implements FirebaseServiceInterface { + public readonly INTERNAL: ProjectManagementInternals = new ProjectManagementInternals(); - private readonly resourceName: string; - private readonly projectId: string; private readonly requestHandler: ProjectManagementRequestHandler; + private projectId: string; /** * @param {object} app The app for this ProjectManagement service. @@ -61,18 +61,6 @@ export class ProjectManagement implements FirebaseServiceInterface { + 'instance.'); } - // Assert that a specific project ID was provided within the app. - const projectId = utils.getProjectId(app); - if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseProjectManagementError( - 'invalid-project-id', - 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' - + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' - + 'environment variable.'); - } - this.projectId = projectId; - this.resourceName = `projects/${this.projectId}`; - this.requestHandler = new ProjectManagementRequestHandler(app); } @@ -115,56 +103,73 @@ export class ProjectManagement implements FirebaseServiceInterface { * Creates a new Firebase Android app, associated with this Firebase project. */ public createAndroidApp(packageName: string, displayName?: string): Promise { - return this.requestHandler.createAndroidApp(this.resourceName, packageName, displayName) - .then((responseData: any) => { - assertServerResponse( - validator.isNonNullObject(responseData), - responseData, - 'createAndroidApp()\'s responseData must be a non-null object.'); + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.createAndroidApp(resourceName, packageName, displayName); + }) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'createAndroidApp()\'s responseData must be a non-null object.'); - assertServerResponse( - validator.isNonEmptyString(responseData.appId), - responseData, - `"responseData.appId" field must be present in createAndroidApp()'s response data.`); - return new AndroidApp(responseData.appId, this.requestHandler); - }); + assertServerResponse( + validator.isNonEmptyString(responseData.appId), + responseData, + `"responseData.appId" field must be present in createAndroidApp()'s response data.`); + return new AndroidApp(responseData.appId, this.requestHandler); + }); } /** * Creates a new Firebase iOS app, associated with this Firebase project. */ public createIosApp(bundleId: string, displayName?: string): Promise { - return this.requestHandler.createIosApp(this.resourceName, bundleId, displayName) - .then((responseData: any) => { - assertServerResponse( - validator.isNonNullObject(responseData), - responseData, - 'createIosApp()\'s responseData must be a non-null object.'); + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.createIosApp(resourceName, bundleId, displayName); + }) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'createIosApp()\'s responseData must be a non-null object.'); - assertServerResponse( - validator.isNonEmptyString(responseData.appId), - responseData, - `"responseData.appId" field must be present in createIosApp()'s response data.`); - return new IosApp(responseData.appId, this.requestHandler); - }); + assertServerResponse( + validator.isNonEmptyString(responseData.appId), + responseData, + `"responseData.appId" field must be present in createIosApp()'s response data.`); + return new IosApp(responseData.appId, this.requestHandler); + }); } /** * Lists up to 100 Firebase apps associated with this Firebase project. */ public listAppMetadata(): Promise { - return this.requestHandler.listAppMetadata(this.resourceName) - .then((responseData) => this.transformResponseToAppMetadata(responseData)); + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.listAppMetadata(resourceName); + }) + .then((responseData) => { + return this.getProjectId() + .then((projectId) => { + return this.transformResponseToAppMetadata(responseData, projectId); + }); + }); } /** * Update display name of the project */ public setDisplayName(newDisplayName: string): Promise { - return this.requestHandler.setDisplayName(this.resourceName, newDisplayName); + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.setDisplayName(resourceName, newDisplayName); + }); } - private transformResponseToAppMetadata(responseData: any): AppMetadata[] { + private transformResponseToAppMetadata(responseData: any, projectId: string): AppMetadata[] { this.assertListAppsResponseData(responseData, 'listAppMetadata()'); if (!responseData.apps) { @@ -183,7 +188,7 @@ export class ProjectManagement implements FirebaseServiceInterface { const metadata: AppMetadata = { appId: appJson.appId, platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN, - projectId: this.projectId, + projectId, resourceName: appJson.name, }; if (appJson.displayName) { @@ -193,34 +198,63 @@ export class ProjectManagement implements FirebaseServiceInterface { }); } + private getResourceName(): Promise { + return this.getProjectId() + .then((projectId) => { + return `projects/${projectId}`; + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + // Assert that a specific project ID was provided within the app. + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseProjectManagementError( + 'invalid-project-id', + 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + + 'environment variable.'); + } + + this.projectId = projectId; + return this.projectId; + }); + } + /** * Lists up to 100 Firebase apps for a specified platform, associated with this Firebase project. */ private listPlatformApps(platform: 'android' | 'ios', callerName: string): Promise { - const listPromise: Promise = (platform === 'android') ? - this.requestHandler.listAndroidApps(this.resourceName) - : this.requestHandler.listIosApps(this.resourceName); + return this.getResourceName() + .then((resourceName) => { + return (platform === 'android') ? + this.requestHandler.listAndroidApps(resourceName) + : this.requestHandler.listIosApps(resourceName); + }) + .then((responseData: any) => { + this.assertListAppsResponseData(responseData, callerName); - return listPromise - .then((responseData: any) => { - this.assertListAppsResponseData(responseData, callerName); + if (!responseData.apps) { + return []; + } - if (!responseData.apps) { - return []; + return responseData.apps.map((appJson: any) => { + assertServerResponse( + validator.isNonEmptyString(appJson.appId), + responseData, + `"apps[].appId" field must be present in the ${callerName} response data.`); + if (platform === 'android') { + return new AndroidApp(appJson.appId, this.requestHandler); + } else { + return new IosApp(appJson.appId, this.requestHandler); } - - return responseData.apps.map((appJson: any) => { - assertServerResponse( - validator.isNonEmptyString(appJson.appId), - responseData, - `"apps[].appId" field must be present in the ${callerName} response data.`); - if (platform === 'android') { - return new AndroidApp(appJson.appId, this.requestHandler); - } else { - return new IosApp(appJson.appId, this.requestHandler); - } - }); }); + }); } private assertListAppsResponseData(responseData: any, callerName: string): void { diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/instance-id/instance-id-request.spec.ts index 10c59c4b05..2b83642f11 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/instance-id/instance-id-request.spec.ts @@ -36,7 +36,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('FirebaseInstanceIdRequestHandler', () => { - const projectId: string = 'test-project-id'; + const projectId: string = 'project_id'; const mockAccessToken: string = utils.generateRandomAccessToken(); let stubs: sinon.SinonStub[] = []; let getTokenStub: sinon.SinonStub; @@ -68,7 +68,7 @@ describe('FirebaseInstanceIdRequestHandler', () => { describe('Constructor', () => { it('should succeed with a FirebaseApp instance', () => { expect(() => { - return new FirebaseInstanceIdRequestHandler(mockApp, projectId); + return new FirebaseInstanceIdRequestHandler(mockApp); }).not.to.throw(Error); }); }); @@ -84,7 +84,7 @@ describe('FirebaseInstanceIdRequestHandler', () => { .resolves(utils.responseFrom('')); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); + const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); return requestHandler.deleteInstanceId('test-iid') .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ @@ -101,7 +101,7 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); + const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); return requestHandler.deleteInstanceId('test-iid') .then(() => { throw new Error('Unexpected success'); @@ -117,7 +117,7 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom({}, 409)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); + const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); return requestHandler.deleteInstanceId('test-iid') .then(() => { throw new Error('Unexpected success'); @@ -134,7 +134,7 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom(expectedResult, 511)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); + const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); return requestHandler.deleteInstanceId('test-iid') .then(() => { throw new Error('Unexpected success'); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index 081e9ddb82..c3dc2fdb81 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -93,13 +93,13 @@ describe('InstanceId', () => { }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); }); - it('should throw given an invalid credential without project ID', () => { + it('should reject given an invalid credential without project ID', () => { // Project ID not set in the environment. delete process.env.GOOGLE_CLOUD_PROJECT; delete process.env.GCLOUD_PROJECT; - expect(() => { - return new InstanceId(mockCredentialApp); - }).to.throw(noProjectIdError); + const instanceId = new InstanceId(mockCredentialApp); + return instanceId.deleteInstanceId('iid') + .should.eventually.rejectedWith(noProjectIdError); }); it('should not throw given a valid app', () => { diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index 9a7de91a2d..cbf391f141 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -90,13 +90,13 @@ describe('ProjectManagement', () => { + 'instance.'); }); - it('should throw given an invalid credential without project ID', () => { + it('should reject given an invalid credential without project ID', () => { // Project ID not set in the environment. delete process.env.GOOGLE_CLOUD_PROJECT; delete process.env.GCLOUD_PROJECT; - expect(() => { - return new ProjectManagement(mockCredentialApp); - }).to.throw(noProjectIdErrorMessage); + const projectMgt = new ProjectManagement(mockCredentialApp); + return projectMgt.listIosApps() + .should.eventually.rejectedWith(noProjectIdErrorMessage); }); it('should not throw given a valid app', () => {