From 3097f06346cc23bc02f2a21c37be1be3fda6ff27 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 1 Aug 2024 15:01:42 -0700 Subject: [PATCH] Added mobileLinksConfig field for project and tenant creation and/or updating workflow. This will be later used in Firebase Dynamic links deprecation work. --- etc/firebase-admin.auth.api.md | 12 +++++ src/auth/auth-config.ts | 54 ++++++++++++++++++++++ src/auth/index.ts | 2 + src/auth/project-config.ts | 30 +++++++++++++ src/auth/tenant.ts | 28 ++++++++++++ test/unit/auth/auth-config.spec.ts | 29 ++++++++++++ test/unit/auth/project-config.spec.ts | 65 +++++++++++++++++++++++++-- test/unit/auth/tenant.spec.ts | 56 +++++++++++++++++++++++ 8 files changed, 273 insertions(+), 3 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index d921458bec..843ee445ca 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -759,6 +759,14 @@ export interface ListUsersResult { users: UserRecord[]; } +// @public +export interface MobileLinksConfig { + domain?: MobileLinksDomain; +} + +// @public +export type MobileLinksDomain = 'HOSTING_DOMAIN' | 'FIREBASE_DYNAMIC_LINK_DOMAIN'; + // @public export interface MultiFactorConfig { factorIds?: AuthFactorType[]; @@ -849,6 +857,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { // @public export class ProjectConfig { readonly emailPrivacyConfig?: EmailPrivacyConfig; + readonly mobileLinksConfig?: MobileLinksConfig; get multiFactorConfig(): MultiFactorConfig | undefined; readonly passwordPolicyConfig?: PasswordPolicyConfig; get recaptchaConfig(): RecaptchaConfig | undefined; @@ -934,6 +943,7 @@ export class Tenant { readonly displayName?: string; readonly emailPrivacyConfig?: EmailPrivacyConfig; get emailSignInConfig(): EmailSignInProviderConfig | undefined; + readonly mobileLinksConfig?: MobileLinksConfig; get multiFactorConfig(): MultiFactorConfig | undefined; readonly passwordPolicyConfig?: PasswordPolicyConfig; get recaptchaConfig(): RecaptchaConfig | undefined; @@ -988,6 +998,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor // @public export interface UpdateProjectConfigRequest { emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; multiFactorConfig?: MultiFactorConfig; passwordPolicyConfig?: PasswordPolicyConfig; recaptchaConfig?: RecaptchaConfig; @@ -1014,6 +1025,7 @@ export interface UpdateTenantRequest { displayName?: string; emailPrivacyConfig?: EmailPrivacyConfig; emailSignInConfig?: EmailSignInProviderConfig; + mobileLinksConfig?: MobileLinksConfig; multiFactorConfig?: MultiFactorConfig; passwordPolicyConfig?: PasswordPolicyConfig; recaptchaConfig?: RecaptchaConfig; diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 28ee595c46..5f8bd76fa6 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1965,6 +1965,60 @@ export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; } +/** + * Configuration for settings related to univeral links (iOS) + * and app links (Android). + */ +export interface MobileLinksConfig { + /** + * Use firebase Hosting or dynamic link domain as the out-of-band code domain. + */ + domain?: MobileLinksDomain; +} + +/** + * Open code in app domain to use for app links and universal links. + */ +export type MobileLinksDomain = 'HOSTING_DOMAIN' | 'FIREBASE_DYNAMIC_LINK_DOMAIN'; + +/** + * Defines the MobileLinksAuthConfig class used for validation. + * + * @internal + */ +export class MobileLinksAuthConfig { + public static validate(options: MobileLinksConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MobileLinksConfig" must be a non-null object.', + ); + } + + const validKeys = { + domain: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid "MobileLinksConfig" parameter.`, + ); + } + } + + if (typeof options.domain !== 'undefined' + && options.domain !== 'HOSTING_DOMAIN' + && options.domain !== 'FIREBASE_DYNAMIC_LINK_DOMAIN') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".', + ); + } + } +} + /** * A password policy's enforcement state. */ diff --git a/src/auth/index.ts b/src/auth/index.ts index f350b28837..e90a4ba211 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -103,6 +103,8 @@ export { PasswordPolicyEnforcementState, CustomStrengthOptionsConfig, EmailPrivacyConfig, + MobileLinksConfig, + MobileLinksDomain, } from './auth-config'; export { diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 250d6549ac..f9ec2a1734 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -28,6 +28,8 @@ import { PasswordPolicyConfig, EmailPrivacyConfig, EmailPrivacyAuthConfig, + MobileLinksConfig, + MobileLinksAuthConfig, } from './auth-config'; import { deepCopy } from '../utils/deep-copy'; @@ -59,6 +61,11 @@ export interface UpdateProjectConfigRequest { * The email privacy configuration to update on the project */ emailPrivacyConfig?: EmailPrivacyConfig; + + /** + * The mobile links configuration for the project + */ + mobileLinksConfig?: MobileLinksConfig; } /** @@ -70,6 +77,7 @@ export interface ProjectConfigServerResponse { recaptchaConfig?: RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; } /** @@ -81,6 +89,7 @@ export interface ProjectConfigClientRequest { recaptchaConfig?: RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; } /** @@ -122,6 +131,11 @@ export class ProjectConfig { */ public readonly emailPrivacyConfig?: EmailPrivacyConfig; + /** + * The mobile links configuration for the project + */ + public readonly mobileLinksConfig?: MobileLinksConfig + /** * Validates a project config options object. Throws an error on failure. * @@ -140,6 +154,7 @@ export class ProjectConfig { recaptchaConfig: true, passwordPolicyConfig: true, emailPrivacyConfig: true, + mobileLinksConfig: true, } // Check for unsupported top level attributes. for (const key in request) { @@ -173,6 +188,11 @@ export class ProjectConfig { if (typeof request.emailPrivacyConfig !== 'undefined') { EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); } + + // Validate Mobile Links Config if provided. + if (typeof request.mobileLinksConfig !== 'undefined') { + MobileLinksAuthConfig.validate(request.mobileLinksConfig); + } } /** @@ -200,6 +220,9 @@ export class ProjectConfig { if (typeof configOptions.emailPrivacyConfig !== 'undefined') { request.emailPrivacyConfig = configOptions.emailPrivacyConfig; } + if (typeof configOptions.mobileLinksConfig !== 'undefined') { + request.mobileLinksConfig = configOptions.mobileLinksConfig; + } return request; } @@ -234,6 +257,9 @@ export class ProjectConfig { if (typeof response.emailPrivacyConfig !== 'undefined') { this.emailPrivacyConfig = response.emailPrivacyConfig; } + if (typeof response.mobileLinksConfig !== 'undefined') { + this.mobileLinksConfig = response.mobileLinksConfig; + } } /** * Returns a JSON-serializable representation of this object. @@ -248,6 +274,7 @@ export class ProjectConfig { recaptchaConfig: this.recaptchaConfig_?.toJSON(), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), + mobileLinksConfig: deepCopy(this.mobileLinksConfig), }; if (typeof json.smsRegionConfig === 'undefined') { delete json.smsRegionConfig; @@ -264,6 +291,9 @@ export class ProjectConfig { if (typeof json.emailPrivacyConfig === 'undefined') { delete json.emailPrivacyConfig; } + if (typeof json.mobileLinksConfig === 'undefined') { + delete json.mobileLinksConfig; + } return json; } } diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 76d97f3259..258c10c5b8 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -24,6 +24,7 @@ import { MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig, PasswordPolicyConfig, PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, EmailPrivacyConfig, EmailPrivacyAuthConfig, + MobileLinksConfig, MobileLinksAuthConfig } from './auth-config'; /** @@ -77,6 +78,11 @@ export interface UpdateTenantRequest { * The email privacy configuration for the tenant */ emailPrivacyConfig?: EmailPrivacyConfig; + + /** + * The mobile links configuration for the project + */ + mobileLinksConfig?: MobileLinksConfig; } /** @@ -95,6 +101,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque recaptchaConfig?: RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; } /** The tenant server response interface. */ @@ -110,6 +117,7 @@ export interface TenantServerResponse { recaptchaConfig? : RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; } /** @@ -175,6 +183,11 @@ export class Tenant { * The email privacy configuration for the tenant */ public readonly emailPrivacyConfig?: EmailPrivacyConfig; + /** + * The mobile links configuration for the tenant + */ + public readonly mobileLinksConfig?: MobileLinksConfig + /** * Builds the corresponding server request for a TenantOptions object. @@ -217,6 +230,9 @@ export class Tenant { if (typeof tenantOptions.emailPrivacyConfig !== 'undefined') { request.emailPrivacyConfig = tenantOptions.emailPrivacyConfig; } + if (typeof tenantOptions.mobileLinksConfig !== 'undefined') { + request.mobileLinksConfig = tenantOptions.mobileLinksConfig; + } return request; } @@ -254,6 +270,7 @@ export class Tenant { recaptchaConfig: true, passwordPolicyConfig: true, emailPrivacyConfig: true, + mobileLinksConfig: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -317,6 +334,10 @@ export class Tenant { if (typeof request.emailPrivacyConfig !== 'undefined') { EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); } + // Validate Mobile Links Config if provided. + if (typeof request.mobileLinksConfig !== 'undefined') { + MobileLinksAuthConfig.validate(request.mobileLinksConfig); + } } /** @@ -363,6 +384,9 @@ export class Tenant { if (typeof response.emailPrivacyConfig !== 'undefined') { this.emailPrivacyConfig = deepCopy(response.emailPrivacyConfig); } + if (typeof response.mobileLinksConfig !== 'undefined') { + this.mobileLinksConfig = deepCopy(response.mobileLinksConfig); + } } /** @@ -403,6 +427,7 @@ export class Tenant { recaptchaConfig: this.recaptchaConfig_?.toJSON(), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), + mobileLinksConfig: deepCopy(this.mobileLinksConfig), }; if (typeof json.multiFactorConfig === 'undefined') { delete json.multiFactorConfig; @@ -422,6 +447,9 @@ export class Tenant { if (typeof json.emailPrivacyConfig === 'undefined') { delete json.emailPrivacyConfig; } + if (typeof json.mobileLinksConfig === 'undefined') { + delete json.mobileLinksConfig; + } return json; } } diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index 8894bdc5ff..c5bdb7c445 100644 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -28,6 +28,8 @@ import { MAXIMUM_TEST_PHONE_NUMBERS, PasswordPolicyAuthConfig, CustomStrengthOptionsConfig, + MobileLinksAuthConfig, + MobileLinksConfig, } from '../../../src/auth/auth-config'; import { SAMLUpdateAuthProviderRequest, OIDCUpdateAuthProviderRequest, @@ -1322,3 +1324,30 @@ describe('PasswordPolicyAuthConfig',() => { }); }); }); + +describe('MobileLinksAuthConfig',() => { + describe('validate',() => { + it('should throw an error on invalid MobileLinksConfig key',() => { + const config: any = { + link: 'HOSTING_DOMAIN' + }; + expect(() => + MobileLinksAuthConfig.validate(config) + ).to.throw('"link" is not a valid "MobileLinksConfig" parameter.'); + }); + + it('should throw an error on invalid MobileLinksDomain',() => { + const config: any = { + domain: 'WRONG_DOMAIN' + }; + expect(() => MobileLinksAuthConfig.validate(config)) + .to.throw('"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".'); + });// + it('should now throw an error on valid MobileLinksConfig',() => { + const config: MobileLinksConfig = { + domain: 'HOSTING_DOMAIN' + }; + expect(() => MobileLinksAuthConfig.validate(config)).not.to.throw(); + }); + }); +}); diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 5934dd15fd..b0aebd0867 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -51,6 +51,18 @@ describe('ProjectConfig', () => { }, ], }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + }, passwordPolicyConfig: { passwordPolicyEnforcementState: 'ENFORCE', forceUpgradeOnSignin: true, @@ -70,6 +82,9 @@ describe('ProjectConfig', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', + }, }; const updateProjectConfigRequest1: UpdateProjectConfigRequest = { @@ -93,6 +108,9 @@ describe('ProjectConfig', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: false, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; const updateProjectConfigRequest2: UpdateProjectConfigRequest = { @@ -451,6 +469,22 @@ describe('ProjectConfig', () => { }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); }); + it('should throw on invalid MobileLinksConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.mobileLinksConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid "MobileLinksConfig" parameter.'); + }); + + it('should throw on invalid domain attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.mobileLinksConfig.domain = 'random domain'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { @@ -490,6 +524,7 @@ describe('ProjectConfig', () => { it('should set readonly property multiFactorConfig', () => { const expectedMultiFactorConfig = { state: 'DISABLED', + factorIds: [], providerConfigs: [ { state: 'ENABLED', @@ -542,17 +577,41 @@ describe('ProjectConfig', () => { }; expect(projectConfig.emailPrivacyConfig).to.deep.equal(expectedEmailPrivacyConfig); }); + + it('should set readonly property mobileLinksConfig', () => { + const expectedMobileLinksConfig = { + domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', + }; + expect(projectConfig.mobileLinksConfig).to.deep.equal(expectedMobileLinksConfig); + }); }); describe('toJSON()', () => { + // server output and toJson does not have the same format + const passwordPolicyJson: any = { + enforcementState: 'ENFORCE', + constraints: { + requireLowercase: true, + requireUppercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + minLength: 8, + maxLength: 30 + }, + forceUpgradeOnSignin: true + }; + const multiFactorJson: any = deepCopy(serverResponse.mfa); + // factorIDs were added by default. + multiFactorJson['factorIds'] = []; const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); it('should return the expected object representation of project config', () => { expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ smsRegionConfig: deepCopy(serverResponse.smsRegionConfig), - multiFactorConfig: deepCopy(serverResponse.mfa), + multiFactorConfig: multiFactorJson, recaptchaConfig: deepCopy(serverResponse.recaptchaConfig), - passwordPolicyConfig: deepCopy(serverResponse.passwordPolicyConfig), + passwordPolicyConfig: passwordPolicyJson, emailPrivacyConfig: deepCopy(serverResponse.emailPrivacyConfig), + mobileLinksConfig: deepCopy(serverResponse.mobileLinksConfig), }); }); @@ -564,8 +623,8 @@ describe('ProjectConfig', () => { delete serverResponseOptionalCopy.recaptchaConfig?.managedRules; delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender; delete serverResponseOptionalCopy.passwordPolicyConfig; - delete serverResponseOptionalCopy.passwordPolicyConfig; delete serverResponseOptionalCopy.emailPrivacyConfig; + delete serverResponseOptionalCopy.mobileLinksConfig; expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ recaptchaConfig: { recaptchaKeys: deepCopy(serverResponse.recaptchaConfig?.recaptchaKeys), diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index e1006e47b7..f9c2eb6fed 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -103,6 +103,9 @@ describe('Tenant', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; const clientRequest: UpdateTenantRequest = { @@ -132,6 +135,9 @@ describe('Tenant', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; const serverRequestWithoutMfa: TenantServerResponse = { @@ -143,6 +149,9 @@ describe('Tenant', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; const clientRequestWithoutMfa: UpdateTenantRequest = { @@ -155,6 +164,9 @@ describe('Tenant', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; const clientRequestWithRecaptcha: UpdateTenantRequest = { @@ -219,6 +231,9 @@ describe('Tenant', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; describe('buildServerRequest()', () => { @@ -584,6 +599,22 @@ describe('Tenant', () => { }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); }); + it('should throw on invalid MobileLinksConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.mobileLinksConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid "MobileLinksConfig" parameter.'); + }); + + it('should throw on invalid domain attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.mobileLinksConfig.domain = 'random domain'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".'); + }); + it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha); expect(() => { @@ -979,6 +1010,22 @@ describe('Tenant', () => { }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); }); + it('should throw on invalid MobileLinksConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.mobileLinksConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid "MobileLinksConfig" parameter.'); + }); + + it('should throw on invalid domain attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.mobileLinksConfig.domain = 'random domain'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -1098,6 +1145,13 @@ describe('Tenant', () => { deepCopy(clientRequest.passwordPolicyConfig)); }); + it('should set readonly property mobileLinksConfig', () => { + const expectedMobileLinksConfig = { + domain: 'HOSTING_DOMAIN', + }; + expect(clientRequest.mobileLinksConfig).to.deep.equal(expectedMobileLinksConfig); + }); + it('should set readonly property emailPrivacyConfig', () => { const expectedEmailPrivacyConfig = { enableImprovedEmailPrivacy: true, @@ -1147,6 +1201,7 @@ describe('Tenant', () => { recaptchaConfig: deepCopy(serverResponseWithRecaptcha.recaptchaConfig), passwordPolicyConfig: deepCopy(clientRequest.passwordPolicyConfig), emailPrivacyConfig: deepCopy(clientRequest.emailPrivacyConfig), + mobileLinksConfig: deepCopy(clientRequest.mobileLinksConfig), }); }); @@ -1158,6 +1213,7 @@ describe('Tenant', () => { delete serverRequestCopyWithoutMfa.recaptchaConfig; delete serverRequestCopyWithoutMfa.passwordPolicyConfig; delete serverRequestCopyWithoutMfa.emailPrivacyConfig; + delete serverRequestCopyWithoutMfa.mobileLinksConfig; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', displayName: 'TENANT-DISPLAY-NAME',