diff --git a/packages/functional-tests/pages/index.ts b/packages/functional-tests/pages/index.ts index cf0550cce8c..c8ab3ac67b8 100644 --- a/packages/functional-tests/pages/index.ts +++ b/packages/functional-tests/pages/index.ts @@ -20,6 +20,7 @@ import { SubscriptionManagementPage } from './products/subscriptionManagement'; import { ResetPasswordPage } from './resetPassword'; import { LegalPage } from './legal'; import { CookiesDisabledPage } from './cookiesDisabled'; +import { PostVerifyPage } from './postVerify'; export function create(page: Page, target: BaseTarget) { return { @@ -44,5 +45,6 @@ export function create(page: Page, target: BaseTarget) { resetPassword: new ResetPasswordPage(page, target), legal: new LegalPage(page, target), cookiesDisabled: new CookiesDisabledPage(page, target), + postVerify: new PostVerifyPage(page, target), }; } diff --git a/packages/functional-tests/pages/login.ts b/packages/functional-tests/pages/login.ts index c8d08e55be7..1c7dc80373e 100644 --- a/packages/functional-tests/pages/login.ts +++ b/packages/functional-tests/pages/login.ts @@ -527,4 +527,8 @@ export class LoginPage extends BaseLayout { const config = await metaConfig.getAttribute('content'); return JSON.parse(decodeURIComponent(config)); } + + async clearEmailTextBox() { + return this.page.locator(selectors.EMAIL).fill(''); + } } diff --git a/packages/functional-tests/pages/postVerify.ts b/packages/functional-tests/pages/postVerify.ts new file mode 100644 index 00000000000..630adf117fe --- /dev/null +++ b/packages/functional-tests/pages/postVerify.ts @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { BaseLayout } from './layout'; + +export class PostVerifyPage extends BaseLayout { + readonly path = ''; + readonly selectors = { + ACCOUNT_RECOVERY_HEADER: '#fxa-add-account-recovery-header', + ADD_RECOVERY_KEY: 'button[type="submit"]', + OLD_PASSWRD: '#opassword', + PASSWORD: '#password', + CONFIRM_PASSWORD: '#vpassword', + SUBMIT: '#submit-btn', + RECOVERY_KEY_TEXT: '.recovery-key', + RECOVERY_KEY_INPUT: '#recovery-key', + DONE: '.primary-button', + TOOLTIP: '.tooltip', + RECOVERY_KEY_VERIFIED_HEADER: '#fxa-account-recovery-complete-header', + CLICK_MAYBE_LATER: '#maybe-later-btn', + FORCE_PASSWORD_CHANGE_HEADER: '#fxa-force-password-change-header', + }; + + async isAccountRecoveryHeader() { + const header = this.page.locator(this.selectors.ACCOUNT_RECOVERY_HEADER); + await header.waitFor(); + return header.isVisible(); + } + + async isAccountRecoveryVerifiedHeader() { + const header = this.page.locator( + this.selectors.RECOVERY_KEY_VERIFIED_HEADER + ); + await header.waitFor(); + return header.isVisible(); + } + + async isForcePasswordChangeHeader() { + const header = this.page.locator( + this.selectors.FORCE_PASSWORD_CHANGE_HEADER + ); + await header.waitFor(); + return header.isVisible(); + } + + async addRecoveryKey(password) { + return Promise.all([ + this.page.locator(this.selectors.ADD_RECOVERY_KEY).click(), + this.page.locator(this.selectors.PASSWORD).fill(password), + ]); + } + + async getKey() { + return this.page.innerText(this.selectors.RECOVERY_KEY_TEXT); + } + + async submit() { + await this.page.locator(this.selectors.SUBMIT).click(); + } + + async clickDone() { + await this.page.locator(this.selectors.DONE).click(); + } + + async clickMaybeLater() { + await this.page.locator(this.selectors.CLICK_MAYBE_LATER).click(); + } + + async inputRecoveryKey(key) { + await this.page.locator(this.selectors.RECOVERY_KEY_INPUT).fill(key); + await this.submit(); + } + + async getTooltipError() { + return this.page.locator(this.selectors.TOOLTIP).innerText(); + } + + async fillOutChangePassword(oldPassword, newPassword) { + await this.page.locator(this.selectors.OLD_PASSWRD).fill(oldPassword); + await this.page.locator(this.selectors.PASSWORD).fill(newPassword); + await this.page.locator(this.selectors.CONFIRM_PASSWORD).fill(newPassword); + } +} diff --git a/packages/functional-tests/pages/relier.ts b/packages/functional-tests/pages/relier.ts index 4b6062cd36a..621ca356453 100644 --- a/packages/functional-tests/pages/relier.ts +++ b/packages/functional-tests/pages/relier.ts @@ -15,6 +15,12 @@ export class RelierPage extends BaseLayout { return login.isVisible(); } + async isOauthSuccessHeader() { + const header = this.page.locator('#fxa-oauth-success-header'); + await header.waitFor(); + return header.isVisible(); + } + async isPro() { const pro = this.page.locator('.pro-status'); await pro.waitFor({ state: 'visible' }); diff --git a/packages/functional-tests/tests/oauth/signUp.spec.ts b/packages/functional-tests/tests/oauth/signUp.spec.ts new file mode 100644 index 00000000000..b0368138dae --- /dev/null +++ b/packages/functional-tests/tests/oauth/signUp.spec.ts @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { test, expect } from '../../lib/fixtures/standard'; + +let email; +let bouncedEmail; +const password = 'passwordzxcv'; + +test.describe('Oauth sign up', () => { + test.beforeEach(async ({ pages: { login } }) => { + test.slow(); + email = login.createEmail(); + bouncedEmail = login.createEmail('bounced{id}'); + await login.clearCache(); + }); + + test.afterEach(async ({ target }) => { + if (email) { + // Cleanup any accounts created during the test + await target.auth.accountDestroy(email, password); + } + }); + + test('sign up', async ({ pages: { login, relier } }) => { + await relier.goto(); + await relier.clickEmailFirst(); + await login.fillOutFirstSignUp(email, password, false); + + //Verify sign up code header + expect(await login.isSignUpCodeHeader()).toBe(true); + await login.fillOutSignUpCode(email); + + //Verify logged in on relier page + expect(await relier.isLoggedIn()).toBe(true); + }); + + test('signup, bounce email, allow user to restart flow but force a different email', async ({ + target, + pages: { login, relier }, + }) => { + const client = await login.getFxaClient(target); + + await relier.goto(); + await relier.clickEmailFirst(); + await login.fillOutFirstSignUp(bouncedEmail, password, false); + + //Verify sign up code header + expect(await login.isSignUpCodeHeader()).toBe(true); + await client.accountDestroy(bouncedEmail, password); + + //Verify error message + expect(await login.getTooltipError()).toContain( + 'Your confirmation email was just returned. Mistyped email?' + ); + + await login.clearEmailTextBox(); + await login.fillOutFirstSignUp(email, password, false); + + //Verify sign up code header + expect(await login.isSignUpCodeHeader()).toBe(true); + await login.fillOutSignUpCode(email); + + //Verify logged in on relier page + expect(await relier.isLoggedIn()).toBe(true); + }); +}); + +test.describe('Oauth sign up success', () => { + test.beforeEach(async ({ pages: { login } }) => { + test.slow(); + await login.clearCache(); + }); + + test('a success screen is available', async ({ + target, + page, + pages: { relier }, + }) => { + await page.goto( + `${target.contentServerUrl}/oauth/success/dcdb5ae7add825d2` + ); + + //Verify oauth success header + expect(await relier.isOauthSuccessHeader()).toBe(true); + }); +}); diff --git a/packages/functional-tests/tests/postVerify/accountRecovery.spec.ts b/packages/functional-tests/tests/postVerify/accountRecovery.spec.ts new file mode 100644 index 00000000000..b33018f4b67 --- /dev/null +++ b/packages/functional-tests/tests/postVerify/accountRecovery.spec.ts @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { test, expect } from '../../lib/fixtures/standard'; +const password = 'passwordzxcv'; +let email; + +test.describe('post verify - account recovery', () => { + test.beforeEach(async ({ target, pages: { login } }) => { + // Generating and consuming recovery keys is a slow process + test.slow(); + email = login.createEmail(); + await target.auth.signUp(email, password, { + lang: 'en', + preVerified: 'true', + }); + await login.clearCache(); + }); + + test.afterEach(async ({ target }) => { + if (email) { + // Cleanup any accounts created during the test + try { + await target.auth.accountDestroy(email, password); + } catch (e) { + // Handle the error here + console.error('An error occurred during account cleanup:', e); + // Optionally, rethrow the error to propagate it further + throw e; + } + } + }); + + test('create account recovery', async ({ + target, + pages: { page, login, postVerify }, + }) => { + await page.goto(target.contentServerUrl, { + waitUntil: 'load', + }); + await login.fillOutEmailFirstSignIn(email, password); + + //Verify logged in on Settings page + expect(await login.loginHeader()).toBe(true); + + await page.goto( + `${target.contentServerUrl}/post_verify/account_recovery/add_recovery_key`, + { waitUntil: 'networkidle' } + ); + + //Verify account recovery header + expect(await postVerify.isAccountRecoveryHeader()).toBe(true); + + //Add recovery key + await postVerify.addRecoveryKey(password); + await postVerify.submit(); + + // Store key to be used later + const key = await postVerify.getKey(); + await postVerify.clickDone(); + + //Enter invalid key + await postVerify.inputRecoveryKey('invalid key'); + + //Verify error message + expect(await postVerify.getTooltipError()).toContain( + 'Invalid account recovery key' + ); + + //Enter correct key + await postVerify.inputRecoveryKey(key); + + //Verify post verify account recovery key complete header + expect(await postVerify.isAccountRecoveryVerifiedHeader()).toBe(true); + }); + + test('abort account recovery at add_recovery_key', async ({ + target, + pages: { page, login, postVerify }, + }) => { + await page.goto(target.contentServerUrl, { + waitUntil: 'load', + }); + await login.fillOutEmailFirstSignIn(email, password); + + //Verify logged in on Settings page + expect(await login.loginHeader()).toBe(true); + + await page.goto( + `${target.contentServerUrl}/post_verify/account_recovery/add_recovery_key`, + { waitUntil: 'networkidle' } + ); + + //Verify account recovery header + expect(await postVerify.isAccountRecoveryHeader()).toBe(true); + + //Add recovery key + await postVerify.clickMaybeLater(); + + //Verify logged in on Settings page + expect(await login.loginHeader()).toBe(true); + }); + + test('abort account recovery at confirm_recovery_key', async ({ + target, + pages: { page, login, postVerify }, + }) => { + await page.goto(target.contentServerUrl, { + waitUntil: 'load', + }); + await login.fillOutEmailFirstSignIn(email, password); + + //Verify logged in on Settings page + expect(await login.loginHeader()).toBe(true); + + await page.goto( + `${target.contentServerUrl}/post_verify/account_recovery/add_recovery_key`, + { waitUntil: 'networkidle' } + ); + + //Verify account recovery header + expect(await postVerify.isAccountRecoveryHeader()).toBe(true); + + //Add recovery key + await postVerify.addRecoveryKey(password); + await postVerify.clickMaybeLater(); + + //Verify logged in on Settings page + expect(await login.loginHeader()).toBe(true); + }); +}); diff --git a/packages/functional-tests/tests/postVerify/forcePasswordChange.spec.ts b/packages/functional-tests/tests/postVerify/forcePasswordChange.spec.ts new file mode 100644 index 00000000000..1753c41ea7b --- /dev/null +++ b/packages/functional-tests/tests/postVerify/forcePasswordChange.spec.ts @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { test, expect } from '../../lib/fixtures/standard'; +let email; +const password = 'password'; +const newPassword = 'new_password'; + +test.describe('post verify - force password change', () => { + test.beforeEach(async ({ target, pages: { login } }) => { + test.slow(); + email = login.createEmail('forcepwdchange{id}'); + await target.auth.signUp(email, password, { + lang: 'en', + preVerified: 'true', + }); + await login.clearCache(); + }); + + test.afterEach(async ({ target }) => { + if (email) { + // Cleanup any accounts created during the test + try { + await target.auth.accountDestroy(email, newPassword); + } catch (e) { + // Handle the error here + console.error('An error occurred during account cleanup:', e); + // Optionally, rethrow the error to propagate it further + throw e; + } + } + }); + + test('navigate to page directly and can change password', async ({ + target, + pages: { page, login, postVerify }, + }) => { + await page.goto(target.contentServerUrl, { + waitUntil: 'load', + }); + await login.fillOutEmailFirstSignIn(email, password); + await login.fillOutSignInCode(email); + + //Verify force password change header + expect(await postVerify.isForcePasswordChangeHeader()).toBe(true); + + //Fill out change password + await postVerify.fillOutChangePassword(password, newPassword); + await postVerify.submit(); + + //Verify logged in on Settings page + expect(await login.loginHeader()).toBe(true); + }); + + test('force change password on login - oauth', async ({ + pages: { login, postVerify, relier }, + }) => { + await relier.goto(); + await relier.clickEmailFirst(); + await login.fillOutEmailFirstSignIn(email, password); + await login.fillOutSignInCode(email); + + //Verify force password change header + expect(await postVerify.isForcePasswordChangeHeader()).toBe(true); + + //Fill out change password + await postVerify.fillOutChangePassword(password, newPassword); + await postVerify.submit(); + + //Verify logged in on relier page + expect(await relier.isLoggedIn()).toBe(true); + }); +}); diff --git a/packages/functional-tests/tests/postVerify/syncForcePasswordChange.spec.ts b/packages/functional-tests/tests/postVerify/syncForcePasswordChange.spec.ts new file mode 100644 index 00000000000..c930104f417 --- /dev/null +++ b/packages/functional-tests/tests/postVerify/syncForcePasswordChange.spec.ts @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { test, expect, newPagesForSync } from '../../lib/fixtures/standard'; +let email; +const password = 'password'; +const newPassword = 'new_password'; +let syncBrowserPages; + +test.describe('post verify - force password change sync', () => { + test.beforeEach(async ({ target }) => { + syncBrowserPages = await newPagesForSync(target); + const { login } = syncBrowserPages; + email = login.createEmail('forcepwdchange{id}'); + await target.auth.signUp(email, password, { + lang: 'en', + preVerified: 'true', + }); + }); + + test.afterEach(async ({ target }) => { + await syncBrowserPages.browser?.close(); + if (email) { + // Cleanup any accounts created during the test + try { + await target.auth.accountDestroy(email, newPassword); + } catch (e) { + // Handle the error here + console.error('An error occurred during account cleanup:', e); + // Optionally, rethrow the error to propagate it further + throw e; + } + } + }); + + test('force change password on login - sync', async ({ target }) => { + const { page, login, postVerify, connectAnotherDevice } = syncBrowserPages; + await page.goto( + `${target.contentServerUrl}?context=fx_desktop_v3&service=sync`, + { + waitUntil: 'load', + } + ); + await login.fillOutEmailFirstSignIn(email, password); + await login.fillOutSignInCode(email); + + //Verify force password change header + expect(await postVerify.isForcePasswordChangeHeader()).toBe(true); + + //Fill out change password + await postVerify.fillOutChangePassword(password, newPassword); + await postVerify.submit(); + + //Verify logged in on connect another device page + expect(await connectAnotherDevice.fxaConnected.isEnabled()).toBeTruthy(); + }); +});