diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index f8ae6d897a..3723abd051 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -238,6 +238,11 @@ export interface EmailIdentifier { email: string; } +// @public +export interface EmailPrivacyConfig { + enableImprovedEmailPrivacy?: boolean; +} + // @public export interface EmailSignInProviderConfig { enabled: boolean; @@ -363,6 +368,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { // @public export class ProjectConfig { + readonly emailPrivacyConfig?: EmailPrivacyConfig; get multiFactorConfig(): MultiFactorConfig | undefined; readonly passwordPolicyConfig?: PasswordPolicyConfig; get recaptchaConfig(): RecaptchaConfig | undefined; @@ -446,6 +452,7 @@ export class Tenant { // (undocumented) readonly anonymousSignInEnabled: boolean; readonly displayName?: string; + readonly emailPrivacyConfig?: EmailPrivacyConfig; get emailSignInConfig(): EmailSignInProviderConfig | undefined; get multiFactorConfig(): MultiFactorConfig | undefined; readonly passwordPolicyConfig?: PasswordPolicyConfig; @@ -500,6 +507,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor // @public export interface UpdateProjectConfigRequest { + emailPrivacyConfig?: EmailPrivacyConfig; multiFactorConfig?: MultiFactorConfig; passwordPolicyConfig?: PasswordPolicyConfig; recaptchaConfig?: RecaptchaConfig; @@ -524,6 +532,7 @@ export interface UpdateRequest { export interface UpdateTenantRequest { anonymousSignInEnabled?: boolean; displayName?: string; + emailPrivacyConfig?: EmailPrivacyConfig; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; passwordPolicyConfig?: PasswordPolicyConfig; diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 0b54171689..28ee595c46 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -2279,3 +2279,50 @@ export interface CustomStrengthOptionsAuthServerConfig { minPasswordLength?: number; maxPasswordLength?: number; } + +/** + * The email privacy configuration of a project or tenant. + */ +export interface EmailPrivacyConfig { + /** + * Whether enhanced email privacy is enabled. + */ + enableImprovedEmailPrivacy?: boolean; +} + +/** + * Defines the EmailPrivacyAuthConfig class used for validation. + * + * @internal + */ +export class EmailPrivacyAuthConfig { + public static validate(options: EmailPrivacyConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"EmailPrivacyConfig" must be a non-null object.', + ); + } + + const validKeys = { + enableImprovedEmailPrivacy: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid "EmailPrivacyConfig" parameter.`, + ); + } + } + + if (typeof options.enableImprovedEmailPrivacy !== 'undefined' + && !validator.isBoolean(options.enableImprovedEmailPrivacy)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.', + ); + } + } +} diff --git a/src/auth/index.ts b/src/auth/index.ts index 2450dd1adf..a559a706f8 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -100,8 +100,9 @@ export { UpdateRequest, TotpMultiFactorProviderConfig, PasswordPolicyConfig, - PasswordPolicyEnforcementState, + PasswordPolicyEnforcementState, CustomStrengthOptionsConfig, + EmailPrivacyConfig, } from './auth-config'; export { diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 2748be0423..250d6549ac 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -26,6 +26,8 @@ import { PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, PasswordPolicyConfig, + EmailPrivacyConfig, + EmailPrivacyAuthConfig, } from './auth-config'; import { deepCopy } from '../utils/deep-copy'; @@ -53,6 +55,10 @@ export interface UpdateProjectConfigRequest { * The password policy configuration to update on the project */ passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration to update on the project + */ + emailPrivacyConfig?: EmailPrivacyConfig; } /** @@ -63,6 +69,7 @@ export interface ProjectConfigServerResponse { mfa?: MultiFactorAuthServerConfig; recaptchaConfig?: RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; } /** @@ -73,6 +80,7 @@ export interface ProjectConfigClientRequest { mfa?: MultiFactorAuthServerConfig; recaptchaConfig?: RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; } /** @@ -91,7 +99,12 @@ export class ProjectConfig { * Supports only phone and TOTP. */ private readonly multiFactorConfig_?: MultiFactorConfig; - + /** + * The multi-factor auth configuration. + */ + get multiFactorConfig(): MultiFactorConfig | undefined { + return this.multiFactorConfig_; + } /** * The reCAPTCHA configuration to update on the project. * By enabling reCAPTCHA Enterprise integration, you are @@ -100,16 +113,14 @@ export class ProjectConfig { */ private readonly recaptchaConfig_?: RecaptchaAuthConfig; - /** - * The multi-factor auth configuration. - */ - get multiFactorConfig(): MultiFactorConfig | undefined { - return this.multiFactorConfig_; - } /** * The password policy configuration for the project */ public readonly passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration for the project + */ + public readonly emailPrivacyConfig?: EmailPrivacyConfig; /** * Validates a project config options object. Throws an error on failure. @@ -128,6 +139,7 @@ export class ProjectConfig { multiFactorConfig: true, recaptchaConfig: true, passwordPolicyConfig: true, + emailPrivacyConfig: true, } // Check for unsupported top level attributes. for (const key in request) { @@ -156,6 +168,11 @@ export class ProjectConfig { if (typeof request.passwordPolicyConfig !== 'undefined') { PasswordPolicyAuthConfig.validate(request.passwordPolicyConfig); } + + // Validate Email Privacy Config if provided. + if (typeof request.emailPrivacyConfig !== 'undefined') { + EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); + } } /** @@ -180,6 +197,9 @@ export class ProjectConfig { if (typeof configOptions.passwordPolicyConfig !== 'undefined') { request.passwordPolicyConfig = PasswordPolicyAuthConfig.buildServerRequest(configOptions.passwordPolicyConfig); } + if (typeof configOptions.emailPrivacyConfig !== 'undefined') { + request.emailPrivacyConfig = configOptions.emailPrivacyConfig; + } return request; } @@ -211,6 +231,9 @@ export class ProjectConfig { if (typeof response.passwordPolicyConfig !== 'undefined') { this.passwordPolicyConfig = new PasswordPolicyAuthConfig(response.passwordPolicyConfig); } + if (typeof response.emailPrivacyConfig !== 'undefined') { + this.emailPrivacyConfig = response.emailPrivacyConfig; + } } /** * Returns a JSON-serializable representation of this object. @@ -224,6 +247,7 @@ export class ProjectConfig { multiFactorConfig: deepCopy(this.multiFactorConfig), recaptchaConfig: this.recaptchaConfig_?.toJSON(), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), }; if (typeof json.smsRegionConfig === 'undefined') { delete json.smsRegionConfig; @@ -237,6 +261,9 @@ export class ProjectConfig { if (typeof json.passwordPolicyConfig === 'undefined') { delete json.passwordPolicyConfig; } + if (typeof json.emailPrivacyConfig === 'undefined') { + delete json.emailPrivacyConfig; + } return json; } } diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 15e941a28f..76d97f3259 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -22,8 +22,8 @@ import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig, - PasswordPolicyConfig, - PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, + PasswordPolicyConfig, + PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, EmailPrivacyConfig, EmailPrivacyAuthConfig, } from './auth-config'; /** @@ -73,6 +73,10 @@ export interface UpdateTenantRequest { * The password policy configuration for the tenant */ passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration for the tenant + */ + emailPrivacyConfig?: EmailPrivacyConfig; } /** @@ -90,6 +94,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque smsRegionConfig?: SmsRegionConfig; recaptchaConfig?: RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; } /** The tenant server response interface. */ @@ -104,6 +109,7 @@ export interface TenantServerResponse { smsRegionConfig?: SmsRegionConfig; recaptchaConfig? : RecaptchaConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; } /** @@ -165,6 +171,10 @@ export class Tenant { * The password policy configuration for the tenant */ public readonly passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration for the tenant + */ + public readonly emailPrivacyConfig?: EmailPrivacyConfig; /** * Builds the corresponding server request for a TenantOptions object. @@ -204,6 +214,9 @@ export class Tenant { if (typeof tenantOptions.passwordPolicyConfig !== 'undefined') { request.passwordPolicyConfig = PasswordPolicyAuthConfig.buildServerRequest(tenantOptions.passwordPolicyConfig); } + if (typeof tenantOptions.emailPrivacyConfig !== 'undefined') { + request.emailPrivacyConfig = tenantOptions.emailPrivacyConfig; + } return request; } @@ -240,6 +253,7 @@ export class Tenant { smsRegionConfig: true, recaptchaConfig: true, passwordPolicyConfig: true, + emailPrivacyConfig: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -299,6 +313,10 @@ export class Tenant { // This will throw an error if invalid. PasswordPolicyAuthConfig.buildServerRequest(request.passwordPolicyConfig); } + // Validate Email Privacy Config if provided. + if (typeof request.emailPrivacyConfig !== 'undefined') { + EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); + } } /** @@ -342,6 +360,9 @@ export class Tenant { if (typeof response.passwordPolicyConfig !== 'undefined') { this.passwordPolicyConfig = new PasswordPolicyAuthConfig(response.passwordPolicyConfig); } + if (typeof response.emailPrivacyConfig !== 'undefined') { + this.emailPrivacyConfig = deepCopy(response.emailPrivacyConfig); + } } /** @@ -381,6 +402,7 @@ export class Tenant { smsRegionConfig: deepCopy(this.smsRegionConfig), recaptchaConfig: this.recaptchaConfig_?.toJSON(), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), }; if (typeof json.multiFactorConfig === 'undefined') { delete json.multiFactorConfig; @@ -397,6 +419,9 @@ export class Tenant { if (typeof json.passwordPolicyConfig === 'undefined') { delete json.passwordPolicyConfig; } + if (typeof json.emailPrivacyConfig === 'undefined') { + delete json.emailPrivacyConfig; + } return json; } } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index c88fdd8722..7b113b3156 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1283,6 +1283,9 @@ describe('admin.auth', () => { ], useAccountDefender: true, }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + } }; const projectConfigOption2: UpdateProjectConfigRequest = { smsRegionConfig: smsRegionAllowlistOnlyConfig, @@ -1290,6 +1293,9 @@ describe('admin.auth', () => { emailPasswordEnforcementState: 'OFF', useAccountDefender: false, }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + } }; const projectConfigOptionSmsEnabledTotpDisabled: UpdateProjectConfigRequest = { smsRegionConfig: smsRegionAllowlistOnlyConfig, @@ -1309,6 +1315,9 @@ describe('admin.auth', () => { ], useAccountDefender: true, }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const expectedProjectConfig2: any = { smsRegionConfig: smsRegionAllowlistOnlyConfig, @@ -1323,6 +1332,7 @@ describe('admin.auth', () => { }, ], }, + emailPrivacyConfig: {}, }; const expectedProjectConfigSmsEnabledTotpDisabled: any = { smsRegionConfig: smsRegionAllowlistOnlyConfig, @@ -1337,6 +1347,7 @@ describe('admin.auth', () => { }, ], }, + emailPrivacyConfig: {}, }; it('updateProjectConfig() should resolve with the updated project config', () => { @@ -1417,6 +1428,9 @@ describe('admin.auth', () => { '+16505551234': '019287', '+16505550676': '985235', }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const expectedCreatedTenant: any = { displayName: 'testTenant1', @@ -1434,6 +1448,9 @@ describe('admin.auth', () => { '+16505551234': '019287', '+16505550676': '985235', }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const expectedUpdatedTenant: any = { displayName: 'testTenantUpdated', @@ -1459,6 +1476,7 @@ describe('admin.auth', () => { ], useAccountDefender: true, }, + emailPrivacyConfig: {}, }; const expectedUpdatedTenant2: any = { displayName: 'testTenantUpdated', @@ -1479,6 +1497,7 @@ describe('admin.auth', () => { ], useAccountDefender: false, }, + emailPrivacyConfig: {}, }; const expectedUpdatedTenantSmsEnabledTotpDisabled: any = { displayName: 'testTenantUpdated', @@ -1499,6 +1518,7 @@ describe('admin.auth', () => { ], useAccountDefender: false, }, + emailPrivacyConfig: {}, }; // https://mochajs.org/ @@ -1912,6 +1932,7 @@ describe('admin.auth', () => { multiFactorConfig: deepCopy(expectedUpdatedTenant.multiFactorConfig), testPhoneNumbers: deepCopy(expectedUpdatedTenant.testPhoneNumbers), recaptchaConfig: deepCopy(expectedUpdatedTenant.recaptchaConfig), + emailPrivacyConfig: { enableImprovedEmailPrivacy: false }, }; const updatedOptions2: UpdateTenantRequest = { emailSignInConfig: { @@ -1923,6 +1944,9 @@ describe('admin.auth', () => { testPhoneNumbers: null, smsRegionConfig: deepCopy(expectedUpdatedTenant2.smsRegionConfig), recaptchaConfig: deepCopy(expectedUpdatedTenant2.recaptchaConfig), + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, }; if (authEmulatorHost) { return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions) diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index eb32cae88e..5934dd15fd 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -67,6 +67,9 @@ describe('ProjectConfig', () => { }, ], }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const updateProjectConfigRequest1: UpdateProjectConfigRequest = { @@ -87,6 +90,9 @@ describe('ProjectConfig', () => { maxLength: 30, }, }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, }; const updateProjectConfigRequest2: UpdateProjectConfigRequest = { @@ -421,6 +427,30 @@ describe('ProjectConfig', () => { ' must be greater than or equal to minLength and at max 4096.'); }); + it('should throw on null EmailPrivacyConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.emailPrivacyConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"EmailPrivacyConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailPrivacyConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.emailPrivacyConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid "EmailPrivacyConfig" parameter.'); + }); + + it('should throw on invalid enableImprovedEmailPrivacy attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.emailPrivacyConfig.enableImprovedEmailPrivacy = []; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { @@ -505,6 +535,13 @@ describe('ProjectConfig', () => { }; expect(projectConfig.passwordPolicyConfig).to.deep.equal(expectedPasswordPolicyConfig); }); + + it('should set readonly property emailPrivacyConfig', () => { + const expectedEmailPrivacyConfig = { + enableImprovedEmailPrivacy: true, + }; + expect(projectConfig.emailPrivacyConfig).to.deep.equal(expectedEmailPrivacyConfig); + }); }); describe('toJSON()', () => { @@ -515,6 +552,7 @@ describe('ProjectConfig', () => { multiFactorConfig: deepCopy(serverResponse.mfa), recaptchaConfig: deepCopy(serverResponse.recaptchaConfig), passwordPolicyConfig: deepCopy(serverResponse.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(serverResponse.emailPrivacyConfig), }); }); @@ -526,6 +564,8 @@ describe('ProjectConfig', () => { delete serverResponseOptionalCopy.recaptchaConfig?.managedRules; delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender; delete serverResponseOptionalCopy.passwordPolicyConfig; + delete serverResponseOptionalCopy.passwordPolicyConfig; + delete serverResponseOptionalCopy.emailPrivacyConfig; 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 0d4d9e8a90..e1006e47b7 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaAuthConfig, +import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaAuthConfig, PasswordPolicyAuthServerConfig, PasswordPolicyConfig, } from '../../../src/auth/auth-config'; import { TenantServerResponse } from '../../../src/auth/tenant'; @@ -100,6 +100,9 @@ describe('Tenant', () => { }, smsRegionConfig: smsAllowByDefault, passwordPolicyConfig: passwordPolicyServerConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const clientRequest: UpdateTenantRequest = { @@ -126,6 +129,9 @@ describe('Tenant', () => { }, smsRegionConfig: smsAllowByDefault, passwordPolicyConfig: passwordPolicyClientConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const serverRequestWithoutMfa: TenantServerResponse = { @@ -134,6 +140,9 @@ describe('Tenant', () => { allowPasswordSignup: true, enableEmailLinkSignin: true, passwordPolicyConfig: passwordPolicyServerConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const clientRequestWithoutMfa: UpdateTenantRequest = { @@ -143,6 +152,9 @@ describe('Tenant', () => { passwordRequired: false, }, passwordPolicyConfig: passwordPolicyClientConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; const clientRequestWithRecaptcha: UpdateTenantRequest = { @@ -203,6 +215,10 @@ describe('Tenant', () => { useAccountDefender: true, }, smsRegionConfig: smsAllowByDefault, + passwordPolicyConfig: passwordPolicyServerConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, }; describe('buildServerRequest()', () => { @@ -544,6 +560,30 @@ describe('Tenant', () => { ' must be greater than or equal to minLength and at max 4096.'); }); + it('should throw on null EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"EmailPrivacyConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid "EmailPrivacyConfig" parameter.'); + }); + + it('should throw on invalid enableImprovedEmailPrivacy attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.enableImprovedEmailPrivacy = []; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); + }); + it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha); expect(() => { @@ -915,6 +955,30 @@ describe('Tenant', () => { ' must be greater than or equal to minLength and at max 4096.'); }); + it('should throw on null EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"EmailPrivacyConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid "EmailPrivacyConfig" parameter.'); + }); + + it('should throw on invalid enableImprovedEmailPrivacy attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.enableImprovedEmailPrivacy = []; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -1034,6 +1098,13 @@ describe('Tenant', () => { deepCopy(clientRequest.passwordPolicyConfig)); }); + it('should set readonly property emailPrivacyConfig', () => { + const expectedEmailPrivacyConfig = { + enableImprovedEmailPrivacy: true, + }; + expect(clientRequest.emailPrivacyConfig).to.deep.equal(expectedEmailPrivacyConfig); + }); + it('should throw when no tenant ID is provided', () => { const invalidOptions = deepCopy(serverRequest); // Use resource name that does not include a tenant ID. @@ -1074,6 +1145,8 @@ describe('Tenant', () => { testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), smsRegionConfig: deepCopy(clientRequest.smsRegionConfig), recaptchaConfig: deepCopy(serverResponseWithRecaptcha.recaptchaConfig), + passwordPolicyConfig: deepCopy(clientRequest.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(clientRequest.emailPrivacyConfig), }); }); @@ -1084,6 +1157,7 @@ describe('Tenant', () => { delete serverRequestCopyWithoutMfa.smsRegionConfig; delete serverRequestCopyWithoutMfa.recaptchaConfig; delete serverRequestCopyWithoutMfa.passwordPolicyConfig; + delete serverRequestCopyWithoutMfa.emailPrivacyConfig; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', displayName: 'TENANT-DISPLAY-NAME',