From d9e456b66866d9ee3663456de74cd5f271cb7e63 Mon Sep 17 00:00:00 2001 From: Pavan Shankar Date: Tue, 29 Oct 2024 10:01:04 +0530 Subject: [PATCH 1/5] Add integration tests for changes related to FDL deprecation --- test/integration/auth.spec.ts | 163 +++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 7b113b3156..d289927662 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -36,6 +36,7 @@ import { } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; +import { ActionCodeSettings } from '../../src/auth/action-code-settings-builder'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -75,10 +76,18 @@ const mockUserData = { photoURL: 'http://www.example.com/' + newUserUid + '/photo.png', disabled: false, }; -const actionCodeSettings = { +const actionCodeSettings : ActionCodeSettings = { url: 'http://localhost/?a=1&b=2#c=3', handleCodeInApp: false, }; +const actionCodeSettingsWithCustomDomain: ActionCodeSettings = { + url: 'http://localhost/?a=1&b=2#c=3', + handleCodeInApp: true, + linkDomain: 'liubin-dev2.testdomaindonotuse.com', + iOS: { + bundleId: 'com.google.experimental1.dev.extension0', + }, +} let deleteQueue = Promise.resolve(); interface UserImportTest { @@ -1199,6 +1208,114 @@ describe('admin.auth', () => { expect(result.user!.emailVerified).to.be.true; }); }); + + it('generateSignInWithEmailLink() with custom linkDomain should return error in case of invalid hosting domain', + function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + const actionCodeSettingsWithInvalidLinkDomain = deepCopy(actionCodeSettings); + actionCodeSettingsWithInvalidLinkDomain.linkDomain = 'invaliddomain.firebaseapp.com'; + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsWithInvalidLinkDomain) + .catch((error) => { + expect(error.code).to.equal('auth/invalid-hosting-link-domain'); + }); + }); + + it('generatePasswordResetLink() should return a password reset link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure old password set on created user. + return getAuth().updateUser(uid, { password: 'password' }) + .then(() => { + return getAuth().generatePasswordResetLink(email, actionCodeSettingsWithCustomDomain); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().confirmPasswordReset(code, newPassword); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, newPassword); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + // Password reset also verifies the user's email. + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateEmailVerificationLink() should return a verification link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is unverified. + return getAuth().updateUser(uid, { password: '123456', emailVerified: false }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.false; + return getAuth().generateEmailVerificationLink(email, actionCodeSettingsWithCustomDomain); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, userData.password); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateSignInWithEmailLink() should return a sign-in link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsWithCustomDomain) + .then((link) => { + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettingsWithCustomDomain.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().signInWithEmailLink(email, link); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateVerifyAndChangeEmailLink() should return a verification link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is verified. + return getAuth().updateUser(uid, { password: '123456', emailVerified: true }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + return getAuth().generateVerifyAndChangeEmailLink(email, newEmail, actionCodeSettingsWithCustomDomain); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(newEmail, 'password'); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(newEmail); + expect(result.user!.emailVerified).to.be.true; + }); + }); }); describe('Project config management operations', () => { @@ -1285,6 +1402,9 @@ describe('admin.auth', () => { }, emailPrivacyConfig: { enableImprovedEmailPrivacy: true, + }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', } }; const projectConfigOption2: UpdateProjectConfigRequest = { @@ -1318,6 +1438,9 @@ describe('admin.auth', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, }; const expectedProjectConfig2: any = { smsRegionConfig: smsRegionAllowlistOnlyConfig, @@ -1333,6 +1456,9 @@ describe('admin.auth', () => { ], }, emailPrivacyConfig: {}, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, }; const expectedProjectConfigSmsEnabledTotpDisabled: any = { smsRegionConfig: smsRegionAllowlistOnlyConfig, @@ -1348,6 +1474,9 @@ describe('admin.auth', () => { ], }, emailPrivacyConfig: {}, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, }; it('updateProjectConfig() should resolve with the updated project config', () => { @@ -3194,6 +3323,38 @@ function getContinueUrl(link: string): string { return continueUrl!; } +/** + * Returns the host name corresponding to the link. + * + * @param link The link to parse for hostname + * @returns Hostname in the link + */ +function getHostName(link: string): string { + const parsedUrl = new url.URL(link); + return parsedUrl.hostname; +} + +/** + * Returns continue URL for handling in app requests. + * URL will be of the form, http://abc/__/auth/link?link= + * Coninue URL will be part of action link url + * + * @param link + * @returns + */ +function getContinueUrlForInAppRequest(link: string): string { + // Extract action url from link param + const parsedUrl = new url.URL(link); + const linkParam = parsedUrl.searchParams.get('link') ?? ''; + expect(linkParam).is.not.empty; + + // Extract continueUrl param from action url + const actionUrl = new url.URL(linkParam); + const continueUrl = actionUrl.searchParams.get('continueUrl'); + expect(continueUrl).to.exist; + return continueUrl!; +} + /** * Returns the tenant ID corresponding to the link. * From 28096d4a52059f4e2e15ff5609b86043cb3d124f Mon Sep 17 00:00:00 2001 From: Pavan Shankar Date: Wed, 30 Oct 2024 14:59:06 +0530 Subject: [PATCH 2/5] Add additional tests --- test/integration/auth.spec.ts | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index d289927662..745ca4496f 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -88,6 +88,13 @@ const actionCodeSettingsWithCustomDomain: ActionCodeSettings = { bundleId: 'com.google.experimental1.dev.extension0', }, } +const actionCodeSettingsForFdlLinks: ActionCodeSettings = { + url: 'http://localhost/?a=1&b=2#c=3', + handleCodeInApp: true, + iOS: { + bundleId: 'com.google.experimental1.dev.extension0', + }, +} let deleteQueue = Promise.resolve(); interface UserImportTest { @@ -1114,6 +1121,13 @@ describe('admin.auth', () => { // Create the test user before running this suite of tests. before(() => { + // Update project config to have HOSTING_DOMAIN as mobileLinksConfig after each test + const updateMobileLinksRequest: UpdateProjectConfigRequest = { + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, + }; + getAuth().projectConfigManager().updateProjectConfig(updateMobileLinksRequest); return getAuth().createUser(userData); }); @@ -1316,6 +1330,54 @@ describe('admin.auth', () => { expect(result.user!.emailVerified).to.be.true; }); }); + + it('generateSignInWithEmailLink() should return a FDL sign-in' + + 'link with mobileLinksConfig set to FIREBASE_DYNAMIC_LINK_DOMAIN', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + + const updateMobileLinksRequest: UpdateProjectConfigRequest = { + mobileLinksConfig: { + domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', + } + }; + return getAuth().projectConfigManager().updateProjectConfig(updateMobileLinksRequest) + .then((projectConfig) => { + expect(projectConfig?.mobileLinksConfig?.domain).equal('FIREBASE_DYNAMIC_LINK_DOMAIN'); + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsForFdlLinks); + }).then((link) => { + expectFDLLink(link); + return clientAuth().signInWithEmailLink(email, link); + }).then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateSignInWithEmailLink() should return a FDL sign-in link with empty mobileLinksConfig', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + + const updateMobileLinksRequest: UpdateProjectConfigRequest = { + mobileLinksConfig: { + } + }; + return getAuth().projectConfigManager().updateProjectConfig(updateMobileLinksRequest) + .then((projectConfig) => { + expect(projectConfig.mobileLinksConfig).is.empty; + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsForFdlLinks); + }).then((link) => { + expectFDLLink(link); + return clientAuth().signInWithEmailLink(email, link); + }).then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); }); describe('Project config management operations', () => { @@ -3339,7 +3401,7 @@ function getHostName(link: string): string { * URL will be of the form, http://abc/__/auth/link?link= * Coninue URL will be part of action link url * - * @param link + * @param link The link to parse for continue url * @returns */ function getContinueUrlForInAppRequest(link: string): string { @@ -3355,6 +3417,21 @@ function getContinueUrlForInAppRequest(link: string): string { return continueUrl!; } +/** + * Verify if the generated link is generated by FDL + * We leverage the params created by FDL to test whether a given link is FDL + * + * @param link Link to check whether it is FDL + */ +function expectFDLLink(link: string): void { + const parsedUrl = new url.URL(link); + // For ios, FDL creates a fallback url with param ifl + // We leverage that to test whether a given link is FDL link + // Note: This param does not exist when the link is generated for HOSTING_DOMAIN + const iflParam = parsedUrl.searchParams.get('ifl'); + expect(iflParam).is.not.null; +} + /** * Returns the tenant ID corresponding to the link. * From f4f24a74da1895442317cc2986155c0179b0fb86 Mon Sep 17 00:00:00 2001 From: Pavan Shankar Date: Wed, 30 Oct 2024 15:04:23 +0530 Subject: [PATCH 3/5] Add additional tests --- test/integration/auth.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 745ca4496f..24a8becbbc 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -85,14 +85,14 @@ const actionCodeSettingsWithCustomDomain: ActionCodeSettings = { handleCodeInApp: true, linkDomain: 'liubin-dev2.testdomaindonotuse.com', iOS: { - bundleId: 'com.google.experimental1.dev.extension0', + bundleId: 'testBundleId', }, } const actionCodeSettingsForFdlLinks: ActionCodeSettings = { url: 'http://localhost/?a=1&b=2#c=3', handleCodeInApp: true, iOS: { - bundleId: 'com.google.experimental1.dev.extension0', + bundleId: 'testBundleId', }, } let deleteQueue = Promise.resolve(); @@ -1336,7 +1336,7 @@ describe('admin.auth', () => { if (authEmulatorHost) { return this.skip(); // Not yet supported in Auth Emulator. } - + const updateMobileLinksRequest: UpdateProjectConfigRequest = { mobileLinksConfig: { domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', From ce3fefee9e1c21a525fe8436dfb2f08fbadc51e4 Mon Sep 17 00:00:00 2001 From: Pavan Shankar Date: Wed, 30 Oct 2024 20:38:37 +0530 Subject: [PATCH 4/5] Update linkDomain to the CI project domain --- test/integration/auth.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 24a8becbbc..ccda0b592d 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -83,7 +83,7 @@ const actionCodeSettings : ActionCodeSettings = { const actionCodeSettingsWithCustomDomain: ActionCodeSettings = { url: 'http://localhost/?a=1&b=2#c=3', handleCodeInApp: true, - linkDomain: 'liubin-dev2.testdomaindonotuse.com', + linkDomain: 'kobayashimaru.testdomaindonotuse.com', iOS: { bundleId: 'testBundleId', }, From 543bc3f4cbc24791c014305540cb7e65ab5d83bf Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Wed, 30 Oct 2024 12:05:39 -0700 Subject: [PATCH 5/5] update the action-code-settings integ tests import --- test/integration/auth.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index ccda0b592d..0b2b97d310 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,11 +32,10 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig, + PasswordPolicyConfig, SmsRegionConfig, ActionCodeSettings, } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; -import { ActionCodeSettings } from '../../src/auth/action-code-settings-builder'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires