From a21b011579c77577ae024f3f7970e64875357116 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 7 May 2019 18:47:13 -0700 Subject: [PATCH 1/4] Starts defining multi-tenancy APIs. This includes: - Defining type definitions. - Adding tenantId to UserRecord and UserImportBuilder. - Adding new errors associated with tenant operations. - Defines the Tenant object. As the changes are quite large. This will be split into multiple PRs. --- src/auth/auth-config.ts | 109 +++++++++ src/auth/tenant.ts | 199 ++++++++++++++++ src/auth/user-import-builder.ts | 3 + src/auth/user-record.ts | 3 + src/index.d.ts | 44 ++++ src/utils/error.ts | 18 ++ test/unit/auth/auth-config.spec.ts | 143 +++++++++++ test/unit/auth/tenant.spec.ts | 263 +++++++++++++++++++++ test/unit/auth/user-import-builder.spec.ts | 2 + test/unit/auth/user-record.spec.ts | 37 ++- test/unit/index.spec.ts | 1 + 11 files changed, 819 insertions(+), 3 deletions(-) create mode 100644 src/auth/tenant.ts create mode 100644 test/unit/auth/tenant.spec.ts diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index b115f17102..57969e5b0d 100755 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -149,6 +149,115 @@ export interface OIDCAuthProviderRequest extends OIDCUpdateAuthProviderRequest { /** The public API request interface for updating a generic Auth provider. */ export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; +/** The email provider configuration interface. */ +export interface EmailSignInProviderConfig { + enabled?: boolean; + passwordRequired?: boolean; // In the backend API, default is true if not provided +} + +/** The server side Email configuration request interface. */ +export interface EmailSignInConfigServerRequest { + allowPasswordSignup?: boolean; + enableEmailLinkSignin?: boolean; +} + + +/** + * Defines the Email sign-in config class used to convert client side EmailSignInConfig + * to a format that is understand by the Auth server. + */ +export class EmailSignInConfig implements EmailSignInProviderConfig { + public readonly enabled?: boolean; + public readonly passwordRequired?: boolean; + + /** + * Static method convert a client side request to a EmailSignInConfigServerRequest. + * Throws an error if validation fails. + * + * @param {any} options The options object to convert to a server request. + * @return {EmailSignInConfigServerRequest} The resulting server request. + */ + public static buildServerRequest(options: any): EmailSignInConfigServerRequest { + const request: EmailSignInConfigServerRequest = {}; + EmailSignInConfig.validate(options); + if (typeof options !== 'undefined' && options.hasOwnProperty('enabled')) { + request.allowPasswordSignup = options.enabled; + } + if (typeof options !== 'undefined' && options.hasOwnProperty('passwordRequired')) { + request.enableEmailLinkSignin = !options.passwordRequired; + } + return request; + } + + /** + * Validates the EmailSignInConfig options object. Throws an error on failure. + * + * @param {any} options The options object to validate. + */ + public static validate(options: any) { + // TODO: Validate the request. + const validKeys = { + enabled: true, + passwordRequired: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig" must be a non-null object.', + ); + } + // Check for unsupported top level attributes. + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid EmailSignInConfig parameter.`, + ); + } + } + // Validate content. + if (typeof options.enabled !== 'undefined' && + !validator.isBoolean(options.enabled)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig.enabled" must be a boolean.', + ); + } + if (typeof options.passwordRequired !== 'undefined' && + !validator.isBoolean(options.passwordRequired)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig.passwordRequired" must be a boolean.', + ); + } + } + + /** + * The EmailSignInConfig constructor. + * + * @param {any} response The server side response used to initialize the + * EmailSignInConfig object. + * @constructor + */ + constructor(response: any) { + if (typeof response.allowPasswordSignup === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response'); + } + utils.addReadonlyGetter(this, 'enabled', response.allowPasswordSignup); + utils.addReadonlyGetter(this, 'passwordRequired', !response.enableEmailLinkSignin); + } + + /** @return {object} The plain object representation of the email sign-in config. */ + public toJSON(): object { + return { + enabled: this.enabled, + passwordRequired: this.passwordRequired, + }; + } +} + /** * Defines the SAMLConfig class used to convert a client side configuration to its diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts new file mode 100644 index 0000000000..c9984173c0 --- /dev/null +++ b/src/auth/tenant.ts @@ -0,0 +1,199 @@ +/*! + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as utils from '../utils'; +import * as validator from '../utils/validator'; +import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { + EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig, +} from './auth-config'; + +/** The server side tenant type enum. */ +export type TenantServerType = 'LIGHTWEIGHT' | 'FULL_SERVICE' | 'TYPE_UNSPECIFIED'; + +/** The client side tenant type enum. */ +export type TenantType = 'lightweight' | 'full_service' | 'type_unspecified'; + +/** The TenantOptions interface used for create/read/update tenant operations. */ +export interface TenantOptions { + displayName?: string; + type?: TenantType; + emailSignInConfig?: EmailSignInProviderConfig; +} + +/** The corresponding server side representation of a TenantOptions object. */ +export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest { + displayName?: string; + type?: TenantServerType; +} + +/** The tenant server response interface. */ +export interface TenantServerResponse { + name: string; + type?: TenantServerType; + displayName?: string; + allowPasswordSignup: boolean; + enableEmailLinkSignin: boolean; +} + +/** The interface representing the listTenant API response. */ +export interface ListTenantsResult { + tenants: Tenant[]; + pageToken?: string; +} + + +/** + * Tenant class that defines a Firebase Auth tenant. + */ +export class Tenant { + public readonly tenantId: string; + public readonly type?: TenantType; + public readonly displayName?: string; + public readonly emailSignInConfig?: EmailSignInConfig | null; + + /** + * Builds the corresponding server request for a TenantOptions object. + * + * @param {TenantOptions} tenantOptions The properties to convert to a server request. + * @param {boolean} createRequest Whether this is a create request. + * @return {object} The equivalent server request. + */ + public static buildServerRequest( + tenantOptions: TenantOptions, createRequest: boolean): TenantOptionsServerRequest { + Tenant.validate(tenantOptions, createRequest); + let request: TenantOptionsServerRequest = {}; + if (typeof tenantOptions.emailSignInConfig !== 'undefined') { + request = EmailSignInConfig.buildServerRequest(tenantOptions.emailSignInConfig); + } + if (typeof tenantOptions.displayName !== 'undefined') { + request.displayName = tenantOptions.displayName; + } + if (typeof tenantOptions.type !== 'undefined') { + request.type = tenantOptions.type.toUpperCase() as TenantServerType; + } + return request; + } + + /** + * Returns the tenant ID corresponding to the resource name if available. + * + * @param {string} resourceName The server side resource name + * @return {?string} The tenant ID corresponding to the resource, null otherwise. + */ + public static getTenantIdFromResourceName(resourceName: string): string | null { + // name is of form projects/project1/tenants/tenant1 + const matchTenantRes = resourceName.match(/\/tenants\/(.*)$/); + if (!matchTenantRes || matchTenantRes.length < 2) { + return null; + } + return matchTenantRes[1]; + } + + /** + * Validates a tenant options object. Throws an error on failure. + * + * @param {any} request The tenant options object to validate. + * @param {boolean} createRequest Whether this is a create request. + */ + public static validate(request: any, createRequest: boolean) { + const validKeys = { + displayName: true, + type: true, + emailSignInConfig: true, + }; + const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; + if (!validator.isNonNullObject(request)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}" must be a valid non-null object.`, + ); + } + // Check for unsupported top level attributes. + for (const key in request) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid ${label} parameter.`, + ); + } + } + // Validate displayName type if provided. + if (typeof request.displayName !== 'undefined' && + !validator.isNonEmptyString(request.displayName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}.displayName" must be a valid non-empty string.`, + ); + } + // Validate type type if provided. + if (typeof request.type !== 'undefined' && !createRequest) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"Tenant.type" is an immutable property.', + ); + } + if (createRequest && + request.type !== 'full_service' && + request.type !== 'lightweight') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}.type" must be either "full_service" or "lightweight".`, + ); + } + // Validate emailSignInConfig type if provided. + if (typeof request.emailSignInConfig !== 'undefined') { + EmailSignInConfig.validate(request.emailSignInConfig); + } + } + + /** + * The Tenant object constructor. + * + * @param {any} response The server side response used to initialize the Tenant object. + * @constructor + */ + constructor(response: any) { + const tenantId = Tenant.getTenantIdFromResourceName(response.name); + if (!tenantId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid tenant response', + ); + } + utils.addReadonlyGetter(this, 'tenantId', tenantId); + utils.addReadonlyGetter(this, 'displayName', response.displayName); + utils.addReadonlyGetter( + this, 'type', (response.type && response.type.toLowerCase()) || undefined); + try { + utils.addReadonlyGetter(this, 'emailSignInConfig', new EmailSignInConfig(response)); + } catch (e) { + utils.addReadonlyGetter(this, 'emailSignInConfig', undefined); + } + } + + /** @return {object} The plain object representation of the tenant. */ + public toJSON(): object { + const json: any = { + tenantId: this.tenantId, + displayName: this.displayName, + type: this.type, + emailSignInConfig: this.emailSignInConfig && this.emailSignInConfig.toJSON(), + }; + return json; + } +} + diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts index 7b5f30c99c..a661023321 100644 --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -63,6 +63,7 @@ export interface UserImportRecord { customClaims?: object; passwordHash?: Buffer; passwordSalt?: Buffer; + tenantId?: string; } @@ -87,6 +88,7 @@ interface UploadAccountUser { lastLoginAt?: number; createdAt?: number; customAttributes?: string; + tenantId?: string; } @@ -153,6 +155,7 @@ function populateUploadAccountUser( photoUrl: user.photoURL, phoneNumber: user.phoneNumber, providerUserInfo: [], + tenantId: user.tenantId, customAttributes: user.customClaims && JSON.stringify(user.customClaims), }; if (typeof user.passwordHash !== 'undefined') { diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 4fb52502c4..2b908e9f0d 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -148,6 +148,7 @@ export class UserRecord { public readonly passwordHash?: string; public readonly passwordSalt?: string; public readonly customClaims: object; + public readonly tenantId?: string | null; public readonly tokensValidAfterTime?: string; constructor(response: any) { @@ -187,6 +188,7 @@ export class UserRecord { validAfterTime = parseDate(response.validSince * 1000); } utils.addReadonlyGetter(this, 'tokensValidAfterTime', validAfterTime || undefined); + utils.addReadonlyGetter(this, 'tenantId', response.tenantId); } /** @return {object} The plain object representation of the user record. */ @@ -205,6 +207,7 @@ export class UserRecord { passwordSalt: this.passwordSalt, customClaims: deepCopy(this.customClaims), tokensValidAfterTime: this.tokensValidAfterTime, + tenantId: this.tenantId, }; json.providerData = []; for (const entry of this.providerData) { diff --git a/src/index.d.ts b/src/index.d.ts index f02bc45c03..443ad82009 100755 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -116,6 +116,7 @@ declare namespace admin.auth { passwordSalt?: string; customClaims?: Object; tokensValidAfterTime?: string; + tenantId?: string | null; toJSON(): Object; } @@ -143,6 +144,7 @@ declare namespace admin.auth { [key: string]: any; }; sign_in_provider: string; + tenant?: string; [key: string]: any; }; iat: number; @@ -203,6 +205,7 @@ declare namespace admin.auth { customClaims?: Object; passwordHash?: Buffer; passwordSalt?: Buffer; + tenantId?: string | null; } interface SessionCookieOptions { @@ -223,6 +226,36 @@ declare namespace admin.auth { dynamicLinkDomain?: string; } + type TenantType = 'lightweight' | 'full_service'; + + interface Tenant { + tenantId: string; + type?: admin.auth.TenantType; + displayName?: string; + emailSignInConfig?: { + enabled: boolean; + passwordRequired?: boolean + }; + toJSON(): Object; + } + + interface UpdateTenantRequest { + displayName: string; + emailSignInConfig?: { + enabled: boolean; + passwordRequired?: boolean; + }; + } + + interface CreateTenantRequest extends UpdateTenantRequest { + type: admin.auth.TenantType; + } + + interface ListTenantsResult { + tenants: admin.auth.Tenant[]; + pageToken?: string; + } + interface AuthProviderConfigFilter { type: 'saml' | 'oidc'; maxResults?: number; @@ -324,8 +357,19 @@ declare namespace admin.auth { ): Promise; } + interface TenantAwareAuth extends BaseAuth { + tenantId: string; + } + interface Auth extends admin.auth.BaseAuth { app: admin.app.App; + + forTenant(tenantId: string): admin.auth.TenantAwareAuth; + getTenant(tenantId: string): Promise; + listTenants(maxResults?: number, pageToken?: string): Promise; + deleteTenant(tenantId: string): Promise; + createTenant(tenantOptions: admin.auth.CreateTenantRequest): Promise; + updateTenant(tenantId: string, tenantOptions: admin.auth.UpdateTenantRequest): Promise; } } diff --git a/src/utils/error.ts b/src/utils/error.ts index 2f3fc2d3fe..4f5aef823d 100755 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -499,6 +499,10 @@ export class AuthClientErrorCode { message: 'The session cookie duration must be a valid number in milliseconds ' + 'between 5 minutes and 2 weeks.', }; + public static INVALID_TENANT_ID = { + code: 'invalid-tenant-id', + message: 'The tenant ID must be a valid non-empty string.', + }; public static INVALID_UID = { code: 'invalid-uid', message: 'The uid must be a non-empty string with at most 128 characters.', @@ -511,6 +515,10 @@ export class AuthClientErrorCode { code: 'invalid-tokens-valid-after-time', message: 'The tokensValidAfterTime must be a valid UTC number in seconds.', }; + public static MISMATCHING_TENANT_ID = { + code: 'mismatching-tenant-id', + message: 'User tenant ID does not match with the current TenantAwareAuth tenant ID.', + }; public static MISSING_ANDROID_PACKAGE_NAME = { code: 'missing-android-pkg-name', message: 'An Android Package Name must be provided if the Android App is ' + @@ -586,6 +594,10 @@ export class AuthClientErrorCode { code: 'session-cookie-revoked', message: 'The Firebase session cookie has been revoked.', }; + public static TENANT_NOT_FOUND = { + code: 'tenant-not-found', + message: 'There is no tenant corresponding to the provided identifier.', + }; public static UID_ALREADY_EXISTS = { code: 'uid-already-exists', message: 'The user with the provided uid already exists.', @@ -763,6 +775,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', // Invalid email provided. INVALID_EMAIL: 'INVALID_EMAIL', + // Invalid tenant display name: CreateTenant and UpdateTenant. + INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', // Invalid ID token provided. INVALID_ID_TOKEN: 'INVALID_ID_TOKEN', // OIDC configuration has an invalid OAuth client ID. @@ -781,6 +795,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { MISSING_CONFIG: 'MISSING_CONFIG', // Missing configuration identifier. MISSING_CONFIG_ID: 'MISSING_PROVIDER_ID', + // Missing tenant display name: CreateTenant and UpdateTenant. + MISSING_DISPLAY_NAME: 'MISSING_DISPLAY_NAME', // Missing iOS bundle ID. MISSING_IOS_BUNDLE_ID: 'MISSING_IOS_BUNDLE_ID', // Missing OIDC issuer. @@ -803,6 +819,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_ALREADY_EXISTS', // Project not found. PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND', + // Tenant not found. + TENANT_NOT_FOUND: 'TENANT_NOT_FOUND', // 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-config.spec.ts b/test/unit/auth/auth-config.spec.ts index 3ed9a3291b..7d9c10af05 100755 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -25,6 +25,7 @@ import { SAMLConfigServerResponse, OIDCConfigServerRequest, OIDCConfigServerResponse, SAMLUpdateAuthProviderRequest, OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, OIDCAuthProviderConfig, + EmailSignInConfig, } from '../../../src/auth/auth-config'; @@ -34,6 +35,148 @@ chai.use(chaiAsPromised); const expect = chai.expect; +describe('EmailSignInConfig', () => { + describe('constructor', () => { + const validConfig = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + + it('should throw on missing allowPasswordSignup', () => { + expect(() => new EmailSignInConfig({ + enableEmailLinkSignin: false, + })).to.throw('INTERNAL ASSERT FAILED: Invalid email sign-in configuration response'); + }); + + it('should set readonly property "enabled" to true on allowPasswordSignup enabled', () => { + expect(validConfig.enabled).to.be.true; + }); + + it('should set readonly property "enabled" to false on allowPasswordSignup disabled', () => { + const validConfig2 = new EmailSignInConfig({ + allowPasswordSignup: false, + enableEmailLinkSignin: false, + }); + expect(validConfig2.enabled).to.be.false; + }); + + it('should set readonly property "passwordRequired" to false on email link sign in enabled', () => { + expect(validConfig.passwordRequired).to.be.false; + }); + + it('should set readonly property "passwordRequired" to true on email link sign in disabled', () => { + const validConfig2 = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: false, + }); + expect(validConfig2.passwordRequired).to.be.true; + }); + + it('should not throw on valid response', () => { + expect(() => new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + })).not.to.throw(); + }); + }); + + describe('toJSON()', () => { + it('should return expected JSON representation', () => { + const config = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + expect(config.toJSON()).to.deep.equal({ + enabled: true, + passwordRequired: false, + }); + }); + }); + + describe('buildServerRequest()', () => { + it('should return expected server request on valid input with email link sign-in', () => { + expect(EmailSignInConfig.buildServerRequest({ + enabled: true, + passwordRequired: false, + })).to.deep.equal({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + }); + + it('should return expected server request on valid input without email link sign-in', () => { + expect(EmailSignInConfig.buildServerRequest({ + enabled: true, + passwordRequired: true, + })).to.deep.equal({ + allowPasswordSignup: true, + enableEmailLinkSignin: false, + }); + }); + + it('should throw on invalid input', () => { + expect(() => EmailSignInConfig.buildServerRequest({ + enabled: 'invalid', + passwordRequired: true, + })).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); + }); + }); + + describe('validate()', () => { + const invalidOptions = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidOptions.forEach((options) => { + it('should throw on invalid EmailSignInConfig:' + JSON.stringify(options), () => { + expect(() => { + EmailSignInConfig.validate(options as any); + }).to.throw('"EmailSignInConfig" must be a non-null object.'); + }); + }); + + it('should throw on EmailSignInConfig with unsupported attribute', () => { + expect(() => { + EmailSignInConfig.validate({ + unsupported: true, + enabled: true, + passwordRequired: false, + } as any); + }).to.throw('"unsupported" is not a valid EmailSignInConfig parameter.'); + }); + + const invalidEnabled = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidEnabled.forEach((enabled) => { + it('should throw on invalid EmailSignInConfig.enabled:' + JSON.stringify(enabled), () => { + expect(() => { + EmailSignInConfig.validate({ + enabled, + passwordRequired: false, + } as any); + }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); + }); + }); + + const invalidPasswordRequired = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidEnabled.forEach((passwordRequired) => { + it('should throw on invalid EmailSignInConfig.passwordRequired:' + JSON.stringify(passwordRequired), () => { + expect(() => { + EmailSignInConfig.validate({ + enabled: true, + passwordRequired, + } as any); + }).to.throw('"EmailSignInConfig.passwordRequired" must be a boolean.'); + }); + }); + }); + + it('should not throw on valid EmailSignInConfig', () => { + expect(() => { + EmailSignInConfig.validate({ + enabled: true, + passwordRequired: false, + } as any); + }).not.to.throw; + }); +}); + describe('SAMLConfig', () => { const serverRequest: SAMLConfigServerRequest = { idpConfig: { diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts new file mode 100644 index 0000000000..3a14367962 --- /dev/null +++ b/test/unit/auth/tenant.spec.ts @@ -0,0 +1,263 @@ +/*! + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import {deepCopy} from '../../../src/utils/deep-copy'; +import {EmailSignInConfig} from '../../../src/auth/auth-config'; +import { + Tenant, TenantOptions, TenantServerResponse, +} from '../../../src/auth/tenant'; + + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Tenant', () => { + const serverRequest = { + name: 'projects/project1/tenants/TENANT_ID', + displayName: 'TENANT_DISPLAY_NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }; + + const clientRequest = { + displayName: 'TENANT_DISPLAY_NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + }; + + const tenantOptions: TenantOptions = { + displayName: 'TENANT_DISPLAY_NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + }; + + describe('buildServerRequest()', () => { + describe('for update request', () => { + it('should return the expected server request', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest = deepCopy(serverRequest); + delete tenantOptionsServerRequest.name; + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should throw on invalid input', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + tenantOptionsClientRequest.displayName = null; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + .to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); + }); + + it('should throw on invalid EmailSignInConfig', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + tenantOptionsClientRequest.emailSignInConfig = null; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + .to.throw('"EmailSignInConfig" must be a non-null object.'); + }); + + it('should throw when type is specified', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); + tenantOptionsClientRequest.type = 'lightweight'; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + .to.throw('"Tenant.type" is an immutable property.'); + }); + }); + + describe('for create request', () => { + it('should return the expected server request', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + tenantOptionsClientRequest.type = 'lightweight'; + const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequest); + delete tenantOptionsServerRequest.name; + tenantOptionsServerRequest.type = 'LIGHTWEIGHT'; + + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should throw on invalid input', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + tenantOptionsClientRequest.displayName = null; + tenantOptionsClientRequest.type = 'full_service'; + + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + .to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); + }); + + it('should throw on invalid EmailSignInConfig', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + tenantOptionsClientRequest.emailSignInConfig = null; + tenantOptionsClientRequest.type = 'full_service'; + + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + .to.throw('"EmailSignInConfig" must be a non-null object.'); + }); + + const invalidTypes = ['invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTypes.forEach((invalidType) => { + it('should throw on invalid type ' + JSON.stringify(invalidType), () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); + tenantOptionsClientRequest.type = invalidType as any; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + .to.throw(`"CreateTenantRequest.type" must be either "full_service" or "lightweight".`); + }); + }); + }); + }); + + describe('getTenantIdFromResourceName()', () => { + it('should return the expected tenant ID from resource name', () => { + expect(Tenant.getTenantIdFromResourceName('projects/project1/tenants/TENANT_ID')) + .to.equal('TENANT_ID'); + }); + + it('should return the expected tenant ID from resource name with multiple tenants substring', () => { + expect(Tenant.getTenantIdFromResourceName('projects/projecttenants/tenants/TENANT_ID')) + .to.equal('TENANT_ID'); + }); + + it('should return null when no tenant ID is found', () => { + expect(Tenant.getTenantIdFromResourceName('projects/project1')).to.be.null; + }); + }); + + describe('validate()', () => { + it('should not throw on valid client request object', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + expect(() => { + Tenant.validate(tenantOptionsClientRequest, false); + }).not.to.throw; + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on non-null Tenant object:' + JSON.stringify(request), () => { + expect(() => { + Tenant.validate(request, false); + }).to.throw('"UpdateTenantRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.unsupported = 'value'; + expect(() => { + Tenant.validate(tenantOptionsClientRequest, false); + }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); + }); + + const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTenantNames.forEach((displayName) => { + it('should throw on invalid displayName:' + JSON.stringify(displayName), () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.displayName = displayName; + expect(() => { + Tenant.validate(tenantOptionsClientRequest, false); + }).to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); + }); + }); + + it('should throw on invalid emailSignInConfig', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailSignInConfig.enabled = 'invalid'; + expect(() => { + Tenant.validate(tenantOptionsClientRequest, false); + }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); + }); + + const invalidTypes = ['invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTypes.forEach((invalidType) => { + it('should throw on creation with invalid type ' + JSON.stringify(invalidType), () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); + tenantOptionsClientRequest.type = invalidType as any; + expect(() => Tenant.validate(tenantOptionsClientRequest, true)) + .to.throw(`"CreateTenantRequest.type" must be either "full_service" or "lightweight".`); + }); + }); + + it('should throw on update with specified type', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); + tenantOptionsClientRequest.type = 'lightweight'; + expect(() => Tenant.validate(tenantOptionsClientRequest, false)) + .to.throw('"Tenant.type" is an immutable property.'); + }); + }); + + describe('constructor', () => { + const serverRequestCopy: TenantServerResponse = deepCopy(serverRequest); + serverRequestCopy.type = 'LIGHTWEIGHT'; + const tenant = new Tenant(serverRequestCopy); + it('should not throw on valid initialization', () => { + expect(() => new Tenant(serverRequest)).not.to.throw(); + }); + + it('should set readonly property tenantId', () => { + expect(tenant.tenantId).to.equal('TENANT_ID'); + }); + + it('should set readonly property displayName', () => { + expect(tenant.displayName).to.equal('TENANT_DISPLAY_NAME'); + }); + + it('should set readonly property type', () => { + expect(tenant.type).to.equal('lightweight'); + }); + + it('should set readonly property emailSignInConfig', () => { + const expectedEmailSignInConfig = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + expect(tenant.emailSignInConfig).to.deep.equal(expectedEmailSignInConfig); + }); + + it('should throw when no tenant ID is provided', () => { + const invalidOptions = deepCopy(serverRequest); + // Use resource name that does not include a tenant ID. + invalidOptions.name = 'projects/project1'; + expect(() => new Tenant(invalidOptions)) + .to.throw('INTERNAL ASSERT FAILED: Invalid tenant response'); + }); + }); + + describe('toJSON()', () => { + const serverRequestCopy: TenantServerResponse = deepCopy(serverRequest); + serverRequestCopy.type = 'LIGHTWEIGHT'; + it('should return the expected object representation of a tenant', () => { + expect(new Tenant(serverRequestCopy).toJSON()).to.deep.equal({ + tenantId: 'TENANT_ID', + type: 'lightweight', + displayName: 'TENANT_DISPLAY_NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + }); + }); + }); +}); diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index e1359292cc..da0911810c 100644 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -66,6 +66,7 @@ describe('UserImportBuilder', () => { }, ], customClaims: {admin: true}, + tenantId: 'TENANT_ID', }, { uid: '9012', @@ -96,6 +97,7 @@ describe('UserImportBuilder', () => { }, ], customAttributes: JSON.stringify({admin: true}), + tenantId: 'TENANT_ID', }, { localId: '9012', diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index e2af7a8925..ccda9d8da8 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -29,11 +29,12 @@ chai.use(chaiAsPromised); const expect = chai.expect; /** + * @param {string=} tenantId The optional tenant ID to add to the response. * @return {object} A sample valid user response as returned from getAccountInfo * endpoint. */ -function getValidUserResponse(): object { - return { +function getValidUserResponse(tenantId?: string): any { + const response: any = { localId: 'abcdefghijklmnopqrstuvwxyz', email: 'user@gmail.com', emailVerified: true, @@ -79,13 +80,18 @@ function getValidUserResponse(): object { admin: true, }), }; + if (typeof tenantId !== 'undefined') { + response.tenantId = tenantId; + } + return response; } /** + * @param {string=} tenantId The optional tenant ID to add to the user. * @return {object} The expected user JSON representation for the above user * server response. */ -function getUserJSON(): object { +function getUserJSON(tenantId?: string): object { return { uid: 'abcdefghijklmnopqrstuvwxyz', email: 'user@gmail.com', @@ -138,6 +144,7 @@ function getUserJSON(): object { admin: true, }, tokensValidAfterTime: new Date(1476136676000).toUTCString(), + tenantId, }; } @@ -626,6 +633,24 @@ describe('UserRecord', () => { (userRecord.providerData[0] as any).displayName = 'John Smith'; }).to.throw(Error); }); + + it('should return undefined tenantId when not available', () => { + expect(userRecord.tenantId).to.be.undefined; + }); + + it('should return expected tenantId', () => { + const resp = deepCopy(getValidUserResponse('TENANT_ID')); + const tenantUserRecord = new UserRecord(resp); + expect(tenantUserRecord.tenantId).to.equal('TENANT_ID'); + }); + + it('should throw when modifying readonly tenantId property', () => { + expect(() => { + const resp = deepCopy(getValidUserResponse('TENANT_ID')); + const tenantUserRecord = new UserRecord(resp); + (tenantUserRecord as any).tenantId = 'OTHER_TENANT_ID'; + }).to.throw(Error); + }); }); describe('toJSON', () => { @@ -640,5 +665,11 @@ describe('UserRecord', () => { it('should return undefined tokensValidAfterTime when not available', () => { expect((userRecordNoValidSince.toJSON() as any).tokensValidAfterTime).to.be.undefined; }); + + it('should return expected JSON object with tenant ID when available', () => { + const resp = deepCopy(getValidUserResponse('TENANT_ID')); + const tenantUserRecord = new UserRecord(resp); + expect(tenantUserRecord.toJSON()).to.deep.equal(getUserJSON('TENANT_ID')); + }); }); }); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index e8e73a3aa3..bebe719b3b 100755 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -35,6 +35,7 @@ import './auth/auth-api-request.spec'; import './auth/user-import-builder.spec'; import './auth/action-code-settings-builder.spec'; import './auth/auth-config.spec'; +import './auth/tenant.spec'; // Database import './database/database.spec'; From 7770609b7713aee21c2abbc9774a7d09f1ef6819 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 7 May 2019 21:18:16 -0700 Subject: [PATCH 2/4] Minor fixes and tweaks. --- src/auth/auth-config.ts | 8 ++++---- src/utils/error.ts | 4 ++-- test/unit/auth/tenant.spec.ts | 36 ++++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 57969e5b0d..ae3ca05834 100755 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -155,7 +155,7 @@ export interface EmailSignInProviderConfig { passwordRequired?: boolean; // In the backend API, default is true if not provided } -/** The server side Email configuration request interface. */ +/** The server side email configuration request interface. */ export interface EmailSignInConfigServerRequest { allowPasswordSignup?: boolean; enableEmailLinkSignin?: boolean; @@ -163,15 +163,15 @@ export interface EmailSignInConfigServerRequest { /** - * Defines the Email sign-in config class used to convert client side EmailSignInConfig - * to a format that is understand by the Auth server. + * Defines the email sign-in config class used to convert client side EmailSignInConfig + * to a format that is understood by the Auth server. */ export class EmailSignInConfig implements EmailSignInProviderConfig { public readonly enabled?: boolean; public readonly passwordRequired?: boolean; /** - * Static method convert a client side request to a EmailSignInConfigServerRequest. + * Static method to convert a client side request to a EmailSignInConfigServerRequest. * Throws an error if validation fails. * * @param {any} options The options object to convert to a server request. diff --git a/src/utils/error.ts b/src/utils/error.ts index 4f5aef823d..95e18c7dba 100755 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -775,7 +775,7 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', // Invalid email provided. INVALID_EMAIL: 'INVALID_EMAIL', - // Invalid tenant display name: CreateTenant and UpdateTenant. + // Invalid tenant display name. This can be thrown on CreateTenant and UpdateTenant. INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', // Invalid ID token provided. INVALID_ID_TOKEN: 'INVALID_ID_TOKEN', @@ -795,7 +795,7 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { MISSING_CONFIG: 'MISSING_CONFIG', // Missing configuration identifier. MISSING_CONFIG_ID: 'MISSING_PROVIDER_ID', - // Missing tenant display name: CreateTenant and UpdateTenant. + // Missing tenant display name: This can be thrown on CreateTenant and UpdateTenant. MISSING_DISPLAY_NAME: 'MISSING_DISPLAY_NAME', // Missing iOS bundle ID. MISSING_IOS_BUNDLE_ID: 'MISSING_IOS_BUNDLE_ID', diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 3a14367962..2e70eaa751 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -57,7 +57,7 @@ describe('Tenant', () => { }; describe('buildServerRequest()', () => { - describe('for update request', () => { + describe('for an update request', () => { it('should return the expected server request', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); const tenantOptionsServerRequest = deepCopy(serverRequest); @@ -88,7 +88,7 @@ describe('Tenant', () => { }); }); - describe('for create request', () => { + describe('for a create request', () => { it('should return the expected server request', () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); tenantOptionsClientRequest.type = 'lightweight'; @@ -156,30 +156,52 @@ describe('Tenant', () => { const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { - it('should throw on non-null Tenant object:' + JSON.stringify(request), () => { + it('should throw on invalid UpdateTenantRequest:' + JSON.stringify(request), () => { expect(() => { Tenant.validate(request, false); }).to.throw('"UpdateTenantRequest" must be a valid non-null object.'); }); + + it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { + expect(() => { + Tenant.validate(request, true); + }).to.throw('"CreateTenantRequest" must be a valid non-null object.'); + }); }); - it('should throw on unsupported attribute', () => { + it('should throw on unsupported attribute for update request', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.unsupported = 'value'; expect(() => { Tenant.validate(tenantOptionsClientRequest, false); - }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); + }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); + }); + + it('should throw on unsupported attribute for create request', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.unsupported = 'value'; + expect(() => { + Tenant.validate(tenantOptionsClientRequest, true); + }).to.throw(`"unsupported" is not a valid CreateTenantRequest parameter.`); }); const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidTenantNames.forEach((displayName) => { - it('should throw on invalid displayName:' + JSON.stringify(displayName), () => { + it('should throw on invalid UpdateTenantRequest displayName:' + JSON.stringify(displayName), () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.displayName = displayName; expect(() => { Tenant.validate(tenantOptionsClientRequest, false); }).to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); }); + + it('should throw on invalid CreateTenantRequest displayName:' + JSON.stringify(displayName), () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.displayName = displayName; + expect(() => { + Tenant.validate(tenantOptionsClientRequest, true); + }).to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); + }); }); it('should throw on invalid emailSignInConfig', () => { @@ -190,7 +212,7 @@ describe('Tenant', () => { }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); }); - const invalidTypes = ['invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + const invalidTypes = [undefined, 'invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidTypes.forEach((invalidType) => { it('should throw on creation with invalid type ' + JSON.stringify(invalidType), () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); From 03926f0fb5ac5a8f4cc208af12f25cf67d2cc00d Mon Sep 17 00:00:00 2001 From: Bassam Ojeil Date: Tue, 21 May 2019 15:43:25 -0700 Subject: [PATCH 3/4] Addresses comments from review. --- src/auth/auth-config.ts | 42 +++---- src/auth/tenant.ts | 19 ++- test/unit/auth/auth-config.spec.ts | 19 +-- test/unit/auth/tenant.spec.ts | 189 ++++++++++++++--------------- 4 files changed, 123 insertions(+), 146 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index ae3ca05834..fee29414e2 100755 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -177,13 +177,13 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { * @param {any} options The options object to convert to a server request. * @return {EmailSignInConfigServerRequest} The resulting server request. */ - public static buildServerRequest(options: any): EmailSignInConfigServerRequest { + public static buildServerRequest(options: EmailSignInProviderConfig): EmailSignInConfigServerRequest { const request: EmailSignInConfigServerRequest = {}; EmailSignInConfig.validate(options); - if (typeof options !== 'undefined' && options.hasOwnProperty('enabled')) { + if (options.hasOwnProperty('enabled')) { request.allowPasswordSignup = options.enabled; } - if (typeof options !== 'undefined' && options.hasOwnProperty('passwordRequired')) { + if (options.hasOwnProperty('passwordRequired')) { request.enableEmailLinkSignin = !options.passwordRequired; } return request; @@ -194,7 +194,7 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { * * @param {any} options The options object to validate. */ - public static validate(options: any) { + private static validate(options: {[key: string]: any}) { // TODO: Validate the request. const validKeys = { enabled: true, @@ -239,14 +239,14 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { * EmailSignInConfig object. * @constructor */ - constructor(response: any) { + constructor(response: {[key: string]: any}) { if (typeof response.allowPasswordSignup === 'undefined') { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response'); } - utils.addReadonlyGetter(this, 'enabled', response.allowPasswordSignup); - utils.addReadonlyGetter(this, 'passwordRequired', !response.enableEmailLinkSignin); + this.enabled = response.allowPasswordSignup; + this.passwordRequired = !response.enableEmailLinkSignin; } /** @return {object} The plain object representation of the email sign-in config. */ @@ -476,24 +476,24 @@ export class SAMLConfig implements SAMLAuthProviderConfig { AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); } - utils.addReadonlyGetter(this, 'providerId', SAMLConfig.getProviderIdFromResourceName(response.name)); + this.providerId = SAMLConfig.getProviderIdFromResourceName(response.name); // RP config. - utils.addReadonlyGetter(this, 'rpEntityId', response.spConfig.spEntityId); - utils.addReadonlyGetter(this, 'callbackURL', response.spConfig.callbackUri); + this.rpEntityId = response.spConfig.spEntityId; + this.callbackURL = response.spConfig.callbackUri; // IdP config. - utils.addReadonlyGetter(this, 'idpEntityId', response.idpConfig.idpEntityId); - utils.addReadonlyGetter(this, 'ssoURL', response.idpConfig.ssoUrl); - utils.addReadonlyGetter(this, 'enableRequestSigning', !!response.idpConfig.signRequest); + this.idpEntityId = response.idpConfig.idpEntityId; + this.ssoURL = response.idpConfig.ssoUrl; + this.enableRequestSigning = !!response.idpConfig.signRequest; const x509Certificates: string[] = []; for (const cert of (response.idpConfig.idpCertificates || [])) { if (cert.x509Certificate) { x509Certificates.push(cert.x509Certificate); } } - utils.addReadonlyGetter(this, 'x509Certificates', x509Certificates); + this.x509Certificates = x509Certificates; // When enabled is undefined, it takes its default value of false. - utils.addReadonlyGetter(this, 'enabled', !!response.enabled); - utils.addReadonlyGetter(this, 'displayName', response.displayName); + this.enabled = !!response.enabled; + this.displayName = response.displayName; } /** @return {SAMLAuthProviderConfig} The plain object representation of the SAMLConfig. */ @@ -664,12 +664,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig { AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); } - utils.addReadonlyGetter(this, 'providerId', OIDCConfig.getProviderIdFromResourceName(response.name)); - utils.addReadonlyGetter(this, 'clientId', response.clientId); - utils.addReadonlyGetter(this, 'issuer', response.issuer); + this.providerId = OIDCConfig.getProviderIdFromResourceName(response.name); + this.clientId = response.clientId; + this.issuer = response.issuer; // When enabled is undefined, it takes its default value of false. - utils.addReadonlyGetter(this, 'enabled', !!response.enabled); - utils.addReadonlyGetter(this, 'displayName', response.displayName); + this.enabled = !!response.enabled; + this.displayName = response.displayName; } /** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */ diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index c9984173c0..3fd4b1b009 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -109,7 +109,7 @@ export class Tenant { * @param {any} request The tenant options object to validate. * @param {boolean} createRequest Whether this is a create request. */ - public static validate(request: any, createRequest: boolean) { + private static validate(request: any, createRequest: boolean) { const validKeys = { displayName: true, type: true, @@ -156,7 +156,8 @@ export class Tenant { } // Validate emailSignInConfig type if provided. if (typeof request.emailSignInConfig !== 'undefined') { - EmailSignInConfig.validate(request.emailSignInConfig); + // This will throw an error if invalid. + EmailSignInConfig.buildServerRequest(request.emailSignInConfig); } } @@ -174,26 +175,24 @@ export class Tenant { 'INTERNAL ASSERT FAILED: Invalid tenant response', ); } - utils.addReadonlyGetter(this, 'tenantId', tenantId); - utils.addReadonlyGetter(this, 'displayName', response.displayName); - utils.addReadonlyGetter( - this, 'type', (response.type && response.type.toLowerCase()) || undefined); + this.tenantId = tenantId; + this.displayName = response.displayName; + this.type = (response.type && response.type.toLowerCase()) || undefined; try { - utils.addReadonlyGetter(this, 'emailSignInConfig', new EmailSignInConfig(response)); + this.emailSignInConfig = new EmailSignInConfig(response); } catch (e) { - utils.addReadonlyGetter(this, 'emailSignInConfig', undefined); + this.emailSignInConfig = undefined; } } /** @return {object} The plain object representation of the tenant. */ public toJSON(): object { - const json: any = { + return { tenantId: this.tenantId, displayName: this.displayName, type: this.type, emailSignInConfig: this.emailSignInConfig && this.emailSignInConfig.toJSON(), }; - return json; } } diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index 7d9c10af05..a18739774a 100755 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -114,27 +114,18 @@ describe('EmailSignInConfig', () => { }); }); - it('should throw on invalid input', () => { - expect(() => EmailSignInConfig.buildServerRequest({ - enabled: 'invalid', - passwordRequired: true, - })).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); - }); - }); - - describe('validate()', () => { const invalidOptions = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; invalidOptions.forEach((options) => { it('should throw on invalid EmailSignInConfig:' + JSON.stringify(options), () => { expect(() => { - EmailSignInConfig.validate(options as any); + EmailSignInConfig.buildServerRequest(options as any); }).to.throw('"EmailSignInConfig" must be a non-null object.'); }); }); it('should throw on EmailSignInConfig with unsupported attribute', () => { expect(() => { - EmailSignInConfig.validate({ + EmailSignInConfig.buildServerRequest({ unsupported: true, enabled: true, passwordRequired: false, @@ -146,7 +137,7 @@ describe('EmailSignInConfig', () => { invalidEnabled.forEach((enabled) => { it('should throw on invalid EmailSignInConfig.enabled:' + JSON.stringify(enabled), () => { expect(() => { - EmailSignInConfig.validate({ + EmailSignInConfig.buildServerRequest({ enabled, passwordRequired: false, } as any); @@ -158,7 +149,7 @@ describe('EmailSignInConfig', () => { invalidEnabled.forEach((passwordRequired) => { it('should throw on invalid EmailSignInConfig.passwordRequired:' + JSON.stringify(passwordRequired), () => { expect(() => { - EmailSignInConfig.validate({ + EmailSignInConfig.buildServerRequest({ enabled: true, passwordRequired, } as any); @@ -169,7 +160,7 @@ describe('EmailSignInConfig', () => { it('should not throw on valid EmailSignInConfig', () => { expect(() => { - EmailSignInConfig.validate({ + EmailSignInConfig.buildServerRequest({ enabled: true, passwordRequired: false, } as any); diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 2e70eaa751..a53f5247b7 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -66,26 +66,69 @@ describe('Tenant', () => { .to.deep.equal(tenantOptionsServerRequest); }); - it('should throw on invalid input', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest); - tenantOptionsClientRequest.displayName = null; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) - .to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); - }); - - it('should throw on invalid EmailSignInConfig', () => { + it('should throw on invalid EmailSignInConfig object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); tenantOptionsClientRequest.emailSignInConfig = null; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) .to.throw('"EmailSignInConfig" must be a non-null object.'); }); + it('should throw on invalid EmailSignInConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailSignInConfig.enabled = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, false); + }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); + }); + it('should throw when type is specified', () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); tenantOptionsClientRequest.type = 'lightweight'; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) .to.throw('"Tenant.type" is an immutable property.'); }); + + it('should not throw on valid client request object', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, false); + }).not.to.throw; + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid UpdateTenantRequest:' + JSON.stringify(request), () => { + expect(() => { + Tenant.buildServerRequest(request as any, false); + }).to.throw('"UpdateTenantRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for update request', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.unsupported = 'value'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, false); + }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); + }); + + const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTenantNames.forEach((displayName) => { + it('should throw on invalid UpdateTenantRequest displayName:' + JSON.stringify(displayName), () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.displayName = displayName; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, false); + }).to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); + }); + }); + + it('should throw on update with specified type', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); + tenantOptionsClientRequest.type = 'lightweight'; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + .to.throw('"Tenant.type" is an immutable property.'); + }); }); describe('for a create request', () => { @@ -100,13 +143,14 @@ describe('Tenant', () => { .to.deep.equal(tenantOptionsServerRequest); }); - it('should throw on invalid input', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); - tenantOptionsClientRequest.displayName = null; - tenantOptionsClientRequest.type = 'full_service'; - - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) - .to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); + const invalidTypes = [undefined, 'invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTypes.forEach((invalidType) => { + it('should throw on invalid type ' + JSON.stringify(invalidType), () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); + tenantOptionsClientRequest.type = invalidType as any; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + .to.throw(`"CreateTenantRequest.type" must be either "full_service" or "lightweight".`); + }); }); it('should throw on invalid EmailSignInConfig', () => { @@ -118,9 +162,36 @@ describe('Tenant', () => { .to.throw('"EmailSignInConfig" must be a non-null object.'); }); - const invalidTypes = ['invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { + expect(() => { + Tenant.buildServerRequest(request as any, true); + }).to.throw('"CreateTenantRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for create request', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.unsupported = 'value'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, true); + }).to.throw(`"unsupported" is not a valid CreateTenantRequest parameter.`); + }); + + const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTenantNames.forEach((displayName) => { + it('should throw on invalid CreateTenantRequest displayName:' + JSON.stringify(displayName), () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.displayName = displayName; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, true); + }).to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); + }); + }); + invalidTypes.forEach((invalidType) => { - it('should throw on invalid type ' + JSON.stringify(invalidType), () => { + it('should throw on creation with invalid type ' + JSON.stringify(invalidType), () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); tenantOptionsClientRequest.type = invalidType as any; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) @@ -146,90 +217,6 @@ describe('Tenant', () => { }); }); - describe('validate()', () => { - it('should not throw on valid client request object', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest); - expect(() => { - Tenant.validate(tenantOptionsClientRequest, false); - }).not.to.throw; - }); - - const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; - nonObjects.forEach((request) => { - it('should throw on invalid UpdateTenantRequest:' + JSON.stringify(request), () => { - expect(() => { - Tenant.validate(request, false); - }).to.throw('"UpdateTenantRequest" must be a valid non-null object.'); - }); - - it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { - expect(() => { - Tenant.validate(request, true); - }).to.throw('"CreateTenantRequest" must be a valid non-null object.'); - }); - }); - - it('should throw on unsupported attribute for update request', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.unsupported = 'value'; - expect(() => { - Tenant.validate(tenantOptionsClientRequest, false); - }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); - }); - - it('should throw on unsupported attribute for create request', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.unsupported = 'value'; - expect(() => { - Tenant.validate(tenantOptionsClientRequest, true); - }).to.throw(`"unsupported" is not a valid CreateTenantRequest parameter.`); - }); - - const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; - invalidTenantNames.forEach((displayName) => { - it('should throw on invalid UpdateTenantRequest displayName:' + JSON.stringify(displayName), () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.displayName = displayName; - expect(() => { - Tenant.validate(tenantOptionsClientRequest, false); - }).to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); - }); - - it('should throw on invalid CreateTenantRequest displayName:' + JSON.stringify(displayName), () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.displayName = displayName; - expect(() => { - Tenant.validate(tenantOptionsClientRequest, true); - }).to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); - }); - }); - - it('should throw on invalid emailSignInConfig', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.emailSignInConfig.enabled = 'invalid'; - expect(() => { - Tenant.validate(tenantOptionsClientRequest, false); - }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); - }); - - const invalidTypes = [undefined, 'invalid', null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; - invalidTypes.forEach((invalidType) => { - it('should throw on creation with invalid type ' + JSON.stringify(invalidType), () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); - tenantOptionsClientRequest.type = invalidType as any; - expect(() => Tenant.validate(tenantOptionsClientRequest, true)) - .to.throw(`"CreateTenantRequest.type" must be either "full_service" or "lightweight".`); - }); - }); - - it('should throw on update with specified type', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); - tenantOptionsClientRequest.type = 'lightweight'; - expect(() => Tenant.validate(tenantOptionsClientRequest, false)) - .to.throw('"Tenant.type" is an immutable property.'); - }); - }); - describe('constructor', () => { const serverRequestCopy: TenantServerResponse = deepCopy(serverRequest); serverRequestCopy.type = 'LIGHTWEIGHT'; From d84dcd4cefbe15adee6c6382e1f348e9d70bd1b2 Mon Sep 17 00:00:00 2001 From: Bassam Ojeil Date: Wed, 22 May 2019 21:15:32 -0700 Subject: [PATCH 4/4] Addresses review comments. --- src/auth/tenant.ts | 4 +-- test/unit/auth/auth-config.spec.ts | 24 +++-------------- test/unit/auth/tenant.spec.ts | 43 +++++++++++++----------------- test/unit/auth/user-record.spec.ts | 2 +- 4 files changed, 26 insertions(+), 47 deletions(-) diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 3fd4b1b009..6d08d9cafb 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -63,7 +63,7 @@ export class Tenant { public readonly tenantId: string; public readonly type?: TenantType; public readonly displayName?: string; - public readonly emailSignInConfig?: EmailSignInConfig | null; + public readonly emailSignInConfig?: EmailSignInConfig; /** * Builds the corresponding server request for a TenantOptions object. @@ -139,7 +139,7 @@ export class Tenant { `"${label}.displayName" must be a valid non-empty string.`, ); } - // Validate type type if provided. + // Validate type if provided. if (typeof request.type !== 'undefined' && !createRequest) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index a18739774a..e37cec8e09 100755 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -53,11 +53,11 @@ describe('EmailSignInConfig', () => { }); it('should set readonly property "enabled" to false on allowPasswordSignup disabled', () => { - const validConfig2 = new EmailSignInConfig({ + const passwordSignupDisabledConfig = new EmailSignInConfig({ allowPasswordSignup: false, enableEmailLinkSignin: false, }); - expect(validConfig2.enabled).to.be.false; + expect(passwordSignupDisabledConfig.enabled).to.be.false; }); it('should set readonly property "passwordRequired" to false on email link sign in enabled', () => { @@ -65,18 +65,11 @@ describe('EmailSignInConfig', () => { }); it('should set readonly property "passwordRequired" to true on email link sign in disabled', () => { - const validConfig2 = new EmailSignInConfig({ + const passwordSignupEnabledConfig = new EmailSignInConfig({ allowPasswordSignup: true, enableEmailLinkSignin: false, }); - expect(validConfig2.passwordRequired).to.be.true; - }); - - it('should not throw on valid response', () => { - expect(() => new EmailSignInConfig({ - allowPasswordSignup: true, - enableEmailLinkSignin: true, - })).not.to.throw(); + expect(passwordSignupEnabledConfig.passwordRequired).to.be.true; }); }); @@ -157,15 +150,6 @@ describe('EmailSignInConfig', () => { }); }); }); - - it('should not throw on valid EmailSignInConfig', () => { - expect(() => { - EmailSignInConfig.buildServerRequest({ - enabled: true, - passwordRequired: false, - } as any); - }).not.to.throw; - }); }); describe('SAMLConfig', () => { diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index a53f5247b7..be1a3a576b 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -57,19 +57,21 @@ describe('Tenant', () => { }; describe('buildServerRequest()', () => { + const createRequest = true; + describe('for an update request', () => { it('should return the expected server request', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); const tenantOptionsServerRequest = deepCopy(serverRequest); delete tenantOptionsServerRequest.name; - expect(Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) .to.deep.equal(tenantOptionsServerRequest); }); it('should throw on invalid EmailSignInConfig object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); tenantOptionsClientRequest.emailSignInConfig = null; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) .to.throw('"EmailSignInConfig" must be a non-null object.'); }); @@ -77,21 +79,21 @@ describe('Tenant', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.emailSignInConfig.enabled = 'invalid'; expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, false); + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); }); - it('should throw when type is specified', () => { + it('should throw when type is specified in an update request', () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); tenantOptionsClientRequest.type = 'lightweight'; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) .to.throw('"Tenant.type" is an immutable property.'); }); it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, false); + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).not.to.throw; }); @@ -99,7 +101,7 @@ describe('Tenant', () => { nonObjects.forEach((request) => { it('should throw on invalid UpdateTenantRequest:' + JSON.stringify(request), () => { expect(() => { - Tenant.buildServerRequest(request as any, false); + Tenant.buildServerRequest(request as any, !createRequest); }).to.throw('"UpdateTenantRequest" must be a valid non-null object.'); }); }); @@ -108,7 +110,7 @@ describe('Tenant', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.unsupported = 'value'; expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, false); + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); }); @@ -118,17 +120,10 @@ describe('Tenant', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.displayName = displayName; expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, false); + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); }).to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); }); }); - - it('should throw on update with specified type', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); - tenantOptionsClientRequest.type = 'lightweight'; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, false)) - .to.throw('"Tenant.type" is an immutable property.'); - }); }); describe('for a create request', () => { @@ -139,7 +134,7 @@ describe('Tenant', () => { delete tenantOptionsServerRequest.name; tenantOptionsServerRequest.type = 'LIGHTWEIGHT'; - expect(Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) .to.deep.equal(tenantOptionsServerRequest); }); @@ -148,7 +143,7 @@ describe('Tenant', () => { it('should throw on invalid type ' + JSON.stringify(invalidType), () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); tenantOptionsClientRequest.type = invalidType as any; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) .to.throw(`"CreateTenantRequest.type" must be either "full_service" or "lightweight".`); }); }); @@ -158,7 +153,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.emailSignInConfig = null; tenantOptionsClientRequest.type = 'full_service'; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) .to.throw('"EmailSignInConfig" must be a non-null object.'); }); @@ -166,7 +161,7 @@ describe('Tenant', () => { nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { expect(() => { - Tenant.buildServerRequest(request as any, true); + Tenant.buildServerRequest(request as any, createRequest); }).to.throw('"CreateTenantRequest" must be a valid non-null object.'); }); }); @@ -175,7 +170,7 @@ describe('Tenant', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.unsupported = 'value'; expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, true); + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw(`"unsupported" is not a valid CreateTenantRequest parameter.`); }); @@ -185,7 +180,7 @@ describe('Tenant', () => { const tenantOptionsClientRequest = deepCopy(clientRequest) as any; tenantOptionsClientRequest.displayName = displayName; expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, true); + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); }).to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); }); }); @@ -194,7 +189,7 @@ describe('Tenant', () => { it('should throw on creation with invalid type ' + JSON.stringify(invalidType), () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(tenantOptions); tenantOptionsClientRequest.type = invalidType as any; - expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, true)) + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) .to.throw(`"CreateTenantRequest.type" must be either "full_service" or "lightweight".`); }); }); @@ -207,7 +202,7 @@ describe('Tenant', () => { .to.equal('TENANT_ID'); }); - it('should return the expected tenant ID from resource name with multiple tenants substring', () => { + it('should return the expected tenant ID from resource name whose project ID contains "tenants" substring', () => { expect(Tenant.getTenantIdFromResourceName('projects/projecttenants/tenants/TENANT_ID')) .to.equal('TENANT_ID'); }); diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index ccda9d8da8..21c93934c4 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -33,7 +33,7 @@ const expect = chai.expect; * @return {object} A sample valid user response as returned from getAccountInfo * endpoint. */ -function getValidUserResponse(tenantId?: string): any { +function getValidUserResponse(tenantId?: string): {[key: string]: any} { const response: any = { localId: 'abcdefghijklmnopqrstuvwxyz', email: 'user@gmail.com',