From 7a94d3fbffbf7d758d25c8bc7f16be0dcc50441c Mon Sep 17 00:00:00 2001 From: Lauren Zugai Date: Mon, 5 Jun 2023 19:17:16 -0500 Subject: [PATCH] feat(react): Send fxaLogin webchannel message conditionally after password reset Because: * The browser should be be notified when a PW reset occurs on a verified account through the Sync flow, as this logs the user in This commit: * Conditionally runs the fxaLogin command with required account data after a password reset with or without recovery key, when the flow is sync * Receives other values off the account reset call needed for webchannel account data * Temporarily checks for SyncBasic and SyncDesktop flows to run the check because we don't need to port over context in local storage logic (which would account for SyncDesktop only), since we are switching to codes soon * Sends up `service` with the email request so we can append the param and check when link is received (causing the SyncBasic integration) * Removes resendResetPassword and calls resetPassword where needed since the code was duplicated Fixes FXA-7172 --- .../src/gql/dto/payload/password-forgot.ts | 3 + .../LinkExpiredResetPassword/index.test.tsx | 47 +++++--- .../LinkExpiredResetPassword/index.tsx | 6 +- .../fxa-settings/src/lib/channels/helpers.ts | 109 +++++------------- .../fxa-settings/src/lib/storybook-utils.ts | 29 +++++ packages/fxa-settings/src/models/Account.ts | 73 ++++-------- .../index.stories.tsx | 57 +++++---- .../AccountRecoveryConfirmKey/index.test.tsx | 7 +- .../AccountRecoveryConfirmKey/mocks.tsx | 72 +++++++----- .../index.test.tsx | 8 +- .../AccountRecoveryResetPassword/index.tsx | 20 +++- .../AccountRecoveryResetPassword/mocks.tsx | 7 ++ .../CompleteResetPassword/index.stories.tsx | 65 ++++++----- .../CompleteResetPassword/index.test.tsx | 24 ++-- .../CompleteResetPassword/index.tsx | 18 ++- .../CompleteResetPassword/mocks.tsx | 80 ++++++++----- .../ConfirmResetPassword/index.stories.tsx | 21 +--- .../ConfirmResetPassword/index.test.tsx | 53 ++++++--- .../ConfirmResetPassword/index.tsx | 6 +- .../index.stories.tsx | 45 +++++--- .../index.tsx | 2 +- .../src/pages/ResetPassword/index.stories.tsx | 70 +++++------ .../src/pages/ResetPassword/index.test.tsx | 6 +- .../src/pages/ResetPassword/index.tsx | 2 +- 24 files changed, 442 insertions(+), 388 deletions(-) create mode 100644 packages/fxa-settings/src/lib/storybook-utils.ts diff --git a/packages/fxa-graphql-api/src/gql/dto/payload/password-forgot.ts b/packages/fxa-graphql-api/src/gql/dto/payload/password-forgot.ts index fbaf56333b8..44b47c64a5c 100644 --- a/packages/fxa-graphql-api/src/gql/dto/payload/password-forgot.ts +++ b/packages/fxa-graphql-api/src/gql/dto/payload/password-forgot.ts @@ -64,4 +64,7 @@ export class AccountResetPayload { @Field({ nullable: true }) public keyFetchToken?: string; + + @Field({ nullable: true }) + public unwrapBKey?: string; } diff --git a/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.test.tsx b/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.test.tsx index 416acdad2ff..54757183785 100644 --- a/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.test.tsx +++ b/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.test.tsx @@ -3,11 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import { LocationProvider } from '@reach/router'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import { LinkExpiredResetPassword } from '.'; -import { mockAppContext, MOCK_ACCOUNT } from '../../models/mocks'; -import { Account, AppContext } from '../../models'; +import { + mockAppContext, + MOCK_ACCOUNT, + createHistoryWithQuery, + renderWithRouter, + createAppContext, +} from '../../models/mocks'; +import { Account } from '../../models'; import { FIREFOX_NOREPLY_EMAIL } from 'fxa-settings/src/constants'; const viewName = 'example-view-name'; @@ -17,25 +22,31 @@ jest.mock('@reach/router', () => ({ ...jest.requireActual('@reach/router'), })); -function renderLinkExpiredResetPasswordWithAccount(account: Account) { - render( - - - - - +const route = '/bloop'; +const renderWithHistory = (ui: any, queryParams = '', account?: Account) => { + const history = createHistoryWithQuery(route, queryParams); + return renderWithRouter( + ui, + { + route, + history, + }, + mockAppContext({ + ...createAppContext(history), + ...(account && { account }), + }) ); -} +}; describe('LinkExpiredResetPassword', () => { - const account = {} as unknown as Account; + const component = ; afterEach(() => { jest.clearAllMocks(); }); it('renders the component as expected for an expired Reset Password link', () => { - renderLinkExpiredResetPasswordWithAccount(account); + renderWithHistory(component); screen.getByRole('heading', { name: 'Reset password link expired', @@ -47,10 +58,10 @@ describe('LinkExpiredResetPassword', () => { }); it('displays a success banner when clicking on "receive a new link" is successful', async () => { const account = { - resendResetPassword: jest.fn().mockResolvedValue(true), + resetPassword: jest.fn().mockResolvedValue(true), } as unknown as Account; - renderLinkExpiredResetPasswordWithAccount(account); + renderWithHistory(component, '', account); const receiveNewLinkButton = screen.getByRole('button', { name: 'Receive new link', }); @@ -65,10 +76,10 @@ describe('LinkExpiredResetPassword', () => { }); it('displays an error banner when clicking on "receive a new link" is unsuccessful', async () => { const account = { - resendResetPassword: jest.fn().mockRejectedValue('error'), + resetPassword: jest.fn().mockRejectedValue('error'), } as unknown as Account; - renderLinkExpiredResetPasswordWithAccount(account); + renderWithHistory(component, '', account); const receiveNewLinkButton = screen.getByRole('button', { name: 'Receive new link', }); diff --git a/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.tsx b/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.tsx index f6abdc51409..ab95d12b77c 100644 --- a/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.tsx +++ b/packages/fxa-settings/src/components/LinkExpiredResetPassword/index.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React, { useState } from 'react'; -import { useAccount } from '../../models'; +import { CreateRelier, useAccount } from '../../models'; import { ResendStatus } from '../../lib/types'; import { logViewEvent } from 'fxa-settings/src/lib/metrics'; import { REACT_ENTRYPOINT } from 'fxa-settings/src/constants'; @@ -20,6 +20,8 @@ export const LinkExpiredResetPassword = ({ }: LinkExpiredResetPasswordProps) => { // TODO in FXA-7630 add metrics event and associated tests for users hitting the LinkExpired page const account = useAccount(); + const relier = CreateRelier(); + const serviceName = relier.getServiceName(); const [resendStatus, setResendStatus] = useState( ResendStatus['not sent'] @@ -27,7 +29,7 @@ export const LinkExpiredResetPassword = ({ const resendResetPasswordLink = async () => { try { - await account.resendResetPassword(email); + await account.resetPassword(email, serviceName); logViewEvent(viewName, 'resend', REACT_ENTRYPOINT); setResendStatus(ResendStatus['sent']); } catch (e) { diff --git a/packages/fxa-settings/src/lib/channels/helpers.ts b/packages/fxa-settings/src/lib/channels/helpers.ts index 54d35897c05..d2bdd678bf9 100644 --- a/packages/fxa-settings/src/lib/channels/helpers.ts +++ b/packages/fxa-settings/src/lib/channels/helpers.ts @@ -2,93 +2,49 @@ * 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 { AccountData } from '../../models'; +import firefox, { FxALoginRequest } from './firefox'; -// const ALLOWED_LOGIN_FIELDS = [ -// 'declinedSyncEngines', -// 'email', -// 'keyFetchToken', -// 'offeredSyncEngines', -// 'sessionToken', -// 'services', -// 'uid', -// 'unwrapBKey', -// 'verified', -// ]; - -// const REQUIRED_LOGIN_FIELDS = [ -// 'email', -// 'keyFetchToken', -// 'sessionToken', -// 'uid', -// 'unwrapBKey', -// 'verified', -// ]; - -// Might need to go on the Integration? +// TODO: might need to go on Integration? // let uidOfLoginNotification: hexstring = ''; -// function hasRequiredLoginFields(loginData) { -// const loginFields = Object.keys(loginData); -// return ( -// REQUIRED_LOGIN_FIELDS.filter((field) => !loginFields.includes(field)) -// .length === 0 -// ); -// } - -/** - * Get login data from `account` to send to the browser. - * All returned keys have a defined value. - */ -// function getLoginData(account: AccountData) { -// let loginData: Partial = {}; -// for (const key of ALLOWED_LOGIN_FIELDS) { -// loginData[key] = account[key]; -// } -// // TODO: account for multiservice when we combine reliers -// // const isMultiService = this.relier && this.relier.get('multiService'); -// // if (isMultiService) { -// // loginData = this._formatForMultiServiceBrowser(loginData); -// // } -// loginData.verified = !!loginData.verified; -// // TODO: this is set in the `beforeSignIn` auth-broker method -// // loginData.verifiedCanLinkAccount = !!this._verifiedCanLinkEmail; -// return Object.fromEntries( -// Object.entries(loginData).filter(([key, value]) => value !== undefined) -// ); -// } +const REQUIRED_LOGIN_FIELDS: Array = [ + 'authAt', + 'email', + 'keyFetchToken', + 'sessionToken', + 'uid', + 'unwrapBKey', + 'verified', +]; -// TODO in FXA-7172 export function notifyFirefoxOfLogin( - account: AccountData, + accountData: FxALoginRequest, isSessionVerified: boolean ) { - // only notify the browser of the login if the user does not have - // to verify their account/session - if (!isSessionVerified) { - return; - } - - /** - * Workaround for #3078. If the user signs up but does not verify - * their account, then visit `/` or `/settings`, they are + /* Only notify the browser of the login if the user 1) does not have to + * verify their account/session and 2) when all required fields are present, + * which is a workaround for #3078. If the user signs up but does not + * verify their account, then visit `/` or `/settings`, they are * redirected to `/confirm` which attempts to notify the browser of * login. Since `unwrapBKey` and `keyFetchToken` are not persisted to * disk, the passed in account lacks these items. The browser can't - * do anything without this data, so don't actually send the message. - * - * Also works around #3514. With e10s enabled, localStorage in - * about:accounts and localStorage in the verification page are not - * shared. This lack of shared state causes the original tab of - * a password reset from about:accounts to not have all the - * required data. The verification tab sends a WebChannel message - * already, so no need here too. - */ - // const loginData = getLoginData(account); - // if (!hasRequiredLoginFields(loginData)) { - // return; + * do anything without this data, so don't actually send the message. */ + + if ( + !isSessionVerified || + !accountData.verified || + !REQUIRED_LOGIN_FIELDS.every((key) => key in accountData) + ) { + return; + } + + // TODO: account for multiservice TODO with relier/integration combination + // const isMultiService = this.relier && this.relier.get('multiService'); + // if (isMultiService) { + // loginData = this._formatForMultiServiceBrowser(loginData); // } + // TODO with relier/integration combination or during login tickets. // Only send one login notification per uid to avoid race // conditions within the browser. Two attempts to send // a login message occur for users that verify while @@ -98,6 +54,5 @@ export function notifyFirefoxOfLogin( // if (loginData.uid !== uidOfLoginNotification) { // uidOfLoginNotification = loginData.uid; - // send web channel LOGIN command with loginData - // } + firefox.fxaLogin(accountData); } diff --git a/packages/fxa-settings/src/lib/storybook-utils.ts b/packages/fxa-settings/src/lib/storybook-utils.ts new file mode 100644 index 00000000000..53bcdcd2d5d --- /dev/null +++ b/packages/fxa-settings/src/lib/storybook-utils.ts @@ -0,0 +1,29 @@ +/* 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 { ReactElement } from 'react'; +import { + MOCK_ACCOUNT, + createAppContext, + createHistoryWithQuery, + produceComponent, +} from '../models/mocks'; +import { Account } from '../models'; + +export function renderStoryWithHistory( + component: ReactElement, + route: string, + account = MOCK_ACCOUNT as unknown as Account, + queryParams?: string +) { + const history = createHistoryWithQuery(route, queryParams); + return produceComponent( + component, + { route, history }, + { + ...createAppContext(history), + account, + } + ); +} diff --git a/packages/fxa-settings/src/models/Account.ts b/packages/fxa-settings/src/models/Account.ts index 08c4a6ef5fc..ea75c8012fb 100644 --- a/packages/fxa-settings/src/models/Account.ts +++ b/packages/fxa-settings/src/models/Account.ts @@ -17,6 +17,7 @@ import Storage from '../lib/storage'; import random from '../lib/random'; import { AuthUiErrorNos, AuthUiErrors } from '../lib/auth-errors/auth-errors'; import { GET_SESSION_VERIFIED } from './Session'; +import { MozServices } from '../lib/types'; export interface DeviceLocation { city: string | null; @@ -538,7 +539,10 @@ export class Account implements AccountData { }); } - async resetPassword(email: string): Promise { + async resetPassword( + email: string, + service?: string + ): Promise { try { const result = await this.apolloClient.mutate({ mutation: gql` @@ -550,7 +554,17 @@ export class Account implements AccountData { } } `, - variables: { input: { email } }, + variables: { + input: { + email, + // Only include the `service` option if the service is Sync. + // This becomes a query param (service=sync) on the email link. + // We need to modify this in FXA-7657 to send the `client_id` param + // when we work on the OAuth flow. + ...(service && + service === MozServices.FirefoxSync && { service: 'sync' }), + }, + }, }); return result.data.passwordForgotSendCode; } catch (err) { @@ -614,51 +628,6 @@ export class Account implements AccountData { } } - async resendResetPassword( - email: string - ): Promise { - try { - const result = await this.apolloClient.mutate({ - mutation: gql` - mutation passwordForgotSendCode( - $input: PasswordForgotSendCodeInput! - ) { - passwordForgotSendCode(input: $input) { - clientMutationId - passwordForgotToken - } - } - `, - variables: { input: { email } }, - }); - return result.data.passwordForgotSendCode; - } catch (err) { - const graphQlError = ((err as ApolloError) || (err as ThrottledError)) - .graphQLErrors[0]; - const errno = graphQlError.extensions?.errno; - if ( - (err as ThrottledError) && - errno && - AuthUiErrorNos[errno] && - errno === AuthUiErrors.THROTTLED.errno - ) { - const throttledErrorWithRetryAfter = { - ...AuthUiErrorNos[errno], - retryAfter: graphQlError.extensions?.retryAfter, - retryAfterLocalized: graphQlError.extensions?.retryAfterLocalized, - }; - throw throttledErrorWithRetryAfter; - } else if ( - errno && - AuthUiErrorNos[errno] && - errno !== AuthUiErrors.THROTTLED.errno - ) { - throw AuthUiErrorNos[errno]; - } - throw AuthUiErrors.UNEXPECTED_ERROR; - } - } - /** * Verify a passwordForgotToken, which returns an accountResetToken that can * be used to perform the actual password reset. @@ -721,6 +690,10 @@ export class Account implements AccountData { clientMutationId sessionToken uid + authAt + keyFetchToken + verified + unwrapBKey } } `, @@ -729,12 +702,13 @@ export class Account implements AccountData { accountResetToken, email, newPassword, - options: { sessionToken: true }, + options: { sessionToken: true, keys: true }, }, }, }); currentAccount(getOldSettingsData(accountReset)); sessionToken(accountReset.sessionToken); + return accountReset; } catch (err) { const errno = (err as ApolloError).graphQLErrors[0].extensions?.errno; if (errno && AuthUiErrorNos[errno]) { @@ -1280,7 +1254,7 @@ export class Account implements AccountData { opts.password, opts.recoveryKeyId, { kB: opts.kB }, - { sessionToken: true } + { sessionToken: true, keys: true } ); currentAccount(currentAccount(getOldSettingsData(data))); sessionToken(data.sessionToken); @@ -1293,5 +1267,6 @@ export class Account implements AccountData { }, }, }); + return data; } } diff --git a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.stories.tsx b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.stories.tsx index 0d46a131714..217d487b354 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.stories.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.stories.tsx @@ -7,25 +7,26 @@ import { Account } from '../../../models'; import AccountRecoveryConfirmKey from '.'; import { Meta } from '@storybook/react'; import { + getSubject, mockCompleteResetPasswordParams, paramsWithMissingEmail, - Subject, } from './mocks'; +import { produceComponent } from '../../../models/mocks'; export default { title: 'Pages/ResetPassword/AccountRecoveryConfirmKey', component: AccountRecoveryConfirmKey, } as Meta; -const storyWithSubject = ( - account: Account, - params: Record, +function renderStory( + { account = accountValid, params = mockCompleteResetPasswordParams } = {}, storyName?: string -) => { - const story = () => ; +) { + const { Subject, history, appCtx } = getSubject(account, params); + const story = () => produceComponent(, { history }, appCtx); story.storyName = storyName; - return story; -}; + return story(); +} const accountValid = { resetPasswordStatus: () => Promise.resolve(true), @@ -45,23 +46,29 @@ const accountWithInvalidRecoveryKey = { verifyPasswordForgotToken: () => Promise.resolve(true), } as unknown as Account; -export const OnConfirmValidKey = storyWithSubject( - accountValid, - mockCompleteResetPasswordParams, - 'Valid recovery key. Users will be redirected to AccountRecoveryResetPassword on confirm' -); +export const OnConfirmValidKey = () => { + return renderStory( + {}, + 'Valid recovery key (32 characters). Users will be redirected to AccountRecoveryResetPassword on confirm' + ); +}; -export const OnConfirmInvalidKey = storyWithSubject( - accountWithInvalidRecoveryKey, - mockCompleteResetPasswordParams -); +export const OnConfirmInvalidKey = () => { + return renderStory({ + account: accountWithInvalidRecoveryKey, + params: mockCompleteResetPasswordParams, + }); +}; -export const OnConfirmLinkExpired = storyWithSubject( - accountWithExpiredLink, - mockCompleteResetPasswordParams -); +export const OnConfirmLinkExpired = () => { + return renderStory({ + account: accountWithExpiredLink, + params: mockCompleteResetPasswordParams, + }); +}; -export const WithDamagedLink = storyWithSubject( - accountValid, - paramsWithMissingEmail -); +export const WithDamagedLink = () => { + return renderStory({ + params: paramsWithMissingEmail, + }); +}; diff --git a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.test.tsx b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.test.tsx index a33b8197c2d..45d1e46c985 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.test.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/index.test.tsx @@ -10,7 +10,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { logPageViewEvent, logViewEvent } from '../../../lib/metrics'; import { viewName } from '.'; import { - Subject, MOCK_RECOVERY_KEY, MOCK_RESET_TOKEN, MOCK_RECOVERY_KEY_ID, @@ -19,12 +18,13 @@ import { paramsWithMissingToken, paramsWithMissingCode, paramsWithMissingEmail, + getSubject, } from './mocks'; import { REACT_ENTRYPOINT } from '../../../constants'; import { Account } from '../../../models'; import { typeByLabelText } from '../../../lib/test-utils'; import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors'; -import { MOCK_ACCOUNT } from '../../../models/mocks'; +import { MOCK_ACCOUNT, renderWithRouter } from '../../../models/mocks'; jest.mock('../../../lib/metrics', () => ({ logPageViewEvent: jest.fn(), @@ -76,7 +76,8 @@ const renderSubject = ({ account = accountWithValidResetToken, params = mockCompleteResetPasswordParams, } = {}) => { - render(); + const { Subject, history, appCtx } = getSubject(account, params); + return renderWithRouter(, { history }, appCtx); }; describe('PageAccountRecoveryConfirmKey', () => { diff --git a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/mocks.tsx b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/mocks.tsx index 0a1527e5c1c..e4f71b163bf 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/mocks.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryConfirmKey/mocks.tsx @@ -4,9 +4,13 @@ import React from 'react'; import { MozServices } from '../../../lib/types'; -import { LocationProvider } from '@reach/router'; -import { Account, AppContext } from '../../../models'; -import { mockAppContext, MOCK_ACCOUNT } from '../../../models/mocks'; +import { Account } from '../../../models'; +import { + mockAppContext, + MOCK_ACCOUNT, + createHistoryWithQuery, + createAppContext, +} from '../../../models/mocks'; import { LinkType } from 'fxa-settings/src/lib/types'; import LinkValidator from '../../../components/LinkValidator'; @@ -77,38 +81,42 @@ class StorageDataMock extends StorageData { } } -export const Subject = ({ - account, - params, -}: { - account: Account; - params: Record; -}) => { - const windowWrapper = new ReachRouterWindow(); +const route = '/account_recovery_confirm_key'; +export const getSubject = ( + account: Account, + params?: Record +) => { const urlQueryData = mockUrlQueryData(params); + const history = createHistoryWithQuery( + route, + new URLSearchParams(params).toString() + ); + const windowWrapper = new ReachRouterWindow(history); - return ( - ( + { + return new CompleteResetPasswordLink(urlQueryData); + }} + > + {({ setLinkStatus, params }) => ( + + )} + + ), + route, + history, + appCtx: { + ...mockAppContext({ + ...createAppContext(history), account, windowWrapper, - storageData: new StorageDataMock(windowWrapper), urlQueryData, - })} - > - - { - return new CompleteResetPasswordLink(urlQueryData); - }} - > - {({ setLinkStatus, params }) => ( - - )} - - - - ); + storageData: new StorageDataMock(windowWrapper), + }), + }, + }; }; diff --git a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.test.tsx b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.test.tsx index 84222d2c5cd..325e48d4842 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.test.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.test.tsx @@ -248,7 +248,9 @@ describe('AccountRecoveryResetPassword page', () => { describe('successful reset', () => { beforeEach(async () => { mockAccount.setLastLogin = jest.fn(); - mockAccount.resetPasswordWithRecoveryKey = jest.fn(); + mockAccount.resetPasswordWithRecoveryKey = jest + .fn() + .mockResolvedValue(mocks.MOCK_RESET_DATA); mockAccount.isSessionVerifiedAuthClient = jest.fn(); mockAccount.hasTotpAuthClient = jest.fn().mockResolvedValue(false); @@ -327,7 +329,9 @@ describe('AccountRecoveryResetPassword page', () => { beforeEach(async () => { window.location.href = originalWindow.href; mockAccount.setLastLogin = jest.fn(); - mockAccount.resetPasswordWithRecoveryKey = jest.fn(); + mockAccount.resetPasswordWithRecoveryKey = jest + .fn() + .mockResolvedValue(mocks.MOCK_RESET_DATA); mockAccount.isSessionVerifiedAuthClient = jest.fn(); mockAccount.hasTotpAuthClient = jest.fn().mockResolvedValue(true); await renderPage(); diff --git a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.tsx b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.tsx index 861bafb4609..653378d3e50 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/index.tsx @@ -252,7 +252,9 @@ const AccountRecoveryResetPassword = ({ verificationInfo.emailToHashWith || verificationInfo.email, }; - await account.resetPasswordWithRecoveryKey(options); + const accountResetData = await account.resetPasswordWithRecoveryKey( + options + ); // must come after completeResetPassword since that receives the sessionToken // required for this check const sessionIsVerified = await account.isSessionVerifiedAuthClient(); @@ -267,8 +269,22 @@ const AccountRecoveryResetPassword = ({ logViewEvent(viewName, 'verification.success'); switch (integration.type) { + // NOTE: SyncBasic check is temporary until we implement codes + // See https://docs.google.com/document/d/1K4AD69QgfOCZwFLp7rUcMOkOTslbLCh7jjSdR9zpAkk/edit#heading=h.kkt4eylho93t case IntegrationType.SyncDesktop: - notifyFirefoxOfLogin(account, sessionIsVerified); + case IntegrationType.SyncBasic: + notifyFirefoxOfLogin( + { + authAt: accountResetData.authAt, + email: verificationInfo.email, + keyFetchToken: accountResetData.keyFetchToken, + sessionToken: accountResetData.sessionToken, + uid: accountResetData.uid, + unwrapBKey: accountResetData.unwrapBKey, + verified: accountResetData.verified, + }, + sessionIsVerified + ); break; case IntegrationType.OAuth: if ( diff --git a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/mocks.tsx b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/mocks.tsx index 72889999985..838e72a861f 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/mocks.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/AccountRecoveryResetPassword/mocks.tsx @@ -277,3 +277,10 @@ export function mockContext() { } export const MOCK_SERVICE_NAME = MozServices.FirefoxSync; +export const MOCK_RESET_DATA = { + authAt: 12345, + keyFetchToken: 'keyFetchToken', + sessionToken: 'sessionToken', + unwrapBKey: 'unwrapBKey', + verified: true, +}; diff --git a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.stories.tsx b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.stories.tsx index fcbdec2ea09..1a7bc885a5f 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.stories.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.stories.tsx @@ -7,7 +7,9 @@ import { Meta } from '@storybook/react'; import CompleteResetPassword from '.'; import { Account } from '../../../models'; import { withLocalization } from '../../../../.storybook/decorators'; -import { paramsWithMissingEmail, Subject } from './mocks'; +import { getSubject, paramsWithMissingEmail } from './mocks'; +import { produceComponent } from '../../../models/mocks'; +// import { resetMocks } from '../AccountRecoveryResetPassword/mocks'; export default { title: 'Pages/ResetPassword/CompleteResetPassword', @@ -15,18 +17,21 @@ export default { decorators: [withLocalization], } as Meta; -const storyWithSubject = ( - account: Account, - params?: Record, - storyName?: string -) => { - const story = () => { - return ; - }; +type RenderStoryOptions = { + account?: Account; + params?: Record; +}; +function renderStory( + { account = accountNoRecoveryKey, params }: RenderStoryOptions = {}, + storyName?: string +) { + // resetMocks(); + const { Subject, history, appCtx } = getSubject(account, params); + const story = () => produceComponent(, { history }, appCtx); story.storyName = storyName; - return story; -}; + return story(); +} const accountNoRecoveryKey = { resetPasswordStatus: () => Promise.resolve(true), @@ -44,21 +49,27 @@ const accountWithFalseyResetPasswordStatus = { resetPasswordStatus: () => Promise.resolve(false), } as unknown as Account; -export const NoRecoveryKeySet = storyWithSubject( - accountNoRecoveryKey, - undefined, - 'Default - no account recovery key set. Users with one set will be redirected to AccountRecoveryConfirmKey' -); - -export const ErrorCheckingRecoveryKeyStatus = storyWithSubject( - accountWithRecoveryKeyStatusError -); +export const NoRecoveryKeySet = () => { + return renderStory( + {}, + 'Default - no account recovery key set. Users with one set will be redirected to AccountRecoveryConfirmKey' + ); +}; -export const WithExpiredLink = storyWithSubject( - accountWithFalseyResetPasswordStatus -); +export const ErrorCheckingRecoveryKeyStatus = () => { + return renderStory({ + account: accountWithRecoveryKeyStatusError, + }); +}; +export const WithExpiredLink = () => { + return renderStory({ + account: accountWithFalseyResetPasswordStatus, + }); +}; -export const WithDamagedLink = storyWithSubject( - accountNoRecoveryKey, - paramsWithMissingEmail -); +export const WithDamagedLink = () => { + return renderStory({ + account: accountNoRecoveryKey, + params: paramsWithMissingEmail, + }); +}; diff --git a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.test.tsx b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.test.tsx index f95cc6be622..96028185139 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.test.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.test.tsx @@ -3,26 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import { Account } from '../../../models'; import { logPageViewEvent } from '../../../lib/metrics'; import { REACT_ENTRYPOINT, SHOW_BALLOON_TIMEOUT } from '../../../constants'; import { + getSubject, + MOCK_RESET_DATA, mockCompleteResetPasswordParams, paramsWithMissingCode, paramsWithMissingEmail, paramsWithMissingEmailToHashWith, paramsWithMissingToken, paramsWithSyncDesktop, - Subject, } from './mocks'; import { notifyFirefoxOfLogin } from '../../../lib/channels/helpers'; +import { renderWithRouter } from '../../../models/mocks'; // import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils'; // import { FluentBundle } from '@fluent/bundle'; @@ -76,9 +72,10 @@ jest.mock('@reach/router', () => ({ useLocation: () => mockLocation(), })); -function renderSubject(account: Account, params?: Record) { - render(); -} +const renderSubject = (account: Account, params?: Record) => { + const { Subject, history, appCtx } = getSubject(account, params); + return renderWithRouter(, { history }, appCtx); +}; describe('CompleteResetPassword page', () => { // TODO: enable l10n tests when they've been updated to handle embedded tags in ftl strings @@ -92,7 +89,7 @@ describe('CompleteResetPassword page', () => { account = { resetPasswordStatus: jest.fn().mockResolvedValue(true), - completeResetPassword: jest.fn().mockResolvedValue(true), + completeResetPassword: jest.fn().mockResolvedValue(MOCK_RESET_DATA), hasRecoveryKey: jest.fn().mockResolvedValue(false), hasTotpAuthClient: jest.fn().mockResolvedValue(false), isSessionVerifiedAuthClient: jest.fn().mockResolvedValue(true), @@ -140,6 +137,7 @@ describe('CompleteResetPassword page', () => { it('renders the component as expected when provided with an expired link', async () => { account = { + ...account, resetPasswordStatus: jest.fn().mockResolvedValue(false), } as unknown as Account; @@ -252,7 +250,7 @@ describe('CompleteResetPassword page', () => { describe('account has recovery key', () => { const accountWithRecoveryKey = { resetPasswordStatus: jest.fn().mockResolvedValue(true), - completeResetPassword: jest.fn().mockResolvedValue(true), + completeResetPassword: jest.fn().mockResolvedValue(MOCK_RESET_DATA), hasRecoveryKey: jest.fn().mockResolvedValue(true), } as unknown as Account; diff --git a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.tsx b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.tsx index 4975829e638..a80254c7594 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/index.tsx @@ -175,7 +175,7 @@ const CompleteResetPassword = ({ // how account password hashing works previously. const emailToUse = emailToHashWith || email; - await account.completeResetPassword( + const accountResetData = await account.completeResetPassword( token, code, emailToUse, @@ -196,8 +196,22 @@ const CompleteResetPassword = ({ let hardNavigate = false; switch (integration.type) { + // NOTE: SyncBasic check is temporary until we implement codes + // See https://docs.google.com/document/d/1K4AD69QgfOCZwFLp7rUcMOkOTslbLCh7jjSdR9zpAkk/edit#heading=h.kkt4eylho93t case IntegrationType.SyncDesktop: - notifyFirefoxOfLogin(account, sessionIsVerified); + case IntegrationType.SyncBasic: + notifyFirefoxOfLogin( + { + authAt: accountResetData.authAt, + email, + keyFetchToken: accountResetData.keyFetchToken, + sessionToken: accountResetData.sessionToken, + uid: accountResetData.uid, + unwrapBKey: accountResetData.unwrapBKey, + verified: accountResetData.verified, + }, + sessionIsVerified + ); break; case IntegrationType.OAuth: if ( diff --git a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/mocks.tsx b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/mocks.tsx index 2af8851cb1e..8358e18b89a 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/mocks.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/mocks.tsx @@ -3,13 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import { LocationProvider } from '@reach/router'; import { LinkType } from 'fxa-settings/src/lib/types'; import CompleteResetPassword from '.'; import LinkValidator from '../../../components/LinkValidator'; import { StorageData, UrlQueryData } from '../../../lib/model-data'; -import { Account, AppContext } from '../../../models'; -import { mockAppContext, MOCK_ACCOUNT } from '../../../models/mocks'; +import { Account } from '../../../models'; +import { + mockAppContext, + MOCK_ACCOUNT, + createHistoryWithQuery, + createAppContext, +} from '../../../models/mocks'; import { CompleteResetPasswordLink } from '../../../models/reset-password/verification'; import { ReachRouterWindow } from '../../../lib/window'; @@ -49,6 +53,14 @@ export const paramsWithMissingToken = { token: '', }; +export const MOCK_RESET_DATA = { + authAt: 12345, + keyFetchToken: 'keyFetchToken', + sessionToken: 'sessionToken', + unwrapBKey: 'unwrapBKey', + verified: true, +}; + export function mockUrlQueryData( params: Record = mockCompleteResetPasswordParams ) { @@ -69,38 +81,42 @@ class StorageDataMock extends StorageData { } } -export const Subject = ({ - account, - params, -}: { - account: Account; - params?: Record; -}) => { - const windowWrapper = new ReachRouterWindow(); +const route = '/complete_reset_password'; +export const getSubject = ( + account: Account, + params?: Record +) => { const urlQueryData = mockUrlQueryData(params); + const history = createHistoryWithQuery( + route, + new URLSearchParams(params).toString() + ); + const windowWrapper = new ReachRouterWindow(history); - return ( - ( + { + return new CompleteResetPasswordLink(urlQueryData); + }} + > + {({ setLinkStatus, params }) => ( + + )} + + ), + route, + history, + appCtx: { + ...mockAppContext({ + ...createAppContext(history), account, windowWrapper, - storageData: new StorageDataMock(windowWrapper), urlQueryData, - })} - > - - { - return new CompleteResetPasswordLink(urlQueryData); - }} - > - {({ setLinkStatus, params }) => ( - - )} - - - - ); + storageData: new StorageDataMock(windowWrapper), + }), + }, + }; }; diff --git a/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.stories.tsx b/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.stories.tsx index d1a2b400df5..dea1b31ae7b 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.stories.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.stories.tsx @@ -4,14 +4,9 @@ import React from 'react'; import ConfirmResetPassword from '.'; -import { - LocationProvider, - createHistory, - createMemorySource, -} from '@reach/router'; import { Meta } from '@storybook/react'; -import { MOCK_EMAIL, MOCK_PASSWORD_FORGOT_TOKEN } from './mocks'; import { withLocalization } from '../../../../.storybook/decorators'; +import { renderStoryWithHistory } from '../../../lib/storybook-utils'; export default { title: 'Pages/ResetPassword/ConfirmResetPassword', @@ -19,15 +14,5 @@ export default { decorators: [withLocalization], } as Meta; -const source = createMemorySource('/fake-memories'); -const history = createHistory(source); -history.location.state = { - email: MOCK_EMAIL, - passwordForgotToken: MOCK_PASSWORD_FORGOT_TOKEN, -}; - -export const Default = () => ( - - - -); +export const Default = () => + renderStoryWithHistory(, '/confirm_reset_password'); diff --git a/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.test.tsx b/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.test.tsx index f57a9d2cf3a..3017752daf7 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.test.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.test.tsx @@ -3,16 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import ConfirmResetPassword, { viewName } from '.'; // import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils'; // import { FluentBundle } from '@fluent/bundle'; import { MOCK_EMAIL, MOCK_PASSWORD_FORGOT_TOKEN } from './mocks'; import { REACT_ENTRYPOINT } from '../../../constants'; -import { LocationProvider } from '@reach/router'; -import { Account, AppContext } from '../../../models'; -import { mockAppContext, MOCK_ACCOUNT } from '../../../models/mocks'; +import { Account } from '../../../models'; +import { + mockAppContext, + MOCK_ACCOUNT, + createAppContext, + renderWithRouter, + createHistoryWithQuery, +} from '../../../models/mocks'; import { usePageViewEvent, logViewEvent } from '../../../lib/metrics'; +import { MozServices } from '../../../lib/types'; jest.mock('../../../lib/metrics', () => ({ logViewEvent: jest.fn(), @@ -21,15 +27,21 @@ jest.mock('../../../lib/metrics', () => ({ const account = MOCK_ACCOUNT as unknown as Account; -function renderWithAccount(account: Account) { - render( - - - - - +const route = '/confirm_reset_password'; +const renderWithHistory = (ui: any, queryParams = '', account?: Account) => { + const history = createHistoryWithQuery(route, queryParams); + return renderWithRouter( + ui, + { + route, + history, + }, + mockAppContext({ + ...createAppContext(history), + ...(account && { account }), + }) ); -} +}; const mockNavigate = jest.fn(); jest.mock('@reach/router', () => ({ @@ -53,7 +65,7 @@ describe('ConfirmResetPassword page', () => { // }); it('renders as expected', () => { - render(); + renderWithHistory(); // testAllL10n(screen, bundle); @@ -72,8 +84,8 @@ describe('ConfirmResetPassword page', () => { }); it('sends a new email when clicking on resend button', async () => { - renderWithAccount(account); - account.resendResetPassword = jest.fn().mockResolvedValue(''); + renderWithHistory(, '', account); + account.resetPassword = jest.fn().mockResolvedValue(''); const resendEmailButton = screen.getByRole('button', { name: 'Not in inbox or spam folder? Resend', @@ -81,7 +93,12 @@ describe('ConfirmResetPassword page', () => { fireEvent.click(resendEmailButton); - await waitFor(() => expect(account.resendResetPassword).toHaveBeenCalled()); + await waitFor(() => + expect(account.resetPassword).toHaveBeenCalledWith( + MOCK_EMAIL, + MozServices.Default + ) + ); expect(logViewEvent).toHaveBeenCalledWith( 'confirm-reset-password', 'resend', @@ -90,12 +107,12 @@ describe('ConfirmResetPassword page', () => { }); it('emits the expected metrics on render', async () => { - render(); + renderWithHistory(); expect(usePageViewEvent).toHaveBeenCalledWith(viewName, REACT_ENTRYPOINT); }); it('renders a "Remember your password?" link', () => { - render(); + renderWithHistory(); expect( screen.getByRole('link', { name: 'Remember your password? Sign in' }) ).toBeInTheDocument(); diff --git a/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.tsx b/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.tsx index 45082fe5e6b..390e01f6b85 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/ConfirmResetPassword/index.tsx @@ -7,7 +7,7 @@ import { RouteComponentProps, useLocation, useNavigate } from '@reach/router'; import { POLLING_INTERVAL_MS, REACT_ENTRYPOINT } from '../../../constants'; import { usePageViewEvent, logViewEvent } from '../../../lib/metrics'; import { ResendStatus } from '../../../lib/types'; -import { useAccount, useInterval } from '../../../models'; +import { CreateRelier, useAccount, useInterval } from '../../../models'; import AppLayout from '../../../components/AppLayout'; import ConfirmWithLink, { ConfirmWithLinkPageStrings, @@ -24,6 +24,8 @@ export type ConfirmResetPasswordLocationState = { const ConfirmResetPassword = (_: RouteComponentProps) => { usePageViewEvent(viewName, REACT_ENTRYPOINT); + const relier = CreateRelier(); + const serviceName = relier.getServiceName(); const navigate = useNavigate(); let { state } = useLocation(); @@ -70,7 +72,7 @@ const ConfirmResetPassword = (_: RouteComponentProps) => { const resendEmailHandler = async () => { try { - const result = await account.resendResetPassword(email); + const result = await account.resetPassword(email, serviceName); logViewEvent(viewName, 'resend', REACT_ENTRYPOINT); setCurrentPasswordForgotToken(result.passwordForgotToken); setResendStatus(ResendStatus['sent']); diff --git a/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.stories.tsx b/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.stories.tsx index 1aa86d307d7..7249e6767e8 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.stories.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.stories.tsx @@ -3,14 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import ResetPasswordWithRecoveryKeyVerified from '.'; +import ResetPasswordWithRecoveryKeyVerified, { + ResetPasswordWithRecoveryKeyVerifiedProps, +} from '.'; import { Meta } from '@storybook/react'; import { withLocalization } from '../../../../.storybook/decorators'; -import { - produceComponent, - createAppContext, - createHistoryWithQuery, -} from '../../../models/mocks'; +import { Account } from '../../../models'; +import { renderStoryWithHistory } from '../../../lib/storybook-utils'; export default { title: 'Pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified', @@ -18,19 +17,29 @@ export default { decorators: [withLocalization], } as Meta; -const route = '/reset_password_with_recovery_key_verified'; +type RenderStoryOptions = { + account?: Account; + props?: ResetPasswordWithRecoveryKeyVerifiedProps; + queryParams?: string; +}; -function render(isSignedIn: boolean, queryParams?: string) { - const history = createHistoryWithQuery(route, queryParams); - const appCtx = createAppContext(history); - return produceComponent( - , - { route, history }, - appCtx +function renderStory({ + account, + props = { isSignedIn: false }, + queryParams, +}: RenderStoryOptions = {}) { + return renderStoryWithHistory( + , + '/reset_password_with_recovery_key_verified', + account, + queryParams ); } -export const DefaultAccountSignedIn = () => render(true); -export const DefaultAccountSignedOut = () => render(false); -export const WithSyncAccountSignedIn = () => render(true, `service=sync`); -export const WithSyncAccountSignedOut = () => render(false, `service=sync`); +export const DefaultAccountSignedIn = () => + renderStory({ props: { isSignedIn: true } }); +export const DefaultAccountSignedOut = () => renderStory(); +export const WithSyncAccountSignedIn = () => + renderStory({ props: { isSignedIn: true }, queryParams: `service=sync` }); +export const WithSyncAccountSignedOut = () => + renderStory({ queryParams: `service=sync` }); diff --git a/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.tsx b/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.tsx index ff9073d76f3..b58ae9285e5 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/ResetPasswordWithRecoveryKeyVerified/index.tsx @@ -11,7 +11,7 @@ import { REACT_ENTRYPOINT } from '../../../constants'; import AppLayout from '../../../components/AppLayout'; import { CreateRelier, useFtlMsgResolver } from '../../../models'; -type ResetPasswordWithRecoveryKeyVerifiedProps = { +export type ResetPasswordWithRecoveryKeyVerifiedProps = { isSignedIn: boolean; }; diff --git a/packages/fxa-settings/src/pages/ResetPassword/index.stories.tsx b/packages/fxa-settings/src/pages/ResetPassword/index.stories.tsx index edd4f092e39..0b17c3bb807 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/index.stories.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/index.stories.tsx @@ -4,23 +4,16 @@ import React from 'react'; import ResetPassword, { ResetPasswordProps } from '.'; -import { LocationProvider } from '@reach/router'; import { Meta } from '@storybook/react'; -import { MOCK_ACCOUNT } from '../../models/mocks'; import { MozServices } from '../../lib/types'; import { withLocalization } from '../../../.storybook/decorators'; -import { Account, AppContext } from '../../models'; import { mockAccountWithThrottledError, mockAccountWithUnexpectedError, - mockDefaultAccount, } from './mocks'; - -import { - produceComponent, - createAppContext, - createHistoryWithQuery, -} from '../../models/mocks'; +import { renderStoryWithHistory } from '../../lib/storybook-utils'; +import { Account } from '../../models'; +import { MOCK_ACCOUNT } from '../../models/mocks'; export default { title: 'Pages/ResetPassword', @@ -28,47 +21,36 @@ export default { decorators: [withLocalization], } as Meta; -const route = '/reset_password'; +type RenderStoryOptions = { + account?: Account; + props?: Partial; + queryParams?: string; +}; -function render( - account: Account, - props?: Partial, - queryParams?: string -) { - const history = createHistoryWithQuery(route, queryParams); - return produceComponent( +function renderStory({ account, props, queryParams }: RenderStoryOptions = {}) { + return renderStoryWithHistory( , - { route, history }, - { - ...createAppContext(history), - account, - } + '/reset_password', + account, + queryParams ); } -export const Default = () => { - return render(mockDefaultAccount); -}; +export const Default = () => renderStory(); -export const WithServiceName = () => { - return render( - mockDefaultAccount, - undefined, - `service=${MozServices.MozillaVPN}` - ); -}; +export const WithServiceName = () => + renderStory({ queryParams: `service=${MozServices.MozillaVPN}` }); -export const WithForceAuth = () => { - return render(mockDefaultAccount, { - prefillEmail: MOCK_ACCOUNT.primaryEmail.email, - forceAuth: true, +export const WithForceAuth = () => + renderStory({ + props: { + prefillEmail: MOCK_ACCOUNT.primaryEmail.email, + forceAuth: true, + }, }); -}; -export const WithThrottledErrorOnSubmit = () => { - return render(mockAccountWithThrottledError); -}; +export const WithThrottledErrorOnSubmit = () => + renderStory({ account: mockAccountWithThrottledError }); -export const WithUnexpectedErrorOnSubmit = () => { - return render(mockAccountWithUnexpectedError); -}; +export const WithUnexpectedErrorOnSubmit = () => + renderStory({ account: mockAccountWithUnexpectedError }); diff --git a/packages/fxa-settings/src/pages/ResetPassword/index.test.tsx b/packages/fxa-settings/src/pages/ResetPassword/index.test.tsx index 2d93d181b3e..3ad49eba485 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/index.test.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/index.test.tsx @@ -13,7 +13,7 @@ import ResetPassword, { viewName } from '.'; import { REACT_ENTRYPOINT } from '../../constants'; import { MOCK_ACCOUNT, mockAppContext } from '../../models/mocks'; -import { Account, AppContext } from '../../models'; +import { Account } from '../../models'; import { AuthUiErrorNos } from '../../lib/auth-errors/auth-errors'; import { typeByLabelText } from '../../lib/test-utils'; import { @@ -21,6 +21,7 @@ import { createHistoryWithQuery, renderWithRouter, } from '../../models/mocks'; +import { MozServices } from '../../lib/types'; const mockLogViewEvent = jest.fn(); const mockLogPageViewEvent = jest.fn(); @@ -148,7 +149,8 @@ describe('PageResetPassword', () => { }); expect(account.resetPassword).toHaveBeenCalledWith( - MOCK_ACCOUNT.primaryEmail.email + MOCK_ACCOUNT.primaryEmail.email, + MozServices.Default ); expect(mockNavigate).toHaveBeenCalledWith( diff --git a/packages/fxa-settings/src/pages/ResetPassword/index.tsx b/packages/fxa-settings/src/pages/ResetPassword/index.tsx index 0fb5fc9fc7b..c59ab29f51c 100644 --- a/packages/fxa-settings/src/pages/ResetPassword/index.tsx +++ b/packages/fxa-settings/src/pages/ResetPassword/index.tsx @@ -100,7 +100,7 @@ const ResetPassword = ({ async (email: string) => { try { clearError(); - const result = await account.resetPassword(email); + const result = await account.resetPassword(email, serviceName); navigateToConfirmPwReset({ passwordForgotToken: result.passwordForgotToken, email,