diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 0428c4b4f1..439966d345 100755 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -79,10 +79,14 @@ const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60; /** Maximum allowed number of provider configurations to batch download at one time. */ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100; -/** The Firebase Auth backend URL format. */ +/** The Firebase Auth backend base URL format. */ const FIREBASE_AUTH_BASE_URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; +/** The Firebase Auth backend multi-tenancy base URL format. */ +const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( + 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); + /** Defines a base utility to help with resource URL construction. */ class AuthResourceUrlBuilder { @@ -120,6 +124,35 @@ class AuthResourceUrlBuilder { } +/** Tenant aware resource builder utility. */ +class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { + /** + * The tenant aware resource URL builder constructor. + * + * @param {string} projectId The resource project ID. + * @param {string} version The endpoint API version. + * @param {string} tenantId The tenant ID. + * @constructor + */ + constructor(protected projectId: string, protected version: string, protected tenantId: string) { + super(projectId, version); + this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; + } + + /** + * Returns the resource URL corresponding to the provided parameters. + * + * @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. + */ + public getUrl(api?: string, params?: object) { + return utils.formatString(super.getUrl(api, params), {tenantId: this.tenantId}); + } +} + + /** * Validates a providerUserInfo object. All unsupported parameters * are removed from the original request. If an invalid field is passed @@ -205,6 +238,7 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean = phoneNumber: true, customAttributes: true, validSince: true, + tenantId: true, passwordHash: uploadAccountRequest, salt: uploadAccountRequest, createdAt: uploadAccountRequest, @@ -217,6 +251,10 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean = delete request[key]; } } + if (typeof request.tenantId !== 'undefined' && + !validator.isNonEmptyString(request.tenantId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + } // For any invalid parameter, use the external key name in the error description. // displayName should be a string. if (typeof request.displayName !== 'undefined' && @@ -633,10 +671,11 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') /** * Class that provides the mechanism to send requests to the Firebase Auth backend endpoints. */ -export class FirebaseAuthRequestHandler { - private readonly httpClient: AuthorizedHttpClient; - private readonly authUrlBuilder: AuthResourceUrlBuilder; - private readonly projectConfigUrlBuilder: AuthResourceUrlBuilder; +export abstract class AbstractAuthRequestHandler { + protected readonly projectId: string; + protected readonly httpClient: AuthorizedHttpClient; + private authUrlBuilder: AuthResourceUrlBuilder; + private projectConfigUrlBuilder: AuthResourceUrlBuilder; /** * @param {any} response The response to check for errors. @@ -651,10 +690,8 @@ export class FirebaseAuthRequestHandler { * @constructor */ constructor(app: FirebaseApp) { - const projectId = utils.getProjectId(app); + this.projectId = utils.getProjectId(app); this.httpClient = new AuthorizedHttpClient(app); - this.authUrlBuilder = new AuthResourceUrlBuilder(projectId, 'v1'); - this.projectConfigUrlBuilder = new AuthResourceUrlBuilder(projectId, 'v2beta1'); } /** @@ -673,7 +710,7 @@ export class FirebaseAuthRequestHandler { // Convert to seconds. validDuration: expiresIn / 1000, }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_CREATE_SESSION_COOKIE, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_CREATE_SESSION_COOKIE, request) .then((response: any) => response.sessionCookie); } @@ -691,7 +728,7 @@ export class FirebaseAuthRequestHandler { const request = { localId: [uid], }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** @@ -708,7 +745,7 @@ export class FirebaseAuthRequestHandler { const request = { email: [email], }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** @@ -725,7 +762,7 @@ export class FirebaseAuthRequestHandler { const request = { phoneNumber: [phoneNumber], }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** @@ -753,7 +790,7 @@ export class FirebaseAuthRequestHandler { if (typeof request.nextPageToken === 'undefined') { delete request.nextPageToken; } - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_DOWNLOAD_ACCOUNT, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_DOWNLOAD_ACCOUNT, request) .then((response: any) => { // No more users available. if (!response.users) { @@ -799,7 +836,7 @@ export class FirebaseAuthRequestHandler { if (request.users.length === 0) { return Promise.resolve(userImportBuilder.buildResponse([])); } - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_UPLOAD_ACCOUNT, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_UPLOAD_ACCOUNT, request) .then((response: any) => { // No error object is returned if no error encountered. const failedUploads = (response.error || []) as Array<{index: number, message: string}>; @@ -822,7 +859,7 @@ export class FirebaseAuthRequestHandler { const request = { localId: uid, }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_DELETE_ACCOUNT, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_DELETE_ACCOUNT, request); } /** @@ -854,7 +891,7 @@ export class FirebaseAuthRequestHandler { localId: uid, customAttributes: JSON.stringify(customUserClaims), }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_SET_ACCOUNT_INFO, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request) .then((response: any) => { return response.localId as string; }); @@ -880,6 +917,15 @@ export class FirebaseAuthRequestHandler { ); } + if (properties.hasOwnProperty('tenantId')) { + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'Tenant ID cannot be modified on an existing user.', + ), + ); + } + // Build the setAccountInfo request. const request: any = deepCopy(properties); request.localId = uid; @@ -931,7 +977,7 @@ export class FirebaseAuthRequestHandler { request.disableUser = request.disabled; delete request.disabled; } - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_SET_ACCOUNT_INFO, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request) .then((response: any) => { return response.localId as string; }); @@ -960,7 +1006,7 @@ export class FirebaseAuthRequestHandler { // validSince is in UTC seconds. validSince: Math.ceil(new Date().getTime() / 1000), }; - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_SET_ACCOUNT_INFO, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request) .then((response: any) => { return response.localId as string; }); @@ -995,7 +1041,7 @@ export class FirebaseAuthRequestHandler { request.localId = request.uid; delete request.uid; } - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_SIGN_UP_NEW_USER, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SIGN_UP_NEW_USER, request) .then((response: any) => { // Return the user id. return response.localId as string; @@ -1028,7 +1074,7 @@ export class FirebaseAuthRequestHandler { return Promise.reject(e); } } - return this.invokeRequestHandler(this.authUrlBuilder, FIREBASE_AUTH_GET_OOB_CODE, request) + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_OOB_CODE, request) .then((response: any) => { // Return the link. return response.oobLink as string; @@ -1045,7 +1091,7 @@ export class FirebaseAuthRequestHandler { if (!OIDCConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.projectConfigUrlBuilder, GET_OAUTH_IDP_CONFIG, {}, {providerId}); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, {providerId}); } /** @@ -1071,7 +1117,7 @@ export class FirebaseAuthRequestHandler { if (typeof pageToken !== 'undefined') { request.pageToken = pageToken; } - return this.invokeRequestHandler(this.projectConfigUrlBuilder, LIST_OAUTH_IDP_CONFIGS, request) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), LIST_OAUTH_IDP_CONFIGS, request) .then((response: any) => { if (!response.oauthIdpConfigs) { response.oauthIdpConfigs = []; @@ -1091,7 +1137,7 @@ export class FirebaseAuthRequestHandler { if (!OIDCConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.projectConfigUrlBuilder, DELETE_OAUTH_IDP_CONFIG, {}, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, {providerId}) .then((response: any) => { // Return nothing. }); @@ -1113,7 +1159,7 @@ export class FirebaseAuthRequestHandler { return Promise.reject(e); } const providerId = options.providerId; - return this.invokeRequestHandler(this.projectConfigUrlBuilder, CREATE_OAUTH_IDP_CONFIG, request, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, {providerId}) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1145,7 +1191,7 @@ export class FirebaseAuthRequestHandler { return Promise.reject(e); } const updateMask = utils.generateUpdateMask(request); - return this.invokeRequestHandler(this.projectConfigUrlBuilder, UPDATE_OAUTH_IDP_CONFIG, request, + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_OAUTH_IDP_CONFIG, request, {providerId, updateMask: updateMask.join(',')}) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { @@ -1167,7 +1213,7 @@ export class FirebaseAuthRequestHandler { if (!SAMLConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.projectConfigUrlBuilder, GET_INBOUND_SAML_CONFIG, {}, {providerId}); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, {providerId}); } /** @@ -1193,7 +1239,7 @@ export class FirebaseAuthRequestHandler { if (typeof pageToken !== 'undefined') { request.pageToken = pageToken; } - return this.invokeRequestHandler(this.projectConfigUrlBuilder, LIST_INBOUND_SAML_CONFIGS, request) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), LIST_INBOUND_SAML_CONFIGS, request) .then((response: any) => { if (!response.inboundSamlConfigs) { response.inboundSamlConfigs = []; @@ -1213,7 +1259,7 @@ export class FirebaseAuthRequestHandler { if (!SAMLConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.projectConfigUrlBuilder, DELETE_INBOUND_SAML_CONFIG, {}, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, {providerId}) .then((response: any) => { // Return nothing. }); @@ -1235,7 +1281,8 @@ export class FirebaseAuthRequestHandler { return Promise.reject(e); } const providerId = options.providerId; - return this.invokeRequestHandler(this.projectConfigUrlBuilder, CREATE_INBOUND_SAML_CONFIG, request, {providerId}) + return this.invokeRequestHandler( + this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, {providerId}) .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1267,7 +1314,7 @@ export class FirebaseAuthRequestHandler { return Promise.reject(e); } const updateMask = utils.generateUpdateMask(request); - return this.invokeRequestHandler(this.projectConfigUrlBuilder, UPDATE_INBOUND_SAML_CONFIG, request, + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_INBOUND_SAML_CONFIG, request, {providerId, updateMask: updateMask.join(',')}) .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { @@ -1316,10 +1363,111 @@ export class FirebaseAuthRequestHandler { .catch((err) => { if (err instanceof HttpError) { const error = err.response.data; - const errorCode = FirebaseAuthRequestHandler.getErrorCode(error); + const errorCode = AbstractAuthRequestHandler.getErrorCode(error); throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, error); } throw err; }); } + + /** + * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. + */ + protected abstract newAuthUrlBuilder(): AuthResourceUrlBuilder; + + /** + * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. + */ + protected abstract newProjectConfigUrlBuilder(): AuthResourceUrlBuilder; + + /** + * @return {AuthResourceUrlBuilder} The current Auth user management resource URL builder. + */ + private getAuthUrlBuilder(): AuthResourceUrlBuilder { + if (!this.authUrlBuilder) { + this.authUrlBuilder = this.newAuthUrlBuilder(); + } + return this.authUrlBuilder; + } + + /** + * @return {AuthResourceUrlBuilder} The current project config resource URL builder. + */ + private getProjectConfigUrlBuilder(): AuthResourceUrlBuilder { + if (!this.projectConfigUrlBuilder) { + this.projectConfigUrlBuilder = this.newProjectConfigUrlBuilder(); + } + return this.projectConfigUrlBuilder; + } +} + + +/** + * Utility for sending requests to Auth server that are Auth instance related. This includes user and + * tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines + * additional tenant management related APIs. + */ +export class AuthRequestHandler extends AbstractAuthRequestHandler { + + protected readonly tenantMgmtResourceBuilder: AuthResourceUrlBuilder; + + /** + * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. + * + * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. + * @constructor. + */ + constructor(private readonly app: FirebaseApp) { + super(app); + this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(utils.getProjectId(app), 'v2beta1'); + } + + /** + * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. + */ + protected newAuthUrlBuilder(): AuthResourceUrlBuilder { + return new AuthResourceUrlBuilder(this.projectId, 'v1'); + } + + /** + * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. + */ + protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { + return new AuthResourceUrlBuilder(this.projectId, 'v2beta1'); + } + + // TODO: add tenant management APIs. +} + +/** + * Utility for sending requests to Auth server that are tenant Auth instance related. This includes user + * management related APIs for specified tenants. + * This extends the BaseFirebaseAuthRequestHandler class. + */ +export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { + /** + * The FirebaseTenantRequestHandler constructor used to initialize an instance using a + * FirebaseApp and a tenant ID. + * + * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. + * @param {string} tenantId The request handler's tenant ID. + * @constructor + */ + constructor(app: FirebaseApp, private readonly tenantId: string) { + super(app); + } + + /** + * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. + */ + protected newAuthUrlBuilder(): AuthResourceUrlBuilder { + return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v1', this.tenantId); + } + + /** + * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. + */ + protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { + return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v2beta1', this.tenantId); + } } diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 8539bd7fe0..f598747335 100755 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -17,7 +17,7 @@ import {UserRecord, CreateRequest, UpdateRequest} from './user-record'; import {FirebaseApp} from '../firebase-app'; import {FirebaseTokenGenerator, CryptoSigner, cryptoSignerFromApp} from './token-generator'; -import {FirebaseAuthRequestHandler} from './auth-api-request'; +import {AuthRequestHandler} from './auth-api-request'; import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error'; import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; import { @@ -101,7 +101,7 @@ class BaseAuth { * @constructor */ constructor(protected readonly projectId: string, - protected readonly authRequestHandler: FirebaseAuthRequestHandler, + protected readonly authRequestHandler: AuthRequestHandler, cryptoSigner: CryptoSigner) { this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); this.sessionCookieVerifier = createSessionCookieVerifier(projectId); @@ -629,7 +629,7 @@ export class Auth extends BaseAuth implements FirebaseServiceInterface { constructor(app: FirebaseApp) { super( Auth.getProjectId(app), - new FirebaseAuthRequestHandler(app), + new AuthRequestHandler(app), cryptoSignerFromApp(app)); this.app_ = app; } diff --git a/src/utils/error.ts b/src/utils/error.ts index 95e18c7dba..7e0ae272ed 100755 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -821,6 +821,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND', // Tenant not found. TENANT_NOT_FOUND: 'TENANT_NOT_FOUND', + // Tenant ID mismatch. + TENANT_ID_MISMATCH: 'MISMATCHING_TENANT_ID', // Token expired error. TOKEN_EXPIRED: 'ID_TOKEN_EXPIRED', // Continue URL provided in ActionCodeSettings has a domain that is not whitelisted. diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 9d3e63960f..0d3690d223 100755 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -30,11 +30,11 @@ import {FirebaseApp} from '../../../src/firebase-app'; import {HttpClient, HttpRequestConfig} from '../../../src/utils/api-request'; import * as validator from '../../../src/utils/validator'; import { - FirebaseAuthRequestHandler, FIREBASE_AUTH_GET_ACCOUNT_INFO, + AuthRequestHandler, FIREBASE_AUTH_GET_ACCOUNT_INFO, FIREBASE_AUTH_DELETE_ACCOUNT, FIREBASE_AUTH_SET_ACCOUNT_INFO, FIREBASE_AUTH_SIGN_UP_NEW_USER, FIREBASE_AUTH_DOWNLOAD_ACCOUNT, RESERVED_CLAIMS, FIREBASE_AUTH_UPLOAD_ACCOUNT, FIREBASE_AUTH_CREATE_SESSION_COOKIE, - EMAIL_ACTION_REQUEST_TYPES, + EMAIL_ACTION_REQUEST_TYPES, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; import {UserImportBuilder, UserImportRecord} from '../../../src/auth/user-import-builder'; import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; @@ -53,6 +53,14 @@ const host = 'identitytoolkit.googleapis.com'; const timeout = 25000; +interface HandlerTest { + name: string; + supportsTenantManagement: boolean; + init(app: FirebaseApp): AbstractAuthRequestHandler; + path(version: string, api: string, projectId: string): string; +} + + /** * @param {number} numOfChars The number of random characters within the string. * @return {string} A string with a specific number of random characters. @@ -723,1216 +731,842 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { }); }); -describe('FirebaseAuthRequestHandler', () => { - let mockApp: FirebaseApp; - let stubs: sinon.SinonStub[] = []; - let getTokenStub: sinon.SinonStub; - const mockAccessToken: string = utils.generateRandomAccessToken(); - const expectedHeaders: {[key: string]: string} = { - 'X-Client-Version': 'Node/Admin/', - 'Authorization': 'Bearer ' + mockAccessToken, - }; - const callParams = (path: string, method: any, data: any): HttpRequestConfig => { - return { - method, - url: `https://${host}${path}`, - headers: expectedHeaders, - data, - timeout, - }; - }; - - before(() => { - getTokenStub = utils.stubGetAccessToken(mockAccessToken); - }); - after(() => { - stubs = []; - getTokenStub.restore(); - }); +const AUTH_REQUEST_HANDLER_TESTS: HandlerTest[] = [ + { + name: 'FirebaseAuthRequestHandler', + init: (app: FirebaseApp) => { + return new AuthRequestHandler(app); + }, + path: (version: string, api: string, projectId: string) => { + return `/${version}/projects/${projectId}${api}`; + }, + supportsTenantManagement: true, + }, + { + name: 'FirebaseTenantRequestHandler', + init: (app: FirebaseApp) => { + return new TenantAwareAuthRequestHandler(app, TENANT_ID); + }, + path: (version: string, api: string, projectId: string) => { + return `/${version}/projects/${projectId}/tenants/${TENANT_ID}${api}`; + }, + supportsTenantManagement: false, + }, +]; + +const TENANT_ID = 'tenantId'; +AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { + describe(handler.name, () => { + let mockApp: FirebaseApp; + let stubs: sinon.SinonStub[] = []; + let getTokenStub: sinon.SinonStub; + const mockAccessToken: string = utils.generateRandomAccessToken(); + const expectedHeaders: {[key: string]: string} = { + 'X-Client-Version': 'Node/Admin/', + 'Authorization': 'Bearer ' + mockAccessToken, + }; + const callParams = (path: string, method: any, data: any): HttpRequestConfig => { + return { + method, + url: `https://${host}${path}`, + headers: expectedHeaders, + data, + timeout, + }; + }; - beforeEach(() => { - mockApp = mocks.app(); - return mockApp.INTERNAL.getToken(); - }); + before(() => { + getTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); - afterEach(() => { - _.forEach(stubs, (stub) => stub.restore()); - return mockApp.delete(); - }); + after(() => { + stubs = []; + getTokenStub.restore(); + }); - describe('Constructor', () => { - it('should succeed with a FirebaseApp instance', () => { - expect(() => { - return new FirebaseAuthRequestHandler(mockApp); - }).not.to.throw(Error); + beforeEach(() => { + mockApp = mocks.app(); + return mockApp.INTERNAL.getToken(); }); - }); - describe('createSessionCookie', () => { - const durationInMs = 24 * 60 * 60 * 1000; - const path = '/v1/projects/project_id:createSessionCookie'; - const method = 'POST'; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + return mockApp.delete(); + }); - it('should be fulfilled given a valid localId', () => { - const expectedResult = utils.responseFrom({ - sessionCookie: 'SESSION_COOKIE', + describe('Constructor', () => { + it('should succeed with a FirebaseApp instance', () => { + expect(() => { + return handler.init(mockApp); + }).not.to.throw(Error); }); - const data = {idToken: 'ID_TOKEN', validDuration: durationInMs / 1000}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('ID_TOKEN', durationInMs) - .then((result) => { - expect(result).to.deep.equal('SESSION_COOKIE'); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be fulfilled given a duration equal to the maximum allowed', () => { - const expectedResult = utils.responseFrom({ - sessionCookie: 'SESSION_COOKIE', - }); - const durationAtLimitInMs = 14 * 24 * 60 * 60 * 1000; - const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('ID_TOKEN', durationAtLimitInMs) - .then((result) => { - expect(result).to.deep.equal('SESSION_COOKIE'); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be fulfilled given a duration equal to the minimum allowed', () => { - const expectedResult = utils.responseFrom({ - sessionCookie: 'SESSION_COOKIE', - }); - const durationAtLimitInMs = 5 * 60 * 1000; - const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('ID_TOKEN', durationAtLimitInMs) - .then((result) => { - expect(result).to.deep.equal('SESSION_COOKIE'); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be rejected given an invalid ID token', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ID_TOKEN, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('', durationInMs) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); - it('should be rejected given an invalid duration', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('ID_TOKEN', 'invalid' as any) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); - it('should be rejected given a duration less than minimum allowed', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, - ); - const outOfBoundDuration = 60 * 1000 * 5 - 1; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('ID_TOKEN', outOfBoundDuration) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); }); - it('should be rejected given a duration greater than maximum allowed', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, - ); - const outOfBoundDuration = 60 * 60 * 1000 * 24 * 14 + 1; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('ID_TOKEN', outOfBoundDuration) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); + + describe('createSessionCookie', () => { + const durationInMs = 24 * 60 * 60 * 1000; + const path = handler.path('v1', ':createSessionCookie', 'project_id'); + const method = 'POST'; + + it('should be fulfilled given a valid localId', () => { + const expectedResult = utils.responseFrom({ + sessionCookie: 'SESSION_COOKIE', }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = utils.errorFrom({ - error: { - message: 'INVALID_ID_TOKEN', - }, + const data = {idToken: 'ID_TOKEN', validDuration: durationInMs / 1000}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', durationInMs) + .then((result) => { + expect(result).to.deep.equal('SESSION_COOKIE'); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); - const data = {idToken: 'invalid-token', validDuration: durationInMs / 1000}; - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createSessionCookie('invalid-token', durationInMs) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + it('should be fulfilled given a duration equal to the maximum allowed', () => { + const expectedResult = utils.responseFrom({ + sessionCookie: 'SESSION_COOKIE', }); - }); - }); + const durationAtLimitInMs = 14 * 24 * 60 * 60 * 1000; + const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('getAccountInfoByEmail', () => { - const path = '/v1/projects/project_id/accounts:lookup'; - const method = 'POST'; - it('should be fulfilled given a valid email', () => { - const expectedResult = utils.responseFrom({ - users : [ - {email: 'user@example.com'}, - ], - }); - const data = {email: ['user@example.com']}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith({ - method, - url: `https://${host}${path}`, - data, - headers: expectedHeaders, - timeout, + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', durationAtLimitInMs) + .then((result) => { + expect(result).to.deep.equal('SESSION_COOKIE'); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); + }); + it('should be fulfilled given a duration equal to the minimum allowed', () => { + const expectedResult = utils.responseFrom({ + sessionCookie: 'SESSION_COOKIE', }); - }); - it('should be rejected given an invalid email', () => { - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#GetAccountInfoResponse', - }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {email: ['user@example.com']}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - }); + const durationAtLimitInMs = 5 * 60 * 1000; + const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('getAccountInfoByUid', () => { - const path = '/v1/projects/project_id/accounts:lookup'; - const method = 'POST'; - it('should be fulfilled given a valid localId', () => { - const expectedResult = utils.responseFrom({ - users : [ - {localId: 'uid'}, - ], + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', durationAtLimitInMs) + .then((result) => { + expect(result).to.deep.equal('SESSION_COOKIE'); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); }); - const data = {localId: ['uid']}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); + it('should be rejected given an invalid ID token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ID_TOKEN, + ); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByUid('uid') - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be rejected given an invalid localId', () => { - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#GetAccountInfoResponse', - }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {localId: ['uid']}; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByUid('uid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = utils.errorFrom({ - error: { - message: 'OPERATION_NOT_ALLOWED', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('', durationInMs) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: ['uid']}; + it('should be rejected given an invalid duration', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + ); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', 'invalid' as any) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + it('should be rejected given a duration less than minimum allowed', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + ); + const outOfBoundDuration = 60 * 1000 * 5 - 1; - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByUid('uid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', outOfBoundDuration) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + it('should be rejected given a duration greater than maximum allowed', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + ); + const outOfBoundDuration = 60 * 60 * 1000 * 24 * 14 + 1; - describe('getAccountInfoByPhoneNumber', () => { - const path = '/v1/projects/project_id/accounts:lookup'; - const method = 'POST'; - it('should be fulfilled given a valid phoneNumber', () => { - const expectedResult = utils.responseFrom({ - users : [ - { - localId: 'uid', - phoneNumber: '+11234567890', - providerUserInfo: [ - { - providerId: 'phone', - rawId: '+11234567890', - phoneNumber: '+11234567890', - }, - ], - }, - ], + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', outOfBoundDuration) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - const data = { - phoneNumber: ['+11234567890'], - }; + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.errorFrom({ + error: { + message: 'INVALID_ID_TOKEN', + }, + }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); + const data = {idToken: 'invalid-token', validDuration: durationInMs / 1000}; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('invalid-token', durationInMs) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByPhoneNumber('+11234567890') - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + describe('getAccountInfoByEmail', () => { + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid email', () => { + const expectedResult = utils.responseFrom({ + users : [ + {email: 'user@example.com'}, + ], }); - }); - it('should be rejected given an invalid phoneNumber', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER); - - const stub = sinon.stub(HttpClient.prototype, 'send'); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByPhoneNumber('invalid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.not.been.called; + const data = {email: ['user@example.com']}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `https://${host}${path}`, + data, + headers: expectedHeaders, + timeout, + }); + }); + }); + it('should be rejected given an invalid email', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#GetAccountInfoResponse', }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const data = {email: ['user@example.com']}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#GetAccountInfoResponse', + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = { - phoneNumber: ['+11234567890'], - }; + }); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); + describe('getAccountInfoByUid', () => { + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid localId', () => { + const expectedResult = utils.responseFrom({ + users : [ + {localId: 'uid'}, + ], + }); + const data = {localId: ['uid']}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByPhoneNumber('+11234567890') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected given an invalid localId', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#GetAccountInfoResponse', }); - }); - }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const data = {localId: ['uid']}; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('uploadAccount', () => { - const path = '/v1/projects/project_id/accounts:batchCreate'; - const method = 'POST'; - const nowString = new Date().toUTCString(); - const users = [ - { - uid: '1234', - email: 'user@example.com', - passwordHash: Buffer.from('password'), - passwordSalt: Buffer.from('salt'), - displayName: 'Test User', - photoURL: 'https://www.example.com/1234/photo.png', - disabled: true, - metadata: { - lastSignInTime: nowString, - creationTime: nowString, - }, - providerData: [ - { - uid: 'google1234', - email: 'user@example.com', - photoURL: 'https://www.google.com/1234/photo.png', - displayName: 'Google User', - providerId: 'google.com', + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', }, - ], - customClaims: {admin: true}, - }, - { - uid: '9012', - email: 'johndoe@example.com', - passwordHash: Buffer.from('userpass'), - passwordSalt: Buffer.from('NaCl'), - }, - {uid: '5678', phoneNumber: '+16505550101'}, - ]; - const options = { - hash: { - algorithm: 'BCRYPT' as any, - }, - }; + }); + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const data = {localId: ['uid']}; - it('should throw on invalid options without making an underlying API call', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `Unsupported hash algorithm provider "invalid".`, - ); - const invalidOptions = { - hash: { - algorithm: 'invalid', - }, - } as any; - const stub = sinon.stub(HttpClient.prototype, 'send'); - stubs.push(stub); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - expect(() => { - requestHandler.uploadAccount(users, invalidOptions); - }).to.throw(expectedError.message); - expect(stub).to.have.not.been.called; + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - it('should throw when 1001 UserImportRecords are provided', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, - `A maximum of 1000 users can be imported at once.`, - ); - const stub = sinon.stub(HttpClient.prototype, 'send'); - stubs.push(stub); - - const testUsers: UserImportRecord[] = []; - for (let i = 0; i < 1001; i++) { - testUsers.push({ - uid: 'USER' + i.toString(), - email: 'user' + i.toString() + '@example.com', - passwordHash: Buffer.from('password'), - }); - } - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - expect(() => { - requestHandler.uploadAccount(testUsers, options); - }).to.throw(expectedError.message); - expect(stub).to.have.not.been.called; - }); + describe('getAccountInfoByPhoneNumber', () => { + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid phoneNumber', () => { + const expectedResult = utils.responseFrom({ + users : [ + { + localId: 'uid', + phoneNumber: '+11234567890', + providerUserInfo: [ + { + providerId: 'phone', + rawId: '+11234567890', + phoneNumber: '+11234567890', + }, + ], + }, + ], + }); + const data = { + phoneNumber: ['+11234567890'], + }; - it('should resolve successfully when 1000 UserImportRecords are provided', () => { - const expectedResult = utils.responseFrom({}); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const testUsers = []; - for (let i = 0; i < 1000; i++) { - testUsers.push({ - uid: 'USER' + i.toString(), - email: 'user' + i.toString() + '@example.com', - passwordHash: Buffer.from('password'), - }); - } - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - const userImportBuilder = new UserImportBuilder(testUsers, options); - return requestHandler.uploadAccount(testUsers, options) - .then((result) => { - expect(result).to.deep.equal(userImportBuilder.buildResponse([])); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, userImportBuilder.buildRequest())); - }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByPhoneNumber('+11234567890') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected given an invalid phoneNumber', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PHONE_NUMBER); - it('should resolve with expected result on underlying API success', () => { - const expectedResult = utils.responseFrom({}); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - const userImportBuilder = new UserImportBuilder(users, options); - return requestHandler.uploadAccount(users, options) - .then((result) => { - expect(result).to.deep.equal(userImportBuilder.buildResponse([])); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, userImportBuilder.buildRequest())); - }); - }); + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByPhoneNumber('invalid') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.not.been.called; + }); - it('should resolve with expected result on underlying API partial succcess', () => { - const expectedResult = utils.responseFrom({ - error: [ - {index: 0, message: 'Some error occurred'}, - {index: 1, message: 'Another error occurred'}, - ], }); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - const userImportBuilder = new UserImportBuilder(users, options); - return requestHandler.uploadAccount(users, options) - .then((result) => { - expect(result).to.deep.equal(userImportBuilder.buildResponse(expectedResult.data.error)); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, userImportBuilder.buildRequest())); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#GetAccountInfoResponse', }); - }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const data = { + phoneNumber: ['+11234567890'], + }; - it('should resolve without underlying API call when users are processed client side', () => { - // These users should fail to upload due to invalid phone number and email fields. - const testUsers = [ - {uid: '1234', phoneNumber: 'invalid'}, - {uid: '5678', email: 'invalid'}, - ] as any; - const expectedResult = { - successCount: 0, - failureCount: 2, - errors: [ - {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, - ], - }; - const stub = sinon.stub(HttpClient.prototype, 'send'); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.uploadAccount(testUsers) - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.not.been.called; - }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByPhoneNumber('+11234567890') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - it('should validate underlying users and resolve with expected errors', () => { - const testUsers = [ - {uid: 'user1', displayName: false}, - {uid: 123}, - {uid: 'user2', email: 'invalid'}, - {uid: 'user3', phoneNumber: 'invalid'}, - {uid: 'user4', emailVerified: 'invalid'}, - {uid: 'user5', photoURL: 'invalid'}, - {uid: 'user6', disabled: 'invalid'}, - {uid: 'user7', metadata: {creationTime: 'invalid'}}, - {uid: 'user8', metadata: {lastSignInTime: 'invalid'}}, - {uid: 'user9', customClaims: {admin: true, aud: 'bla'}}, - {uid: 'user10', email: 'user10@example.com', passwordHash: 'invalid'}, - {uid: 'user11', email: 'user11@example.com', passwordSalt: 'invalid'}, - {uid: 'user12', providerData: [{providerId: 'google.com'}]}, - { - uid: 'user13', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', displayName: false}], - }, + describe('uploadAccount', () => { + const path = handler.path('v1', '/accounts:batchCreate', 'project_id'); + const method = 'POST'; + const nowString = new Date().toUTCString(); + const users = [ { - uid: 'user14', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', email: 'invalid'}], + uid: '1234', + email: 'user@example.com', + passwordHash: Buffer.from('password'), + passwordSalt: Buffer.from('salt'), + displayName: 'Test User', + photoURL: 'https://www.example.com/1234/photo.png', + disabled: true, + metadata: { + lastSignInTime: nowString, + creationTime: nowString, + }, + providerData: [ + { + uid: 'google1234', + email: 'user@example.com', + photoURL: 'https://www.google.com/1234/photo.png', + displayName: 'Google User', + providerId: 'google.com', + }, + ], + customClaims: {admin: true}, + // Tenant ID accepted on user batch upload. + tenantId: 'TENANT_ID', }, { - uid: 'user15', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid'}], + uid: '9012', + email: 'johndoe@example.com', + passwordHash: Buffer.from('userpass'), + passwordSalt: Buffer.from('NaCl'), }, - {uid: 'user16', providerData: [{}]}, - {email: 'user17@example.com'}, - ] as any; - const validOptions = { + {uid: '5678', phoneNumber: '+16505550101'}, + ]; + const options = { hash: { - algorithm: 'BCRYPT', + algorithm: 'BCRYPT' as any, }, - } as any; - const expectedResult = { - successCount: 0, - failureCount: testUsers.length, - errors: [ - {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME)}, - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, - {index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, - {index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, - {index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED)}, - {index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL)}, - {index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD)}, - {index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME)}, - {index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME)}, - { - index: 9, - error: new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, - ), - }, - {index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH)}, - {index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT)}, - { - index: 12, - error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_UID, - `The provider "uid" for "google.com" must be a valid non-empty string.`, - ), - }, - { - index: 13, - error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The provider "displayName" for "google.com" must be a valid string.`, - ), - }, - { - index: 14, - error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_EMAIL, - `The provider "email" for "google.com" must be a valid email string.`, - ), - }, - { - index: 15, - error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHOTO_URL, - `The provider "photoURL" for "google.com" must be a valid URL string.`, - ), - }, - {index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)}, - {index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, - ], }; - const stub = sinon.stub(HttpClient.prototype, 'send'); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.uploadAccount(testUsers, validOptions) - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.not.been.called; - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = utils.errorFrom({ - error: { - message: 'INTERNAL_ERROR', - }, + it('should throw on invalid options without making an underlying API call', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ALGORITHM, + `Unsupported hash algorithm provider "invalid".`, + ); + const invalidOptions = { + hash: { + algorithm: 'invalid', + }, + } as any; + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + expect(() => { + requestHandler.uploadAccount(users, invalidOptions); + }).to.throw(expectedError.message); + expect(stub).to.have.not.been.called; }); - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + - `"${JSON.stringify(expectedServerError.response.data)}"`, - ); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - const userImportBuilder = new UserImportBuilder(users, options); - return requestHandler.uploadAccount(users, options) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, userImportBuilder.buildRequest())); - }); - }); - }); + it('should throw when 1001 UserImportRecords are provided', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + `A maximum of 1000 users can be imported at once.`, + ); + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); - describe('downloadAccount', () => { - const path = '/v1/projects/project_id/accounts:batchGet'; - const method = 'GET'; - const nextPageToken = 'PAGE_TOKEN'; - const maxResults = 500; - const expectedResult = utils.responseFrom({ - users : [ - {localId: 'uid1'}, - {localId: 'uid2'}, - ], - nextPageToken: 'NEXT_PAGE_TOKEN', - }); - it('should be fulfilled given a valid parameters', () => { - const data = { - maxResults, - nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be fulfilled with empty user array when no users exist', () => { - const data = { - maxResults, - nextPageToken, - }; + const testUsers: UserImportRecord[] = []; + for (let i = 0; i < 1001; i++) { + testUsers.push({ + uid: 'USER' + i.toString(), + email: 'user' + i.toString() + '@example.com', + passwordHash: Buffer.from('password'), + }); + } - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + expect(() => { + requestHandler.uploadAccount(testUsers, options); + }).to.throw(expectedError.message); + expect(stub).to.have.not.been.called; + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal({users: []}); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be fulfilled given no parameters', () => { - // Default maxResults should be used. - const data = { - maxResults: 1000, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount() - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be rejected given an invalid maxResults', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 1000.`, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(1001, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); - it('should be rejected given an invalid next page token', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, '') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_PAGE_SELECTION', - }, - }); - const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); - const data = { - maxResults, - nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - }); + it('should resolve successfully when 1000 UserImportRecords are provided', () => { + const expectedResult = utils.responseFrom({}); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('deleteAccount', () => { - const path = '/v1/projects/project_id/accounts:delete'; - const method = 'POST'; - it('should be fulfilled given a valid localId', () => { - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#DeleteAccountResponse', - }); - const data = {localId: 'uid'}; + const testUsers = []; + for (let i = 0; i < 1000; i++) { + testUsers.push({ + uid: 'USER' + i.toString(), + email: 'user' + i.toString() + '@example.com', + passwordHash: Buffer.from('password'), + }); + } + + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(testUsers, options); + return requestHandler.uploadAccount(testUsers, options) + .then((result) => { + expect(result).to.deep.equal(userImportBuilder.buildResponse([])); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteAccount('uid') - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = utils.errorFrom({ - error: { - message: 'OPERATION_NOT_ALLOWED', - }, }); - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: 'uid'}; - - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteAccount('uid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); - }); - }); - }); - describe('updateExistingAccount', () => { - const path = '/v1/projects/project_id/accounts:update'; - const method = 'POST'; - const uid = '12345678'; - const validData = { - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disabled: false, - photoURL: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - ignoredProperty: 'value', - }; - const expectedValidData = { - localId: uid, - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disableUser: false, - photoUrl: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - }; - // Valid request to delete photoURL and displayName. - const validDeleteData = deepCopy(validData); - validDeleteData.displayName = null; - validDeleteData.photoURL = null; - const expectedValidDeleteData = { - localId: uid, - email: 'user@example.com', - emailVerified: true, - disableUser: false, - password: 'password', - phoneNumber: '+11234567890', - deleteAttribute: ['DISPLAY_NAME', 'PHOTO_URL'], - }; - // Valid request to delete phoneNumber. - const validDeletePhoneNumberData = deepCopy(validData); - validDeletePhoneNumberData.phoneNumber = null; - const expectedValidDeletePhoneNumberData = { - localId: uid, - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disableUser: false, - photoUrl: 'http://localhost/1234/photo.png', - password: 'password', - deleteProvider: ['phone'], - }; - const invalidData = { - uid, - email: 'user@invalid@', - }; - const invalidPhoneNumberData = { - uid, - phoneNumber: 'invalid', - }; + it('should resolve with expected result on underlying API success', () => { + const expectedResult = utils.responseFrom({}); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be fulfilled given a valid localId', () => { - // Successful result server response. - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(users, options); + return requestHandler.uploadAccount(users, options) + .then((result) => { + expect(result).to.deep.equal(userImportBuilder.buildResponse([])); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); }); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send empty update request. - return requestHandler.updateExistingAccount(uid, {}) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, {localId: uid})); - }); - }); - it('should be fulfilled given valid parameters', () => { - // Successful result server response. - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, - }); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request with all possible valid parameters. - return requestHandler.updateExistingAccount(uid, validData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidData)); + it('should resolve with expected result on underlying API partial succcess', () => { + const expectedResult = utils.responseFrom({ + error: [ + {index: 0, message: 'Some error occurred'}, + {index: 1, message: 'Another error occurred'}, + ], }); - }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be fulfilled given valid profile parameters to delete', () => { - // Successful result server response. - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(users, options); + return requestHandler.uploadAccount(users, options) + .then((result) => { + expect(result).to.deep.equal(userImportBuilder.buildResponse(expectedResult.data.error)); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); }); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request to delete display name and photo URL. - return requestHandler.updateExistingAccount(uid, validDeleteData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. In this case, displayName - // and photoURL removed from request and deleteAttribute added. - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidDeleteData)); - }); - }); - it('should be fulfilled given phone number to delete', () => { - // Successful result server response. - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, - }); + it('should resolve without underlying API call when users are processed client side', () => { + // These users should fail to upload due to invalid phone number and email fields. + const testUsers = [ + {uid: '1234', phoneNumber: 'invalid'}, + {uid: '5678', email: 'invalid'}, + ] as any; + const expectedResult = { + successCount: 0, + failureCount: 2, + errors: [ + {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, + {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, + ], + }; + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.uploadAccount(testUsers) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.not.been.called; + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request to delete phone number. - return requestHandler.updateExistingAccount(uid, validDeletePhoneNumberData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. In this case, phoneNumber - // removed from request and deleteProvider added. - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidDeletePhoneNumberData)); - }); - }); + it('should validate underlying users and resolve with expected errors', () => { + const testUsers = [ + {uid: 'user1', displayName: false}, + {uid: 123}, + {uid: 'user2', email: 'invalid'}, + {uid: 'user3', phoneNumber: 'invalid'}, + {uid: 'user4', emailVerified: 'invalid'}, + {uid: 'user5', photoURL: 'invalid'}, + {uid: 'user6', disabled: 'invalid'}, + {uid: 'user7', metadata: {creationTime: 'invalid'}}, + {uid: 'user8', metadata: {lastSignInTime: 'invalid'}}, + {uid: 'user9', customClaims: {admin: true, aud: 'bla'}}, + {uid: 'user10', email: 'user10@example.com', passwordHash: 'invalid'}, + {uid: 'user11', email: 'user11@example.com', passwordSalt: 'invalid'}, + {uid: 'user12', providerData: [{providerId: 'google.com'}]}, + { + uid: 'user13', + providerData: [{providerId: 'google.com', uid: 'RAW_ID', displayName: false}], + }, + { + uid: 'user14', + providerData: [{providerId: 'google.com', uid: 'RAW_ID', email: 'invalid'}], + }, + { + uid: 'user15', + providerData: [{providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid'}], + }, + {uid: 'user16', providerData: [{}]}, + {email: 'user17@example.com'}, + ] as any; + const validOptions = { + hash: { + algorithm: 'BCRYPT', + }, + } as any; + const expectedResult = { + successCount: 0, + failureCount: testUsers.length, + errors: [ + {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME)}, + {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, + {index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, + {index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, + {index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED)}, + {index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL)}, + {index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD)}, + {index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME)}, + {index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME)}, + { + index: 9, + error: new FirebaseAuthError( + AuthClientErrorCode.FORBIDDEN_CLAIM, + `Developer claim "aud" is reserved and cannot be specified.`, + ), + }, + {index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH)}, + {index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT)}, + { + index: 12, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_UID, + `The provider "uid" for "google.com" must be a valid non-empty string.`, + ), + }, + { + index: 13, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_DISPLAY_NAME, + `The provider "displayName" for "google.com" must be a valid string.`, + ), + }, + { + index: 14, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_EMAIL, + `The provider "email" for "google.com" must be a valid email string.`, + ), + }, + { + index: 15, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_PHOTO_URL, + `The provider "photoURL" for "google.com" must be a valid URL string.`, + ), + }, + {index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)}, + {index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, + ], + }; + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); - it('should be rejected given invalid parameters such as email', () => { - // Expected error when an invalid email is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request with invalid email. - return requestHandler.updateExistingAccount(uid, invalidData) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.uploadAccount(testUsers, validOptions) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.not.been.called; + }); + }); - it('should be rejected given invalid parameters such as phoneNumber', () => { - // Expected error when an invalid phone number is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request with invalid phone number. - return requestHandler.updateExistingAccount(uid, invalidPhoneNumberData) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid phone number error should be thrown. - expect(error).to.deep.equal(expectedError); + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INTERNAL_ERROR', + }, }); - }); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + `An internal error has occurred. Raw server response: ` + + `"${JSON.stringify(expectedServerError.response.data)}"`, + ); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const expectedResult = utils.errorFrom({ - error: { - message: 'OPERATION_NOT_ALLOWED', - }, + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(users, options); + return requestHandler.uploadAccount(users, options) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateExistingAccount(uid, validData) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidData)); - }); }); - }); - describe('setCustomUserClaims', () => { - const path = '/v1/projects/project_id/accounts:update'; - const method = 'POST'; - const uid = '12345678'; - const claims = {admin: true, groupId: '1234'}; - const expectedValidData = { - localId: uid, - customAttributes: JSON.stringify(claims), - }; - const expectedEmptyClaimsData = { - localId: uid, - customAttributes: JSON.stringify({}), - }; - const expectedResult = utils.responseFrom({ - localId: uid, - }); + describe('downloadAccount', () => { + const path = handler.path('v1', '/accounts:batchGet', 'project_id'); + const method = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 500; + const expectedResult = utils.responseFrom({ + users : [ + {localId: 'uid1'}, + {localId: 'uid2'}, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + it('should be fulfilled given a valid parameters', () => { + const data = { + maxResults, + nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be fulfilled given a valid localId and customAttributes', () => { - // Successful result server response. - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send empty request. - return requestHandler.setCustomUserClaims(uid, claims) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidData)); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be fulfilled with empty user array when no users exist', () => { + const data = { + maxResults, + nextPageToken, + }; - it('should be fulfilled given valid localId and null claims', () => { - // Successful result server response. - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request to delete custom claims. - return requestHandler.setCustomUserClaims(uid, null) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedEmptyClaimsData)); - }); - }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); - it('should be rejected given invalid parameters such as uid', () => { - // Expected error when an invalid uid is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request with invalid uid. - return requestHandler.setCustomUserClaims('', claims) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({users: []}); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + maxResults: 1000, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be rejected given invalid parameters such as customClaims', () => { - // Expected error when invalid claims are provided. - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'CustomUserClaims argument must be an object or null.', - ); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request with invalid claims. - return requestHandler.setCustomUserClaims(uid, 'invalid' as any) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount() + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected given an invalid maxResults', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `Required "maxResults" must be a positive integer that does not ` + + `exceed 1000.`, + ); - it('should be rejected given customClaims with blacklisted claims', () => { - // Expected error when invalid claims are provided. - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, - ); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - const blacklistedClaims = {admin: true, aud: 'bla'}; - // Send request with blacklisted claims. - return requestHandler.setCustomUserClaims(uid, blacklistedClaims) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Forbidden claims error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(1001, nextPageToken) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); - const expectedServerError = utils.errorFrom({ - error: { - message: 'USER_NOT_FOUND', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, '') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.setCustomUserClaims(uid, claims) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidData)); + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_PAGE_SELECTION', + }, }); - }); - }); - - describe('revokeRefreshTokens', () => { - const path = '/v1/projects/project_id/accounts:update'; - const method = 'POST'; - const uid = '12345678'; - const now = new Date(); - const expectedResult = utils.responseFrom({ - localId: uid, - }); - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - clock = sinon.useFakeTimers(now.getTime()); - }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + maxResults, + nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - afterEach(() => { - clock.restore(); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, nextPageToken) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - it('should be fulfilled given a valid uid', () => { - const requestData = { - localId: uid, - // Current time should be passed, rounded up. - validSince: Math.ceil((now.getTime() + 5000) / 1000), - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Simulate 5 seconds passed. - clock.tick(5000); - return requestHandler.revokeRefreshTokens(uid) - .then((returnedUid: string) => { - expect(returnedUid).to.be.equal(uid); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + describe('deleteAccount', () => { + const path = handler.path('v1', '/accounts:delete', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid localId', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#DeleteAccountResponse', }); - }); + const data = {localId: 'uid'}; - it('should be rejected given an invalid uid', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); - const invalidUid: any = {localId: uid}; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.revokeRefreshTokens(invalidUid as any) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.deleteAccount('uid') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, }); - }); + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const data = {localId: 'uid'}; - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); - const expectedServerError = utils.errorFrom({ - error: { - message: 'USER_NOT_FOUND', - }, + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.deleteAccount('uid') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); }); - const requestData = { - localId: uid, - validSince: Math.ceil((now.getTime() + 5000) / 1000), - }; - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Simulate 5 seconds passed. - clock.tick(5000); - return requestHandler.revokeRefreshTokens(uid) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); - }); }); - }); - describe('createNewAccount', () => { - describe('with uid specified', () => { - const path = '/v1/projects/project_id/accounts'; + describe('updateExistingAccount', () => { + const path = handler.path('v1', '/accounts:update', 'project_id'); const method = 'POST'; const uid = '12345678'; const validData = { - uid, displayName: 'John Doe', email: 'user@example.com', emailVerified: true, @@ -1947,11 +1581,37 @@ describe('FirebaseAuthRequestHandler', () => { displayName: 'John Doe', email: 'user@example.com', emailVerified: true, - disabled: false, + disableUser: false, photoUrl: 'http://localhost/1234/photo.png', password: 'password', phoneNumber: '+11234567890', }; + // Valid request to delete photoURL and displayName. + const validDeleteData = deepCopy(validData); + validDeleteData.displayName = null; + validDeleteData.photoURL = null; + const expectedValidDeleteData = { + localId: uid, + email: 'user@example.com', + emailVerified: true, + disableUser: false, + password: 'password', + phoneNumber: '+11234567890', + deleteAttribute: ['DISPLAY_NAME', 'PHOTO_URL'], + }; + // Valid request to delete phoneNumber. + const validDeletePhoneNumberData = deepCopy(validData); + validDeletePhoneNumberData.phoneNumber = null; + const expectedValidDeletePhoneNumberData = { + localId: uid, + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disableUser: false, + photoUrl: 'http://localhost/1234/photo.png', + password: 'password', + deleteProvider: ['phone'], + }; const invalidData = { uid, email: 'user@invalid@', @@ -1960,40 +1620,40 @@ describe('FirebaseAuthRequestHandler', () => { uid, phoneNumber: 'invalid', }; - const emptyRequest = { - localId: uid, - }; + it('should be fulfilled given a valid localId', () => { - // Successful uploadAccount response. + // Successful result server response. const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SignupNewUserResponse', + kind: 'identitytoolkit#SetAccountInfoResponse', localId: uid, }); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send empty create new account request with only a uid provided. - return requestHandler.createNewAccount({uid}) + const requestHandler = handler.init(mockApp); + // Send empty update request. + return requestHandler.updateExistingAccount(uid, {}) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, emptyRequest)); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, {localId: uid})); }); }); it('should be fulfilled given valid parameters', () => { + // Successful result server response. const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SignupNewUserResponse', + kind: 'identitytoolkit#SetAccountInfoResponse', localId: uid, }); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Create a new account with all possible valid data. - return requestHandler.createNewAccount(validData) + const requestHandler = handler.init(mockApp); + // Send update request with all possible valid parameters. + return requestHandler.updateExistingAccount(uid, validData) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2003,73 +1663,112 @@ describe('FirebaseAuthRequestHandler', () => { }); }); + it('should be fulfilled given valid profile parameters to delete', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SetAccountInfoResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request to delete display name and photo URL. + return requestHandler.updateExistingAccount(uid, validDeleteData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, displayName + // and photoURL removed from request and deleteAttribute added. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidDeleteData)); + }); + }); + + it('should be fulfilled given phone number to delete', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SetAccountInfoResponse', + localId: uid, + }); + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request to delete phone number. + return requestHandler.updateExistingAccount(uid, validDeletePhoneNumberData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, phoneNumber + // removed from request and deleteProvider added. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidDeletePhoneNumberData)); + }); + }); + it('should be rejected given invalid parameters such as email', () => { // Expected error when an invalid email is provided. const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Create new account with invalid email. - return requestHandler.createNewAccount(invalidData) + const requestHandler = handler.init(mockApp); + // Send update request with invalid email. + return requestHandler.updateExistingAccount(uid, invalidData) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { - // Expected invalid email error should be thrown. + // Invalid email error should be thrown. expect(error).to.deep.equal(expectedError); }); }); - it('should be rejected given invalid parameters such as phoneNumber', () => { - // Expected error when an invalid phone number is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Create new account with invalid phone number. - return requestHandler.createNewAccount(invalidPhoneNumberData) + it('should be rejected given a tenant ID to modify', () => { + const dataWithModifiedTenantId = deepCopy(validData); + (dataWithModifiedTenantId as any).tenantId = 'MODIFIED_TENANT_ID'; + // Expected error when a tenant ID is provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'Tenant ID cannot be modified on an existing user.', + ); + const requestHandler = handler.init(mockApp); + // Send update request with tenant ID. + return requestHandler.updateExistingAccount(uid, dataWithModifiedTenantId) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { - // Expected invalid phone number error should be thrown. + // Invalid argument error should be thrown. expect(error).to.deep.equal(expectedError); }); }); - it('should be rejected when the backend returns a user exists error', () => { - // Expected error when the uid already exists. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.UID_ALREADY_EXISTS); - const expectedResult = utils.errorFrom({ - error: { - message: 'DUPLICATE_LOCAL_ID', - }, - }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request and simulate a backend error that the user - // already exists. - return requestHandler.createNewAccount(validData) + it('should be rejected given invalid parameters such as phoneNumber', () => { + // Expected error when an invalid phone number is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const requestHandler = handler.init(mockApp); + // Send update request with invalid phone number. + return requestHandler.updateExistingAccount(uid, invalidPhoneNumberData) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { + // Invalid phone number error should be thrown. expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidData)); }); }); - it('should be rejected when the backend returns an email exists error', () => { - // Expected error when the email already exists. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.EMAIL_ALREADY_EXISTS); + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); const expectedResult = utils.errorFrom({ error: { - message: 'EMAIL_EXISTS', + message: 'OPERATION_NOT_ALLOWED', }, }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request and simulate a backend error that the email - // already exists. - return requestHandler.createNewAccount(validData) + const requestHandler = handler.init(mockApp); + return requestHandler.updateExistingAccount(uid, validData) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { @@ -2078,125 +1777,121 @@ describe('FirebaseAuthRequestHandler', () => { callParams(path, method, expectedValidData)); }); }); + }); - it('should be rejected when the backend returns a generic error', () => { - // Some generic backend error. - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const expectedResult = utils.errorFrom({ - error: { - message: 'OPERATION_NOT_ALLOWED', - }, - }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + describe('setCustomUserClaims', () => { + const path = handler.path('v1', '/accounts:update', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const claims = {admin: true, groupId: '1234'}; + const expectedValidData = { + localId: uid, + customAttributes: JSON.stringify(claims), + }; + const expectedEmptyClaimsData = { + localId: uid, + customAttributes: JSON.stringify({}), + }; + const expectedResult = utils.responseFrom({ + localId: uid, + }); + + it('should be fulfilled given a valid localId and customAttributes', () => { + // Successful result server response. + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request with valid data but simulate backend error. - return requestHandler.createNewAccount(validData) + const requestHandler = handler.init(mockApp); + // Send empty request. + return requestHandler.setCustomUserClaims(uid, claims) .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); }); - }); - - describe('with no uid specified', () => { - const path = '/v1/projects/project_id/accounts'; - const method = 'POST'; - const uid = '12345678'; - const validData = { - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disabled: false, - photoURL: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - ignoredProperty: 'value', - }; - const expectedValidData = { - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disabled: false, - photoUrl: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - }; - const invalidData = { - email: 'user@invalid@', - }; - const invalidPhoneNumberData = { - uid, - phoneNumber: 'invalid', - }; - it('should be fulfilled given valid parameters', () => { - // signupNewUser successful response. - const expectedResult = utils.responseFrom({ - kind: 'identitytoolkit#SignupNewUserResponse', - localId: uid, - }); + it('should be fulfilled given valid localId and null claims', () => { + // Successful result server response. const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request with valid data. - return requestHandler.createNewAccount(validData) + const requestHandler = handler.init(mockApp); + // Send request to delete custom claims. + return requestHandler.setCustomUserClaims(uid, null) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); // Confirm expected rpc request parameters sent. expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, expectedValidData)); + callParams(path, method, expectedEmptyClaimsData)); }); }); - it('should be rejected given invalid parameters such as email', () => { - // Expected error when an invalid email is provided. - const expectedError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request with invalid data. - return requestHandler.createNewAccount(invalidData) + it('should be rejected given invalid parameters such as uid', () => { + // Expected error when an invalid uid is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + const requestHandler = handler.init(mockApp); + // Send request with invalid uid. + return requestHandler.setCustomUserClaims('', claims) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { + // Invalid uid error should be thrown. expect(error).to.deep.equal(expectedError); }); }); - it('should be rejected given invalid parameters such as phone number', () => { - // Expected error when an invalid phone number is provided. - const expectedError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request with invalid data. - return requestHandler.createNewAccount(invalidPhoneNumberData) + it('should be rejected given invalid parameters such as customClaims', () => { + // Expected error when invalid claims are provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'CustomUserClaims argument must be an object or null.', + ); + const requestHandler = handler.init(mockApp); + // Send request with invalid claims. + return requestHandler.setCustomUserClaims(uid, 'invalid' as any) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { + // Invalid argument error should be thrown. expect(error).to.deep.equal(expectedError); }); }); - it('should be rejected when the backend returns a generic error', () => { - // Some generic backend error. - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const expectedResult = utils.errorFrom({ + it('should be rejected given customClaims with blacklisted claims', () => { + // Expected error when invalid claims are provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.FORBIDDEN_CLAIM, + `Developer claim "aud" is reserved and cannot be specified.`, + ); + const requestHandler = handler.init(mockApp); + const blacklistedClaims = {admin: true, aud: 'bla'}; + // Send request with blacklisted claims. + return requestHandler.setCustomUserClaims(uid, blacklistedClaims) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + // Forbidden claims error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); + const expectedServerError = utils.errorFrom({ error: { - message: 'OPERATION_NOT_ALLOWED', + message: 'USER_NOT_FOUND', }, }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send valid create new account request and simulate backend error. - return requestHandler.createNewAccount(validData) + const requestHandler = handler.init(mockApp); + return requestHandler.setCustomUserClaims(uid, claims) .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { @@ -2206,1284 +1901,1650 @@ describe('FirebaseAuthRequestHandler', () => { }); }); }); - }); - describe('getEmailActionLink', () => { - const path = '/v1/projects/project_id/accounts:sendOobCode'; - const method = 'POST'; - const email = 'user@example.com'; - const actionCodeSettings = { - url: 'https://www.example.com/path/file?a=1&b=2', - handleCodeInApp: true, - iOS: { - bundleId: 'com.example.ios', - }, - android: { - packageName: 'com.example.android', - installApp: true, - minimumVersion: '6', - }, - dynamicLinkDomain: 'custom.page.link', - }; - const expectedActionCodeSettingsRequest = new ActionCodeSettingsBuilder(actionCodeSettings).buildRequest(); - const expectedLink = 'https://custom.page.link?link=' + - encodeURIComponent('https://projectId.firebaseapp.com/__/auth/action?oobCode=CODE') + - '&apn=com.example.android&ibi=com.example.ios'; - const expectedResult = utils.responseFrom({ - email, - oobLink: expectedLink, - }); + describe('revokeRefreshTokens', () => { + const path = handler.path('v1', '/accounts:update', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const now = new Date(); + const expectedResult = utils.responseFrom({ + localId: uid, + }); + let clock: sinon.SinonFakeTimers; - it('should be fulfilled given a valid email', () => { - const requestData = deepExtend({ - requestType: 'PASSWORD_RESET', - email, - returnOobLink: true, - }, expectedActionCodeSettingsRequest); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink('PASSWORD_RESET', email, actionCodeSettings) - .then((oobLink: string) => { - expect(oobLink).to.be.equal(expectedLink); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); - }); - }); + beforeEach(() => { + clock = sinon.useFakeTimers(now.getTime()); + }); - EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { - it('should be fulfilled given a valid requestType:' + requestType + ' and ActionCodeSettings', () => { - const requestData = deepExtend({ - requestType, - email, - returnOobLink: true, - }, expectedActionCodeSettingsRequest); + afterEach(() => { + clock.restore(); + }); + + it('should be fulfilled given a valid uid', () => { + const requestData = { + localId: uid, + // Current time should be passed, rounded up. + validSince: Math.ceil((now.getTime() + 5000) / 1000), + }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings) - .then((oobLink: string) => { - expect(oobLink).to.be.equal(expectedLink); + const requestHandler = handler.init(mockApp); + // Simulate 5 seconds passed. + clock.tick(5000); + return requestHandler.revokeRefreshTokens(uid) + .then((returnedUid: string) => { + expect(returnedUid).to.be.equal(uid); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); - }); - EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { - if (requestType === 'EMAIL_SIGNIN') { - return; - } - it('should be fulfilled given requestType:' + requestType + ' and no ActionCodeSettings', () => { + it('should be rejected given an invalid uid', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + const invalidUid: any = {localId: uid}; + + const requestHandler = handler.init(mockApp); + return requestHandler.revokeRefreshTokens(invalidUid as any) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid uid error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); + const expectedServerError = utils.errorFrom({ + error: { + message: 'USER_NOT_FOUND', + }, + }); const requestData = { - requestType, - email, - returnOobLink: true, + localId: uid, + validSince: Math.ceil((now.getTime() + 5000) / 1000), }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink(requestType, email) - .then((oobLink: string) => { - expect(oobLink).to.be.equal(expectedLink); + const requestHandler = handler.init(mockApp); + // Simulate 5 seconds passed. + clock.tick(5000); + return requestHandler.revokeRefreshTokens(uid) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); }); - it('should be rejected given requestType:EMAIL_SIGNIN and no ActionCodeSettings', () => { - const invalidRequestType = 'EMAIL_SIGNIN'; - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `"ActionCodeSettings" must be a non-null object.`, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink('EMAIL_SIGNIN', email) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected given an invalid email', () => { - const invalidEmail = 'invalid'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink('PASSWORD_RESET', invalidEmail, actionCodeSettings) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected given an invalid request type', () => { - const invalidRequestType = 'invalid'; - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `"invalid" is not a supported email action request type.`, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink(invalidRequestType, email, actionCodeSettings) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); + describe('createNewAccount', () => { + describe('with uid specified', () => { + const path = handler.path('v1', '/accounts', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const validData = { + uid, + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoURL: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + ignoredProperty: 'value', + // Tenant ID accepted on creation and relayed to Auth server. + tenantId: 'TENANT_ID', + }; + const expectedValidData = { + localId: uid, + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoUrl: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + tenantId: 'TENANT_ID', + }; + const invalidData = { + uid, + email: 'user@invalid@', + }; + const invalidPhoneNumberData = { + uid, + phoneNumber: 'invalid', + }; + const emptyRequest = { + localId: uid, + }; + it('should be fulfilled given a valid localId', () => { + // Successful uploadAccount response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SignupNewUserResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send empty create new account request with only a uid provided. + return requestHandler.createNewAccount({uid}) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, emptyRequest)); + }); + }); + + it('should be fulfilled given valid parameters', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SignupNewUserResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Create a new account with all possible valid data. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected given invalid parameters such as email', () => { + // Expected error when an invalid email is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const requestHandler = handler.init(mockApp); + // Create new account with invalid email. + return requestHandler.createNewAccount(invalidData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected invalid email error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected given invalid parameters such as phoneNumber', () => { + // Expected error when an invalid phone number is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const requestHandler = handler.init(mockApp); + // Create new account with invalid phone number. + return requestHandler.createNewAccount(invalidPhoneNumberData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected invalid phone number error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected when the backend returns a user exists error', () => { + // Expected error when the uid already exists. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.UID_ALREADY_EXISTS); + const expectedResult = utils.errorFrom({ + error: { + message: 'DUPLICATE_LOCAL_ID', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request and simulate a backend error that the user + // already exists. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected when the backend returns an email exists error', () => { + // Expected error when the email already exists. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.EMAIL_ALREADY_EXISTS); + const expectedResult = utils.errorFrom({ + error: { + message: 'EMAIL_EXISTS', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request and simulate a backend error that the email + // already exists. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected when the backend returns a generic error', () => { + // Some generic backend error. + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request with valid data but simulate backend error. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + }); + + describe('with no uid specified', () => { + const path = handler.path('v1', '/accounts', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const validData = { + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoURL: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + ignoredProperty: 'value', + }; + const expectedValidData = { + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoUrl: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + }; + const invalidData = { + email: 'user@invalid@', + }; + const invalidPhoneNumberData = { + uid, + phoneNumber: 'invalid', + }; - it('should be rejected given an invalid ActionCodeSettings object', () => { - const invalidActionCodeSettings = 'invalid' as any; - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - '"ActionCodeSettings" must be a non-null object.', - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink('EMAIL_SIGNIN', email, invalidActionCodeSettings) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); + it('should be fulfilled given valid parameters', () => { + // signupNewUser successful response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SignupNewUserResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send request with valid data. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected given invalid parameters such as email', () => { + // Expected error when an invalid email is provided. + const expectedError = + new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const requestHandler = handler.init(mockApp); + // Send create new account request with invalid data. + return requestHandler.createNewAccount(invalidData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected given invalid parameters such as phone number', () => { + // Expected error when an invalid phone number is provided. + const expectedError = + new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const requestHandler = handler.init(mockApp); + // Send create new account request with invalid data. + return requestHandler.createNewAccount(invalidPhoneNumberData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected when the backend returns a generic error', () => { + // Some generic backend error. + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); - it('should be rejected when the response does not contain a link', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create the email action link'); - const requestData = deepExtend({ - requestType: 'VERIFY_EMAIL', - email, - returnOobLink: true, - }, expectedActionCodeSettingsRequest); - // Simulate response missing link. - const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({email})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + const requestHandler = handler.init(mockApp); + // Send valid create new account request and simulate backend error. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); }); + }); }); - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); - const expectedServerError = utils.errorFrom({ - error: { - message: 'USER_NOT_FOUND', + describe('getEmailActionLink', () => { + const path = handler.path('v1', '/accounts:sendOobCode', 'project_id'); + const method = 'POST'; + const email = 'user@example.com'; + const actionCodeSettings = { + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: { + bundleId: 'com.example.ios', }, - }); - const requestData = deepExtend({ - requestType: 'VERIFY_EMAIL', + android: { + packageName: 'com.example.android', + installApp: true, + minimumVersion: '6', + }, + dynamicLinkDomain: 'custom.page.link', + }; + const expectedActionCodeSettingsRequest = new ActionCodeSettingsBuilder(actionCodeSettings).buildRequest(); + const expectedLink = 'https://custom.page.link?link=' + + encodeURIComponent('https://projectId.firebaseapp.com/__/auth/action?oobCode=CODE') + + '&apn=com.example.android&ibi=com.example.ios'; + const expectedResult = utils.responseFrom({ email, - returnOobLink: true, - }, expectedActionCodeSettingsRequest); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); - }); - }); - }); + oobLink: expectedLink, + }); - describe('getOAuthIdpConfig()', () => { - const providerId = 'oidc.provider'; - const path = `/v2beta1/projects/project_id/oauthIdpConfigs/${providerId}`; - const expectedHttpMethod = 'GET'; - const expectedResult = utils.responseFrom({ - name: `projects/project1/oauthIdpConfigs/${providerId}`, - }); + it('should be fulfilled given a valid email', () => { + const requestData = deepExtend({ + requestType: 'PASSWORD_RESET', + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('PASSWORD_RESET', email, actionCodeSettings) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); - it('should be fulfilled given a valid provider ID', () => { - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); + EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { + it('should be fulfilled given a valid requestType:' + requestType + ' and ActionCodeSettings', () => { + const requestData = deepExtend({ + requestType, + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + }); + + EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { + if (requestType === 'EMAIL_SIGNIN') { + return; + } + it('should be fulfilled given requestType:' + requestType + ' and no ActionCodeSettings', () => { + const requestData = { + requestType, + email, + returnOobLink: true, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getOAuthIdpConfig(providerId) - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, {})); + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(requestType, email) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); }); - }); + }); - const invalidProviderIds = [ - null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { - it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + it('should be rejected given requestType:EMAIL_SIGNIN and no ActionCodeSettings', () => { + const invalidRequestType = 'EMAIL_SIGNIN'; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"ActionCodeSettings" must be a non-null object.`, + ); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getOAuthIdpConfig(invalidProviderId as any) - .then((result) => { + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('EMAIL_SIGNIN', email) + .then((resp) => { throw new Error('Unexpected success'); }, (error) => { + // Invalid argument error should be thrown. expect(error).to.deep.equal(expectedError); }); }); - }); - it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); - const expectedServerError = utils.errorFrom({ - error: { - message: 'CONFIGURATION_NOT_FOUND', - }, - }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getOAuthIdpConfig(providerId) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, {})); - }); - }); - }); + it('should be rejected given an invalid email', () => { + const invalidEmail = 'invalid'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - describe('listOAuthIdpConfigs()', () => { - const path = '/v2beta1/projects/project_id/oauthIdpConfigs'; - const expectedHttpMethod = 'GET'; - const nextPageToken = 'PAGE_TOKEN'; - const maxResults = 50; - const expectedResult = utils.responseFrom({ - oauthIdpConfigs : [ - {name: 'projects/project1/oauthIdpConfigs/oidc.provider1'}, - {name: 'projects/project1/oauthIdpConfigs/oidc.provider2'}, - ], - nextPageToken: 'NEXT_PAGE_TOKEN', - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('PASSWORD_RESET', invalidEmail, actionCodeSettings) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid email error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); - it('should be fulfilled given a valid parameters', () => { - const data = { - pageSize: maxResults, - pageToken: nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, data)); - }); - }); + it('should be rejected given an invalid request type', () => { + const invalidRequestType = 'invalid'; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"invalid" is not a supported email action request type.`, + ); - it('should be fulfilled with empty configuration array when no configurations exist', () => { - const data = { - pageSize: maxResults, - pageToken: nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal({oauthIdpConfigs: []}); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, data)); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(invalidRequestType, email, actionCodeSettings) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); - it('should be fulfilled given no parameters', () => { - // Default maxResults should be used. - const data = { - pageSize: 100, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listOAuthIdpConfigs() - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, data)); - }); - }); + it('should be rejected given an invalid ActionCodeSettings object', () => { + const invalidActionCodeSettings = 'invalid' as any; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings" must be a non-null object.', + ); - it('should be rejected given an invalid maxResults', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 100.`, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listOAuthIdpConfigs(101, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('EMAIL_SIGNIN', email, invalidActionCodeSettings) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.equal(expectedError); + }); + }); - it('should be rejected given an invalid next page token', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listOAuthIdpConfigs(maxResults, '') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); + it('should be rejected when the response does not contain a link', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create the email action link'); + const requestData = deepExtend({ + requestType: 'VERIFY_EMAIL', + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + // Simulate response missing link. + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({email})); + stubs.push(stub); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_PAGE_SELECTION', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings) + .then((returnedUid: string) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); }); - const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); - const data = { - pageSize: maxResults, - pageToken: nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, data)); - }); - }); - }); - describe('deleteOAuthIdpConfig()', () => { - const providerId = 'oidc.provider'; - const path = `/v2beta1/projects/project_id/oauthIdpConfigs/${providerId}`; - const expectedHttpMethod = 'DELETE'; - const expectedResult = utils.responseFrom({}); - - it('should be fulfilled given a valid provider ID', () => { - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteOAuthIdpConfig(providerId) - .then((result) => { - expect(result).to.be.undefined; - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, {})); + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); + const expectedServerError = utils.errorFrom({ + error: { + message: 'USER_NOT_FOUND', + }, }); - }); - - const invalidProviderIds = [ - null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { - it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const requestData = deepExtend({ + requestType: 'VERIFY_EMAIL', + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteOAuthIdpConfig(invalidProviderId as any) - .then((result) => { + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings) + .then((returnedUid: string) => { throw new Error('Unexpected success'); }, (error) => { expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); }); - it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); - const expectedServerError = utils.errorFrom({ - error: { - message: 'CONFIGURATION_NOT_FOUND', - }, + describe('getOAuthIdpConfig()', () => { + const providerId = 'oidc.provider'; + const path = handler.path('v2beta1', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'GET'; + const expectedResult = utils.responseFrom({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteOAuthIdpConfig(providerId) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, {})); - }); - }); - }); - describe('createOAuthIdpConfig', () => { - const providerId = 'oidc.provider'; - const path = `/v2beta1/projects/project_id/oauthIdpConfigs?oauthIdpConfigId=${providerId}`; - const expectedHttpMethod = 'POST'; - const configOptions = { - providerId, - displayName: 'OIDC_DISPLAY_NAME', - enabled: true, - clientId: 'CLIENT_ID', - issuer: 'https://oidc.com/issuer', - }; - const expectedRequest = { - displayName: 'OIDC_DISPLAY_NAME', - enabled: true, - clientId: 'CLIENT_ID', - issuer: 'https://oidc.com/issuer', - }; - const expectedResult = utils.responseFrom(deepExtend({ - name: `projects/project1/oauthIdpConfigs/${providerId}`, - }, expectedRequest)); - - it('should be fulfilled given valid parameters', () => { - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createOAuthIdpConfig(configOptions) - .then((response) => { - expect(response).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, expectedRequest)); - }); - }); + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be rejected given invalid parameters', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', - ); - const invalidOptions: OIDCAuthProviderConfig = deepCopy(configOptions); - invalidOptions.issuer = 'invalid'; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createOAuthIdpConfig(invalidOptions) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getOAuthIdpConfig(providerId) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); - it('should be rejected when the backend returns a response missing name', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', - ); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createOAuthIdpConfig(configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, expectedRequest)); + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.getOAuthIdpConfig(invalidProviderId as any) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getOAuthIdpConfig(providerId) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); }); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_CONFIG', - }, + describe('listOAuthIdpConfigs()', () => { + const path = handler.path('v2beta1', '/oauthIdpConfigs', 'project_id'); + const expectedHttpMethod = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 50; + const expectedResult = utils.responseFrom({ + oauthIdpConfigs : [ + {name: 'projects/project1/oauthIdpConfigs/oidc.provider1'}, + {name: 'projects/project1/oauthIdpConfigs/oidc.provider2'}, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + + it('should be fulfilled given a valid parameters', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); + + it('should be fulfilled with empty configuration array when no configurations exist', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({oauthIdpConfigs: []}); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createOAuthIdpConfig(configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, expectedRequest)); + + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + pageSize: 100, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs() + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); + + it('should be rejected given an invalid maxResults', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `Required "maxResults" must be a positive integer that does not ` + + `exceed 100.`, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(101, nextPageToken) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, '') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_PAGE_SELECTION', + }, }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); }); - }); - describe('updateOAuthIdpConfig()', () => { - const providerId = 'oidc.provider'; - const path = `/v2beta1/projects/project_id/oauthIdpConfigs/${providerId}`; - const expectedHttpMethod = 'PATCH'; - const configOptions = { - displayName: 'OIDC_DISPLAY_NAME', - enabled: true, - clientId: 'CLIENT_ID', - issuer: 'https://oidc.com/issuer', - }; - const expectedRequest = { - displayName: 'OIDC_DISPLAY_NAME', - enabled: true, - clientId: 'CLIENT_ID', - issuer: 'https://oidc.com/issuer', - }; - const expectedResult = utils.responseFrom(deepExtend({ - name: `projects/project_id/oauthIdpConfigs/${providerId}`, - }, expectedRequest)); - const expectedPartialResult = utils.responseFrom(deepExtend({ - name: `projects/project_id/oauthIdpConfigs/${providerId}`, - }, { - displayName: 'OIDC_DISPLAY_NAME', - enabled: false, - clientId: 'NEW_CLIENT_ID', - issuer: 'https://oidc.com/issuer2', - })); - - it('should be fulfilled given full parameters', () => { - const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(providerId, configOptions) - .then((response) => { - expect(response).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, expectedRequest)); + describe('deleteOAuthIdpConfig()', () => { + const providerId = 'oidc.provider'; + const path = handler.path('v2beta1', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'DELETE'; + const expectedResult = utils.responseFrom({}); + + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteOAuthIdpConfig(providerId) + .then((result) => { + expect(result).to.be.undefined; + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteOAuthIdpConfig(invalidProviderId as any) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - }); + }); - it('should be fulfilled given partial parameters', () => { - const expectedPath = path + '?updateMask=enabled,clientId'; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); - const partialConfigOptions = { - enabled: false, - clientId: 'NEW_CLIENT_ID', - }; - const partialRequest: OIDCUpdateAuthProviderRequest = { - enabled: false, - displayName: undefined, - issuer: undefined, - clientId: 'NEW_CLIENT_ID', - }; - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(providerId, partialConfigOptions) - .then((response) => { - expect(response).to.deep.equal(expectedPartialResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, partialRequest)); + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteOAuthIdpConfig(providerId) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); }); - it('should be fulfilled given single parameter to change', () => { - const expectedPath = path + '?updateMask=issuer'; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); - const partialConfigOptions = { - issuer: 'https://oidc.com/issuer2', + describe('createOAuthIdpConfig', () => { + const providerId = 'oidc.provider'; + const path = handler.path('v2beta1', `/oauthIdpConfigs?oauthIdpConfigId=${providerId}`, 'project_id'); + const expectedHttpMethod = 'POST'; + const configOptions = { + providerId, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', }; - const partialRequest: OIDCUpdateAuthProviderRequest = { - clientId: undefined, - displayName: undefined, - enabled: undefined, - issuer: 'https://oidc.com/issuer2', + const expectedRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', }; - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(providerId, partialConfigOptions) - .then((response) => { - expect(response).to.deep.equal(expectedPartialResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, partialRequest)); - }); - }); + const expectedResult = utils.responseFrom(deepExtend({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, + }, expectedRequest)); - const invalidProviderIds = [ - null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { - it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + it('should be fulfilled given valid parameters', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(invalidProviderId as any, configOptions) + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', + ); + const invalidOptions: OIDCAuthProviderConfig = deepCopy(configOptions); + invalidOptions.issuer = 'invalid'; + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(invalidOptions) .then((result) => { throw new Error('Unexpected success'); }, (error) => { expect(error).to.deep.equal(expectedError); }); }); - }); - it('should be rejected given invalid parameters', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', - ); - const invalidOptions: OIDCUpdateAuthProviderRequest = deepCopy(configOptions); - invalidOptions.issuer = 'invalid'; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(providerId, invalidOptions) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); + it('should be rejected when the backend returns a response missing name', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); - it('should be rejected when the backend returns a response missing name', () => { - const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', - ); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(providerId, configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, expectedRequest)); + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_CONFIG', + }, }); - }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - it('should be rejected when the backend returns an error', () => { - const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_CONFIG', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateOAuthIdpConfig(providerId, configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, expectedRequest)); - }); }); - }); - describe('getInboundSamlConfig()', () => { - const providerId = 'saml.provider'; - const path = `/v2beta1/projects/project_id/inboundSamlConfigs/${providerId}`; - const expectedHttpMethod = 'GET'; - const expectedResult = utils.responseFrom({ - name: `projects/project1/inboundSamlConfigs/${providerId}`, - }); + describe('updateOAuthIdpConfig()', () => { + const providerId = 'oidc.provider'; + const path = handler.path('v2beta1', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'PATCH'; + const configOptions = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + }, expectedRequest)); + const expectedPartialResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + }, { + displayName: 'OIDC_DISPLAY_NAME', + enabled: false, + clientId: 'NEW_CLIENT_ID', + issuer: 'https://oidc.com/issuer2', + })); + + it('should be fulfilled given full parameters', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be fulfilled given partial parameters', () => { + const expectedPath = path + '?updateMask=enabled,clientId'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + enabled: false, + clientId: 'NEW_CLIENT_ID', + }; + const partialRequest: OIDCUpdateAuthProviderRequest = { + enabled: false, + displayName: undefined, + issuer: undefined, + clientId: 'NEW_CLIENT_ID', + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); + + it('should be fulfilled given single parameter to change', () => { + const expectedPath = path + '?updateMask=issuer'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + issuer: 'https://oidc.com/issuer2', + }; + const partialRequest: OIDCUpdateAuthProviderRequest = { + clientId: undefined, + displayName: undefined, + enabled: undefined, + issuer: 'https://oidc.com/issuer2', + }; + stubs.push(stub); - it('should be fulfilled given a valid provider ID', () => { - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getInboundSamlConfig(providerId) - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(invalidProviderId as any, configOptions) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - }); + }); - const invalidProviderIds = [ - null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { - it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', + ); + const invalidOptions: OIDCUpdateAuthProviderRequest = deepCopy(configOptions); + invalidOptions.issuer = 'invalid'; - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getInboundSamlConfig(invalidProviderId as any) + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, invalidOptions) .then((result) => { throw new Error('Unexpected success'); }, (error) => { expect(error).to.deep.equal(expectedError); }); }); - }); - it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); - const expectedServerError = utils.errorFrom({ - error: { - message: 'CONFIGURATION_NOT_FOUND', - }, + it('should be rejected when the backend returns a response missing name', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getInboundSamlConfig(providerId) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + it('should be rejected when the backend returns an error', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_CONFIG', + }, }); - }); - }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - describe('listInboundSamlConfigs()', () => { - const path = '/v2beta1/projects/project_id/inboundSamlConfigs'; - const expectedHttpMethod = 'GET'; - const nextPageToken = 'PAGE_TOKEN'; - const maxResults = 50; - const expectedResult = utils.responseFrom({ - inboundSamlConfigs : [ - {name: 'projects/project1/inboundSamlConfigs/saml.provider1'}, - {name: 'projects/project1/inboundSamlConfigs/saml.provider2'}, - ], - nextPageToken: 'NEXT_PAGE_TOKEN', + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); }); - it('should be fulfilled given a valid parameters', () => { - const data = { - pageSize: maxResults, - pageToken: nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); - }); - }); + describe('getInboundSamlConfig()', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2beta1', `/inboundSamlConfigs/${providerId}`, 'project_id'); - it('should be fulfilled with empty configuration array when no configurations exist', () => { - const data = { - pageSize: maxResults, - pageToken: nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal({inboundSamlConfigs: []}); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); - }); - }); + const expectedHttpMethod = 'GET'; + const expectedResult = utils.responseFrom({ + name: `projects/project1/inboundSamlConfigs/${providerId}`, + }); - it('should be fulfilled given no parameters', () => { - // Default maxResults should be used. - const data = { - pageSize: 100, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listInboundSamlConfigs() - .then((result) => { - expect(result).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); - }); - }); + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getInboundSamlConfig(providerId) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); - it('should be rejected given an invalid maxResults', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 100.`, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listInboundSamlConfigs(101, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); + const requestHandler = handler.init(mockApp); + return requestHandler.getInboundSamlConfig(invalidProviderId as any) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - }); + }); - it('should be rejected given an invalid next page token', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listInboundSamlConfigs(maxResults, '') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, }); - }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_PAGE_SELECTION', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.getInboundSamlConfig(providerId) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); }); - const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); - const data = { - pageSize: maxResults, - pageToken: nextPageToken, - }; - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); - }); }); - }); - describe('deleteInboundSamlConfig()', () => { - const providerId = 'saml.provider'; - const path = `/v2beta1/projects/project_id/inboundSamlConfigs/${providerId}`; - const expectedHttpMethod = 'DELETE'; - const expectedResult = utils.responseFrom({}); - - it('should be fulfilled given a valid provider ID', () => { - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteInboundSamlConfig(providerId) - .then((result) => { - expect(result).to.be.undefined; - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); - }); - }); + describe('listInboundSamlConfigs()', () => { + const path = handler.path('v2beta1', '/inboundSamlConfigs', 'project_id'); + const expectedHttpMethod = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 50; + const expectedResult = utils.responseFrom({ + inboundSamlConfigs : [ + {name: 'projects/project1/inboundSamlConfigs/saml.provider1'}, + {name: 'projects/project1/inboundSamlConfigs/saml.provider2'}, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + + it('should be fulfilled given a valid parameters', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); + }); + }); + + it('should be fulfilled with empty configuration array when no configurations exist', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({inboundSamlConfigs: []}); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); + }); + }); - const invalidProviderIds = [ - null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { - it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + pageSize: 100, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteInboundSamlConfig(invalidProviderId as any) + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs() .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); + }); + }); + + it('should be rejected given an invalid maxResults', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `Required "maxResults" must be a positive integer that does not ` + + `exceed 100.`, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(101, nextPageToken) + .then((resp) => { throw new Error('Unexpected success'); }, (error) => { expect(error).to.deep.equal(expectedError); }); }); - }); - it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); - const expectedServerError = utils.errorFrom({ - error: { - message: 'CONFIGURATION_NOT_FOUND', - }, + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, '') + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteInboundSamlConfig(providerId) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_PAGE_SELECTION', + }, }); - }); - }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - describe('createInboundSamlConfig', () => { - const providerId = 'saml.provider'; - const path = `/v2beta1/projects/project_id/inboundSamlConfigs?inboundSamlConfigId=${providerId}`; - const expectedHttpMethod = 'POST'; - const configOptions = { - providerId, - displayName: 'SAML_DISPLAY_NAME', - enabled: true, - idpEntityId: 'IDP_ENTITY_ID', - ssoURL: 'https://example.com/login', - x509Certificates: ['CERT1', 'CERT2'], - rpEntityId: 'RP_ENTITY_ID', - callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', - enableRequestSigning: true, - }; - const expectedRequest = { - idpConfig: { - idpEntityId: 'IDP_ENTITY_ID', - ssoUrl: 'https://example.com/login', - signRequest: true, - idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, - ], - }, - spConfig: { - spEntityId: 'RP_ENTITY_ID', - callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', - }, - displayName: 'SAML_DISPLAY_NAME', - enabled: true, - }; - const expectedResult = utils.responseFrom(deepExtend({ - name: 'projects/project1/inboundSamlConfigs/saml.provider', - }, expectedRequest)); - - it('should be fulfilled given valid parameters', () => { - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createInboundSamlConfig(configOptions) - .then((response) => { - expect(response).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, expectedRequest)); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); + }); + }); }); - it('should be rejected given invalid parameters', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.', - ); - const invalidOptions: SAMLAuthProviderConfig = deepCopy(configOptions); - invalidOptions.callbackURL = 'invalid'; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createInboundSamlConfig(invalidOptions) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); + describe('deleteInboundSamlConfig()', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2beta1', `/inboundSamlConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'DELETE'; + const expectedResult = utils.responseFrom({}); - it('should be rejected when the backend returns a response missing name', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', - ); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createInboundSamlConfig(configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, expectedRequest)); - }); - }); + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_CONFIG', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.deleteInboundSamlConfig(providerId) + .then((result) => { + expect(result).to.be.undefined; + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.createInboundSamlConfig(configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, expectedHttpMethod, expectedRequest)); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteInboundSamlConfig(invalidProviderId as any) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - }); - }); + }); - describe('updateInboundSamlConfig()', () => { - const providerId = 'saml.provider'; - const path = `/v2beta1/projects/project_id/inboundSamlConfigs/${providerId}`; - const expectedHttpMethod = 'PATCH'; - const configOptions = { - idpEntityId: 'IDP_ENTITY_ID', - ssoURL: 'https://example.com/login', - x509Certificates: ['CERT1', 'CERT2'], - rpEntityId: 'RP_ENTITY_ID', - callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', - enableRequestSigning: true, - enabled: true, - displayName: 'samlProviderName', - }; - const expectedRequest = { - idpConfig: { - idpEntityId: 'IDP_ENTITY_ID', - ssoUrl: 'https://example.com/login', - signRequest: true, - idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, - ], - }, - spConfig: { - spEntityId: 'RP_ENTITY_ID', - callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', - }, - displayName: 'samlProviderName', - enabled: true, - }; - const expectedResult = utils.responseFrom(deepExtend({ - name: `projects/project_id/inboundSamlConfigs/${providerId}`, - }, expectedRequest)); - const expectedPartialResult = utils.responseFrom(deepExtend({ - name: `projects/project_id/inboundSamlConfigs/${providerId}`, - }, { - idpConfig: { - idpEntityId: 'IDP_ENTITY_ID', - ssoUrl: 'https://example.com/login2', - signRequest: true, - idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, - ], - }, - spConfig: { - spEntityId: 'RP_ENTITY_ID2', - callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', - }, - displayName: 'samlProviderName', - enabled: false, - })); - const fullUpadateMask = - 'enabled,displayName,idpConfig.idpEntityId,idpConfig.ssoUrl,' + - 'idpConfig.signRequest,idpConfig.idpCertificates,spConfig.spEntityId,spConfig.callbackUri'; - - it('should be fulfilled given full parameters', () => { - const expectedPath = path + `?updateMask=${fullUpadateMask}`; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(providerId, configOptions) - .then((response) => { - expect(response).to.deep.equal(expectedResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, expectedRequest)); + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteInboundSamlConfig(providerId) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); + }); }); - it('should be fulfilled given partial parameters', () => { - const expectedPath = path + '?updateMask=enabled,idpConfig.ssoUrl,spConfig.spEntityId'; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); - const partialConfigOptions = { - ssoURL: 'https://example.com/login2', - rpEntityId: 'RP_ENTITY_ID2', - enabled: false, + describe('createInboundSamlConfig', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2beta1', `/inboundSamlConfigs?inboundSamlConfigId=${providerId}`, 'project_id'); + const expectedHttpMethod = 'POST'; + const configOptions = { + providerId, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, }; - const partialRequest: SAMLConfigServerResponse = { + const expectedRequest = { idpConfig: { - idpEntityId: undefined, - ssoUrl: 'https://example.com/login2', - signRequest: undefined, - idpCertificates: undefined, + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + {x509Certificate: 'CERT1'}, + {x509Certificate: 'CERT2'}, + ], }, spConfig: { - spEntityId: 'RP_ENTITY_ID2', - callbackUri: undefined, + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', }, - displayName: undefined, - enabled: false, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, }; - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(providerId, partialConfigOptions) - .then((response) => { - expect(response).to.deep.equal(expectedPartialResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, partialRequest)); + const expectedResult = utils.responseFrom(deepExtend({ + name: 'projects/project1/inboundSamlConfigs/saml.provider', + }, expectedRequest)); + + it('should be fulfilled given valid parameters', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.', + ); + const invalidOptions: SAMLAuthProviderConfig = deepCopy(configOptions); + invalidOptions.callbackURL = 'invalid'; + + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(invalidOptions) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); + }); + + it('should be rejected when the backend returns a response missing name', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_CONFIG', + }, }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); }); - it('should be fulfilled given single parameter to change', () => { - const expectedPath = path + '?updateMask=idpConfig.ssoUrl'; - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); - const partialConfigOptions = { - ssoURL: 'https://example.com/login2', + describe('updateInboundSamlConfig()', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2beta1', `/inboundSamlConfigs/${providerId}`, 'project_id'); + + const expectedHttpMethod = 'PATCH'; + const configOptions = { + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + enabled: true, + displayName: 'samlProviderName', }; - const partialRequest: SAMLConfigServerResponse = { + const expectedRequest = { idpConfig: { - idpEntityId: undefined, - ssoUrl: 'https://example.com/login2', - signRequest: undefined, - idpCertificates: undefined, + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + {x509Certificate: 'CERT1'}, + {x509Certificate: 'CERT2'}, + ], }, - displayName: undefined, - enabled: undefined, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'samlProviderName', + enabled: true, }; - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(providerId, partialConfigOptions) - .then((response) => { - expect(response).to.deep.equal(expectedPartialResult.data); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, partialRequest)); + const expectedResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + }, expectedRequest)); + const expectedPartialResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + }, { + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login2', + signRequest: true, + idpCertificates: [ + {x509Certificate: 'CERT1'}, + {x509Certificate: 'CERT2'}, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID2', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'samlProviderName', + enabled: false, + })); + const fullUpadateMask = + 'enabled,displayName,idpConfig.idpEntityId,idpConfig.ssoUrl,' + + 'idpConfig.signRequest,idpConfig.idpCertificates,spConfig.spEntityId,spConfig.callbackUri'; + + it('should be fulfilled given full parameters', () => { + const expectedPath = path + `?updateMask=${fullUpadateMask}`; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be fulfilled given partial parameters', () => { + const expectedPath = path + '?updateMask=enabled,idpConfig.ssoUrl,spConfig.spEntityId'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + ssoURL: 'https://example.com/login2', + rpEntityId: 'RP_ENTITY_ID2', + enabled: false, + }; + const partialRequest: SAMLConfigServerResponse = { + idpConfig: { + idpEntityId: undefined, + ssoUrl: 'https://example.com/login2', + signRequest: undefined, + idpCertificates: undefined, + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID2', + callbackUri: undefined, + }, + displayName: undefined, + enabled: false, + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); + + it('should be fulfilled given single parameter to change', () => { + const expectedPath = path + '?updateMask=idpConfig.ssoUrl'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + ssoURL: 'https://example.com/login2', + }; + const partialRequest: SAMLConfigServerResponse = { + idpConfig: { + idpEntityId: undefined, + ssoUrl: 'https://example.com/login2', + signRequest: undefined, + idpCertificates: undefined, + }, + displayName: undefined, + enabled: undefined, + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(invalidProviderId as any, configOptions) + .then((result) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + }); }); - }); + }); - const invalidProviderIds = [ - null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { - it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.', + ); + const invalidOptions: SAMLUpdateAuthProviderRequest = deepCopy(configOptions); + invalidOptions.ssoURL = 'invalid'; - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(invalidProviderId as any, configOptions) + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, invalidOptions) .then((result) => { throw new Error('Unexpected success'); }, (error) => { expect(error).to.deep.equal(expectedError); }); }); - }); - it('should be rejected given invalid parameters', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.', - ); - const invalidOptions: SAMLUpdateAuthProviderRequest = deepCopy(configOptions); - invalidOptions.ssoURL = 'invalid'; - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(providerId, invalidOptions) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); + it('should be rejected when the backend returns a response missing name', () => { + const expectedPath = path + `?updateMask=${fullUpadateMask}`; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); - it('should be rejected when the backend returns a response missing name', () => { - const expectedPath = path + `?updateMask=${fullUpadateMask}`; - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', - ); - const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(providerId, configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, expectedRequest)); + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedPath = path + `?updateMask=${fullUpadateMask}`; + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_CONFIG', + }, }); - }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); - it('should be rejected when the backend returns an error', () => { - const expectedPath = path + `?updateMask=${fullUpadateMask}`; - const expectedServerError = utils.errorFrom({ - error: { - message: 'INVALID_CONFIG', - }, + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, configOptions) + .then((resp) => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.equal(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateInboundSamlConfig(providerId, configOptions) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(expectedPath, expectedHttpMethod, expectedRequest)); - }); }); - }); - describe('non-2xx responses', () => { - it('should be rejected given a simulated non-2xx response with a known error code', () => { - const mockErrorResponse = utils.errorFrom({ - error: { - message: 'USER_NOT_FOUND', - }, - }, 400); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); - stubs.push(stub); + describe('non-2xx responses', () => { + it('should be rejected given a simulated non-2xx response with a known error code', () => { + const mockErrorResponse = utils.errorFrom({ + error: { + message: 'USER_NOT_FOUND', + }, + }, 400); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); - it('should be rejected given a simulated non-2xx response with an unknown error code', () => { - const mockErrorResponse = utils.errorFrom({ - error: { - message: 'UNKNOWN_ERROR_CODE', - }, - }, 400); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); - stubs.push(stub); + it('should be rejected given a simulated non-2xx response with an unknown error code', () => { + const mockErrorResponse = utils.errorFrom({ + error: { + message: 'UNKNOWN_ERROR_CODE', + }, + }, 400); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); - }); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); + }); - it('should be rejected given a simulated non-2xx response with no error code', () => { - const mockErrorResponse = utils.errorFrom({ - error: { - foo: 'bar', - }, - }, 400); - const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); - stubs.push(stub); + it('should be rejected given a simulated non-2xx response with no error code', () => { + const mockErrorResponse = utils.errorFrom({ + error: { + foo: 'bar', + }, + }, 400); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); + }); }); }); }); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 9f2ffbe070..3b344fb80b 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -29,7 +29,7 @@ import {Auth, DecodedIdToken} from '../../../src/auth/auth'; import {UserRecord} from '../../../src/auth/user-record'; import {FirebaseApp} from '../../../src/firebase-app'; import {FirebaseTokenGenerator} from '../../../src/auth/token-generator'; -import {FirebaseAuthRequestHandler} from '../../../src/auth/auth-api-request'; +import {AuthRequestHandler} from '../../../src/auth/auth-api-request'; import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; import * as validator from '../../../src/utils/validator'; @@ -790,7 +790,7 @@ describe('Auth', () => { it('should resolve with a UserRecord on success', () => { // Stub getAccountInfoByUid to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .resolves(expectedGetAccountInfoResult); stubs.push(stub); return auth.getUser(uid) @@ -804,7 +804,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub getAccountInfoByUid to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .rejects(expectedError); stubs.push(stub); return auth.getUser(uid) @@ -868,7 +868,7 @@ describe('Auth', () => { it('should resolve with a UserRecord on success', () => { // Stub getAccountInfoByEmail to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByEmail') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByEmail') .resolves(expectedGetAccountInfoResult); stubs.push(stub); return auth.getUserByEmail(email) @@ -882,7 +882,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub getAccountInfoByEmail to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByEmail') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByEmail') .rejects(expectedError); stubs.push(stub); return auth.getUserByEmail(email) @@ -947,7 +947,7 @@ describe('Auth', () => { it('should resolve with a UserRecord on success', () => { // Stub getAccountInfoByPhoneNumber to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByPhoneNumber') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByPhoneNumber') .resolves(expectedGetAccountInfoResult); stubs.push(stub); return auth.getUserByPhoneNumber(phoneNumber) @@ -961,7 +961,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub getAccountInfoByPhoneNumber to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByPhoneNumber') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByPhoneNumber') .rejects(expectedError); stubs.push(stub); return auth.getUserByPhoneNumber(phoneNumber) @@ -1024,7 +1024,7 @@ describe('Auth', () => { it('should resolve with void on success', () => { // Stub deleteAccount to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteAccount') + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteAccount') .resolves(expectedDeleteAccountResult); stubs.push(stub); return auth.deleteUser(uid) @@ -1038,7 +1038,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub deleteAccount to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteAccount') + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteAccount') .rejects(expectedError); stubs.push(stub); return auth.deleteUser(uid) @@ -1113,10 +1113,10 @@ describe('Auth', () => { it('should resolve with a UserRecord on createNewAccount request success', () => { // Stub createNewAccount to return expected uid. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') + const createUserStub = sinon.stub(AuthRequestHandler.prototype, 'createNewAccount') .resolves(uid); // Stub getAccountInfoByUid to return expected result. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const getUserStub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .resolves(expectedGetAccountInfoResult); stubs.push(createUserStub); stubs.push(getUserStub); @@ -1132,7 +1132,7 @@ describe('Auth', () => { it('should throw an error when createNewAccount returns an error', () => { // Stub createNewAccount to throw a backend error. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') + const createUserStub = sinon.stub(AuthRequestHandler.prototype, 'createNewAccount') .rejects(expectedError); stubs.push(createUserStub); return auth.createUser(propertiesToCreate) @@ -1148,11 +1148,11 @@ describe('Auth', () => { it('should throw an error when getUser returns a User not found error', () => { // Stub createNewAccount to return expected uid. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') + const createUserStub = sinon.stub(AuthRequestHandler.prototype, 'createNewAccount') .resolves(uid); // Stub getAccountInfoByUid to throw user not found error. const userNotFoundError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const getUserStub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .rejects(userNotFoundError); stubs.push(createUserStub); stubs.push(getUserStub); @@ -1170,10 +1170,10 @@ describe('Auth', () => { it('should echo getUser error if an error occurs while retrieving the user record', () => { // Stub createNewAccount to return expected uid. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') + const createUserStub = sinon.stub(AuthRequestHandler.prototype, 'createNewAccount') .resolves(uid); // Stub getAccountInfoByUid to throw expected error. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const getUserStub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .rejects(expectedError); stubs.push(createUserStub); stubs.push(getUserStub); @@ -1266,10 +1266,10 @@ describe('Auth', () => { it('should resolve with a UserRecord on updateExistingAccount request success', () => { // Stub updateExistingAccount to return expected uid. - const updateUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateExistingAccount') + const updateUserStub = sinon.stub(AuthRequestHandler.prototype, 'updateExistingAccount') .resolves(uid); // Stub getAccountInfoByUid to return expected result. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const getUserStub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .resolves(expectedGetAccountInfoResult); stubs.push(updateUserStub); stubs.push(getUserStub); @@ -1285,7 +1285,7 @@ describe('Auth', () => { it('should throw an error when updateExistingAccount returns an error', () => { // Stub updateExistingAccount to throw a backend error. - const updateUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateExistingAccount') + const updateUserStub = sinon.stub(AuthRequestHandler.prototype, 'updateExistingAccount') .rejects(expectedError); stubs.push(updateUserStub); return auth.updateUser(uid, propertiesToEdit) @@ -1301,10 +1301,10 @@ describe('Auth', () => { it('should echo getUser error if an error occurs while retrieving the user record', () => { // Stub updateExistingAccount to return expected uid. - const updateUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateExistingAccount') + const updateUserStub = sinon.stub(AuthRequestHandler.prototype, 'updateExistingAccount') .resolves(uid); // Stub getAccountInfoByUid to throw an expected error. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') + const getUserStub = sinon.stub(AuthRequestHandler.prototype, 'getAccountInfoByUid') .rejects(expectedError); stubs.push(updateUserStub); stubs.push(getUserStub); @@ -1392,7 +1392,7 @@ describe('Auth', () => { it('should resolve on setCustomUserClaims request success', () => { // Stub setCustomUserClaims to return expected uid. const setCustomUserClaimsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'setCustomUserClaims') + .stub(AuthRequestHandler.prototype, 'setCustomUserClaims') .resolves(uid); stubs.push(setCustomUserClaimsStub); return auth.setCustomUserClaims(uid, customClaims) @@ -1407,7 +1407,7 @@ describe('Auth', () => { it('should throw an error when setCustomUserClaims returns an error', () => { // Stub setCustomUserClaims to throw a backend error. const setCustomUserClaimsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'setCustomUserClaims') + .stub(AuthRequestHandler.prototype, 'setCustomUserClaims') .rejects(expectedError); stubs.push(setCustomUserClaimsStub); return auth.setCustomUserClaims(uid, customClaims) @@ -1506,7 +1506,7 @@ describe('Auth', () => { it('should resolve on downloadAccount request success with users in response', () => { // Stub downloadAccount to return expected response. const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') + .stub(AuthRequestHandler.prototype, 'downloadAccount') .resolves(downloadAccountResponse); stubs.push(downloadAccountStub); return auth.listUsers(maxResult, pageToken) @@ -1521,7 +1521,7 @@ describe('Auth', () => { it('should resolve on downloadAccount request success with default options', () => { // Stub downloadAccount to return expected response. const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') + .stub(AuthRequestHandler.prototype, 'downloadAccount') .resolves(downloadAccountResponse); stubs.push(downloadAccountStub); return auth.listUsers() @@ -1537,7 +1537,7 @@ describe('Auth', () => { it('should resolve on downloadAccount request success with no users in response', () => { // Stub downloadAccount to return expected response. const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') + .stub(AuthRequestHandler.prototype, 'downloadAccount') .resolves(emptyDownloadAccountResponse); stubs.push(downloadAccountStub); return auth.listUsers(maxResult, pageToken) @@ -1552,7 +1552,7 @@ describe('Auth', () => { it('should throw an error when downloadAccount returns an error', () => { // Stub downloadAccount to throw a backend error. const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') + .stub(AuthRequestHandler.prototype, 'downloadAccount') .rejects(expectedError); stubs.push(downloadAccountStub); return auth.listUsers(maxResult, pageToken) @@ -1617,7 +1617,7 @@ describe('Auth', () => { it('should resolve on underlying revokeRefreshTokens request success', () => { // Stub revokeRefreshTokens to return expected uid. const revokeRefreshTokensStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'revokeRefreshTokens') + sinon.stub(AuthRequestHandler.prototype, 'revokeRefreshTokens') .resolves(uid); stubs.push(revokeRefreshTokensStub); return auth.revokeRefreshTokens(uid) @@ -1632,7 +1632,7 @@ describe('Auth', () => { it('should throw when underlying revokeRefreshTokens request returns an error', () => { // Stub revokeRefreshTokens to throw a backend error. const revokeRefreshTokensStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'revokeRefreshTokens') + sinon.stub(AuthRequestHandler.prototype, 'revokeRefreshTokens') .rejects(expectedError); stubs.push(revokeRefreshTokensStub); return auth.revokeRefreshTokens(uid) @@ -1698,7 +1698,7 @@ describe('Auth', () => { it('should resolve on underlying uploadAccount request resolution', () => { // Stub uploadAccount to return expected result. const uploadAccountStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'uploadAccount') + sinon.stub(AuthRequestHandler.prototype, 'uploadAccount') .resolves(expectedUserImportResult); stubs.push(uploadAccountStub); return auth.importUsers(users, options) @@ -1713,7 +1713,7 @@ describe('Auth', () => { it('should reject when underlying uploadAccount request rejects with an error', () => { // Stub uploadAccount to reject with expected error. const uploadAccountStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'uploadAccount') + sinon.stub(AuthRequestHandler.prototype, 'uploadAccount') .rejects(expectedServerError); stubs.push(uploadAccountStub); return auth.importUsers(users, options) @@ -1730,7 +1730,7 @@ describe('Auth', () => { it('should throw and fail quickly when underlying uploadAccount throws', () => { // Stub uploadAccount to throw with expected error. const uploadAccountStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'uploadAccount') + sinon.stub(AuthRequestHandler.prototype, 'uploadAccount') .throws(expectedOptionsError); stubs.push(uploadAccountStub); expect(() => { @@ -1812,7 +1812,7 @@ describe('Auth', () => { it('should resolve on underlying createSessionCookie request success', () => { // Stub createSessionCookie to return expected sessionCookie. const createSessionCookieStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'createSessionCookie') + sinon.stub(AuthRequestHandler.prototype, 'createSessionCookie') .resolves(sessionCookie); stubs.push(createSessionCookieStub); return auth.createSessionCookie(idToken, options) @@ -1828,7 +1828,7 @@ describe('Auth', () => { it('should throw when underlying createSessionCookie request returns an error', () => { // Stub createSessionCookie to throw a backend error. const createSessionCookieStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'createSessionCookie') + sinon.stub(AuthRequestHandler.prototype, 'createSessionCookie') .rejects(expectedError); stubs.push(createSessionCookieStub); return auth.createSessionCookie(idToken, options) @@ -1908,7 +1908,7 @@ describe('Auth', () => { it('should resolve when called with actionCodeSettings with a generated link on success', () => { // Stub getEmailActionLink to return expected link. - const getEmailActionLinkStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getEmailActionLink') + const getEmailActionLinkStub = sinon.stub(AuthRequestHandler.prototype, 'getEmailActionLink') .resolves(expectedLink); stubs.push(getEmailActionLinkStub); return (auth as any)[emailActionFlow.api](email, actionCodeSettings) @@ -1929,7 +1929,7 @@ describe('Auth', () => { } else { it('should resolve when called without actionCodeSettings with a generated link on success', () => { // Stub getEmailActionLink to return expected link. - const getEmailActionLinkStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getEmailActionLink') + const getEmailActionLinkStub = sinon.stub(AuthRequestHandler.prototype, 'getEmailActionLink') .resolves(expectedLink); stubs.push(getEmailActionLinkStub); return (auth as any)[emailActionFlow.api](email) @@ -1945,7 +1945,7 @@ describe('Auth', () => { it('should throw an error when getEmailAction returns an error', () => { // Stub getEmailActionLink to throw a backend error. - const getEmailActionLinkStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getEmailActionLink') + const getEmailActionLinkStub = sinon.stub(AuthRequestHandler.prototype, 'getEmailActionLink') .rejects(expectedError); stubs.push(getEmailActionLinkStub); return (auth as any)[emailActionFlow.api](email, actionCodeSettings) @@ -2021,7 +2021,7 @@ describe('Auth', () => { it('should resolve with an OIDCConfig on success', () => { // Stub getOAuthIdpConfig to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getOAuthIdpConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getOAuthIdpConfig') .resolves(serverResponse); stubs.push(stub); return (auth as Auth).getProviderConfig(providerId) @@ -2035,7 +2035,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub getOAuthIdpConfig to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getOAuthIdpConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getOAuthIdpConfig') .rejects(expectedError); stubs.push(stub); return (auth as Auth).getProviderConfig(providerId) @@ -2075,7 +2075,7 @@ describe('Auth', () => { it('should resolve with a SAMLConfig on success', () => { // Stub getInboundSamlConfig to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getInboundSamlConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getInboundSamlConfig') .resolves(serverResponse); stubs.push(stub); return (auth as Auth).getProviderConfig(providerId) @@ -2089,7 +2089,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub getInboundSamlConfig to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getInboundSamlConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getInboundSamlConfig') .rejects(expectedError); stubs.push(stub); return (auth as Auth).getProviderConfig(providerId) @@ -2177,7 +2177,7 @@ describe('Auth', () => { it('should resolve on success with configs in response', () => { // Stub listOAuthIdpConfigs to return expected response. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listOAuthIdpConfigs') + .stub(AuthRequestHandler.prototype, 'listOAuthIdpConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); return auth.listProviderConfigs(filterOptions) @@ -2192,7 +2192,7 @@ describe('Auth', () => { it('should resolve on success with default options', () => { // Stub listOAuthIdpConfigs to return expected response. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listOAuthIdpConfigs') + .stub(AuthRequestHandler.prototype, 'listOAuthIdpConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); return (auth as Auth).listProviderConfigs({type: 'oidc'}) @@ -2208,7 +2208,7 @@ describe('Auth', () => { it('should resolve on success with no configs in response', () => { // Stub listOAuthIdpConfigs to return expected response. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listOAuthIdpConfigs') + .stub(AuthRequestHandler.prototype, 'listOAuthIdpConfigs') .resolves(emptyListConfigsResponse); stubs.push(listConfigsStub); return auth.listProviderConfigs(filterOptions) @@ -2223,7 +2223,7 @@ describe('Auth', () => { it('should throw an error when listOAuthIdpConfigs returns an error', () => { // Stub listOAuthIdpConfigs to throw a backend error. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listOAuthIdpConfigs') + .stub(AuthRequestHandler.prototype, 'listOAuthIdpConfigs') .rejects(expectedError); stubs.push(listConfigsStub); return auth.listProviderConfigs(filterOptions) @@ -2272,7 +2272,7 @@ describe('Auth', () => { it('should resolve on success with configs in response', () => { // Stub listInboundSamlConfigs to return expected response. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listInboundSamlConfigs') + .stub(AuthRequestHandler.prototype, 'listInboundSamlConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); return auth.listProviderConfigs(filterOptions) @@ -2287,7 +2287,7 @@ describe('Auth', () => { it('should resolve on success with default options', () => { // Stub listInboundSamlConfigs to return expected response. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listInboundSamlConfigs') + .stub(AuthRequestHandler.prototype, 'listInboundSamlConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); return (auth as Auth).listProviderConfigs({type: 'saml'}) @@ -2303,7 +2303,7 @@ describe('Auth', () => { it('should resolve on success with no configs in response', () => { // Stub listInboundSamlConfigs to return expected response. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listInboundSamlConfigs') + .stub(AuthRequestHandler.prototype, 'listInboundSamlConfigs') .resolves(emptyListConfigsResponse); stubs.push(listConfigsStub); return auth.listProviderConfigs(filterOptions) @@ -2318,7 +2318,7 @@ describe('Auth', () => { it('should throw an error when listInboundSamlConfigs returns an error', () => { // Stub listInboundSamlConfigs to throw a backend error. const listConfigsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'listInboundSamlConfigs') + .stub(AuthRequestHandler.prototype, 'listInboundSamlConfigs') .rejects(expectedError); stubs.push(listConfigsStub); return auth.listProviderConfigs(filterOptions) @@ -2383,7 +2383,7 @@ describe('Auth', () => { it('should resolve with void on success', () => { // Stub deleteOAuthIdpConfig to resolve. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteOAuthIdpConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteOAuthIdpConfig') .resolves(); stubs.push(stub); return (auth as Auth).deleteProviderConfig(providerId) @@ -2397,7 +2397,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub deleteOAuthIdpConfig to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteOAuthIdpConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteOAuthIdpConfig') .rejects(expectedError); stubs.push(stub); return (auth as Auth).deleteProviderConfig(providerId) @@ -2419,7 +2419,7 @@ describe('Auth', () => { it('should resolve with void on success', () => { // Stub deleteInboundSamlConfig to resolve. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteInboundSamlConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteInboundSamlConfig') .resolves(); stubs.push(stub); return (auth as Auth).deleteProviderConfig(providerId) @@ -2433,7 +2433,7 @@ describe('Auth', () => { it('should throw an error when the backend returns an error', () => { // Stub deleteInboundSamlConfig to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteInboundSamlConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteInboundSamlConfig') .rejects(expectedError); stubs.push(stub); return (auth as Auth).deleteProviderConfig(providerId) @@ -2528,7 +2528,7 @@ describe('Auth', () => { it('should resolve with an OIDCConfig on updateOAuthIdpConfig request success', () => { // Stub updateOAuthIdpConfig to return expected server response. - const updateConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateOAuthIdpConfig') + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateOAuthIdpConfig') .resolves(serverResponse); stubs.push(updateConfigStub); @@ -2543,7 +2543,7 @@ describe('Auth', () => { it('should throw an error when updateOAuthIdpConfig returns an error', () => { // Stub updateOAuthIdpConfig to throw a backend error. - const updateConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateOAuthIdpConfig') + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateOAuthIdpConfig') .rejects(expectedError); stubs.push(updateConfigStub); @@ -2594,7 +2594,7 @@ describe('Auth', () => { it('should resolve with a SAMLConfig on updateInboundSamlConfig request success', () => { // Stub updateInboundSamlConfig to return expected server response. - const updateConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateInboundSamlConfig') + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateInboundSamlConfig') .resolves(serverResponse); stubs.push(updateConfigStub); @@ -2609,7 +2609,7 @@ describe('Auth', () => { it('should throw an error when updateInboundSamlConfig returns an error', () => { // Stub updateInboundSamlConfig to throw a backend error. - const updateConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateInboundSamlConfig') + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateInboundSamlConfig') .rejects(expectedError); stubs.push(updateConfigStub); @@ -2694,7 +2694,7 @@ describe('Auth', () => { it('should resolve with an OIDCConfig on createOAuthIdpConfig request success', () => { // Stub createOAuthIdpConfig to return expected server response. - const createConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createOAuthIdpConfig') + const createConfigStub = sinon.stub(AuthRequestHandler.prototype, 'createOAuthIdpConfig') .resolves(serverResponse); stubs.push(createConfigStub); @@ -2709,7 +2709,7 @@ describe('Auth', () => { it('should throw an error when createOAuthIdpConfig returns an error', () => { // Stub createOAuthIdpConfig to throw a backend error. - const createConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createOAuthIdpConfig') + const createConfigStub = sinon.stub(AuthRequestHandler.prototype, 'createOAuthIdpConfig') .rejects(expectedError); stubs.push(createConfigStub); @@ -2761,7 +2761,7 @@ describe('Auth', () => { it('should resolve with a SAMLConfig on createInboundSamlConfig request success', () => { // Stub createInboundSamlConfig to return expected server response. - const createConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createInboundSamlConfig') + const createConfigStub = sinon.stub(AuthRequestHandler.prototype, 'createInboundSamlConfig') .resolves(serverResponse); stubs.push(createConfigStub); @@ -2776,7 +2776,7 @@ describe('Auth', () => { it('should throw an error when createInboundSamlConfig returns an error', () => { // Stub createInboundSamlConfig to throw a backend error. - const createConfigStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createInboundSamlConfig') + const createConfigStub = sinon.stub(AuthRequestHandler.prototype, 'createInboundSamlConfig') .rejects(expectedError); stubs.push(createConfigStub);