Skip to content

Commit 701b660

Browse files
Merge pull request #19925 from mozilla/FXA-12814
fix(settings): fix password reset with relay integration
2 parents 0070b1c + 36da5f7 commit 701b660

3 files changed

Lines changed: 196 additions & 3 deletions

File tree

packages/functional-tests/tests/misc/relayIntegration.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,46 @@ test.describe('relay integration', () => {
138138
relay: {},
139139
});
140140
});
141+
142+
test('reset password with Relay desktop', async ({
143+
target,
144+
syncOAuthBrowserPages: { page, signin, resetPassword, settings },
145+
testAccountTracker,
146+
}) => {
147+
const { email } = await testAccountTracker.signUp();
148+
const newPassword = testAccountTracker.generatePassword();
149+
150+
await resetPassword.goto(
151+
'/authorization',
152+
relayDesktopOAuthQueryParams.toString()
153+
);
154+
155+
await expect(signin.page.getByText('Create an email mask')).toBeVisible();
156+
157+
await signin.fillOutEmailFirstForm(email);
158+
159+
await signin.forgotPasswordLink.click();
160+
161+
await page.waitForURL(/reset_password/);
162+
163+
await resetPassword.fillOutEmailForm(email);
164+
165+
const code = await target.emailClient.getResetPasswordCode(email);
166+
await resetPassword.fillOutResetPasswordCodeForm(code);
167+
168+
await resetPassword.fillOutNewPasswordForm(newPassword);
169+
170+
await page.waitForURL(/settings/);
171+
172+
await expect(settings.alertBar).toContainText(
173+
'Your password has been reset'
174+
);
175+
176+
await resetPassword.checkWebChannelMessage(FirefoxCommand.OAuthLogin);
177+
await resetPassword.checkWebChannelMessageServices(FirefoxCommand.Login, {
178+
relay: {},
179+
});
180+
181+
testAccountTracker.updateAccountPassword(email, newPassword);
182+
});
141183
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React from 'react';
6+
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
7+
import CompleteResetPasswordContainer from './container';
8+
import { mockOAuthNativeSigninIntegration } from '../../Signin/SigninTotpCode/mocks';
9+
import { Integration } from '../../../models';
10+
import {
11+
MOCK_EMAIL,
12+
MOCK_OAUTH_FLOW_HANDLER_RESPONSE,
13+
MOCK_UID,
14+
} from '../../mocks';
15+
import { SETTINGS_PATH } from '../../../constants';
16+
import { screen, waitFor } from '@testing-library/react';
17+
import userEvent from '@testing-library/user-event';
18+
import { LocationProvider } from '@reach/router';
19+
import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks';
20+
import firefox from '../../../lib/channels/firefox';
21+
22+
const mockNavigateWithQuery = jest.fn();
23+
jest.mock('../../../lib/hooks/useNavigateWithQuery', () => ({
24+
useNavigateWithQuery: () => mockNavigateWithQuery,
25+
}));
26+
27+
const mockAlertBar = {
28+
success: jest.fn(),
29+
};
30+
31+
const mockAccount = {
32+
completeResetPassword: jest.fn(),
33+
};
34+
35+
const mockAuthClient = {};
36+
37+
const mockSensitiveDataClient = {
38+
getDataType: jest.fn(),
39+
setDataType: jest.fn(),
40+
};
41+
42+
jest.mock('../../../lib/oauth/hooks', () => ({
43+
useFinishOAuthFlowHandler: jest.fn(),
44+
}));
45+
46+
jest.mock('../../../models', () => ({
47+
__esModule: true,
48+
...jest.requireActual('../../../models'),
49+
useAlertBar: () => mockAlertBar,
50+
useAccount: () => mockAccount,
51+
useAuthClient: () => mockAuthClient,
52+
useSensitiveDataClient: () => mockSensitiveDataClient,
53+
useFtlMsgResolver: () => ({
54+
getMsg: (_id: string, fallback: string) => fallback,
55+
}),
56+
}));
57+
58+
jest.mock('@reach/router', () => {
59+
const actual = jest.requireActual('@reach/router');
60+
return {
61+
...actual,
62+
useLocation: () => ({
63+
state: {
64+
email: MOCK_EMAIL,
65+
uid: MOCK_UID,
66+
token: 'tok',
67+
code: '1234567890',
68+
},
69+
pathname: '/complete_reset_password',
70+
search: '',
71+
hash: '',
72+
}),
73+
};
74+
});
75+
76+
describe('CompleteResetPasswordContainer', () => {
77+
let fxaLoginSignedInUserSpy: jest.SpyInstance;
78+
79+
beforeEach(() => {
80+
mockNavigateWithQuery.mockImplementation(() => {});
81+
fxaLoginSignedInUserSpy = jest.spyOn(firefox, 'fxaLoginSignedInUser');
82+
83+
mockAccount.completeResetPassword.mockResolvedValue({
84+
uid: MOCK_UID,
85+
sessionToken: 'sessionToken123',
86+
sessionVerified: true,
87+
keyFetchToken: 'keyFetchToken123',
88+
unwrapBKey: 'unwrapBKey123',
89+
authAt: Date.now(),
90+
});
91+
92+
(useFinishOAuthFlowHandler as jest.Mock).mockReturnValue({
93+
finishOAuthFlowHandler: jest
94+
.fn()
95+
.mockResolvedValue(MOCK_OAUTH_FLOW_HANDLER_RESPONSE),
96+
oAuthDataError: null,
97+
});
98+
});
99+
100+
afterEach(() => {
101+
jest.clearAllMocks();
102+
fxaLoginSignedInUserSpy.mockRestore();
103+
});
104+
105+
it('sends webchannel message and navigates to settings for relay integration', async () => {
106+
renderWithLocalizationProvider(
107+
<LocationProvider>
108+
<CompleteResetPasswordContainer
109+
integration={mockOAuthNativeSigninIntegration(false) as Integration}
110+
/>
111+
</LocationProvider>
112+
);
113+
114+
expect(await screen.findByLabelText('New password')).toBeInTheDocument();
115+
116+
const user = userEvent.setup();
117+
const newPasswordInput = screen.getByLabelText('New password');
118+
const confirmPasswordInput = screen.getByLabelText('Confirm password');
119+
120+
await user.type(newPasswordInput, 'newPassword123!');
121+
await user.type(confirmPasswordInput, 'newPassword123!');
122+
123+
const submitButton = screen.getByRole('button', {
124+
name: 'Create new password',
125+
});
126+
await user.click(submitButton);
127+
128+
await waitFor(() => {
129+
expect(mockAccount.completeResetPassword).toHaveBeenCalled();
130+
});
131+
132+
expect(fxaLoginSignedInUserSpy).toHaveBeenCalledWith(
133+
expect.objectContaining({
134+
services: { relay: {} },
135+
})
136+
);
137+
138+
await waitFor(() => {
139+
expect(mockNavigateWithQuery).toHaveBeenCalledWith(SETTINGS_PATH, {
140+
replace: true,
141+
});
142+
});
143+
144+
expect(mockAlertBar.success).toHaveBeenCalledWith(
145+
'Your password has been reset'
146+
);
147+
});
148+
});

packages/fxa-settings/src/pages/ResetPassword/CompleteResetPassword/container.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ const CompleteResetPasswordContainer = ({
126126
) => {
127127
if (accountResetData.sessionVerified) {
128128
// For verified users with OAuth integration, navigate to confirmation page then to the relying party
129-
if (isOAuth && !integration.isSync()) {
129+
if (
130+
isOAuth &&
131+
!(integration.isSync() || integration.isFirefoxNonSync())
132+
) {
130133
sensitiveDataClient.setDataType(SensitiveData.Key.AccountReset, {
131134
keyFetchToken: accountResetData.keyFetchToken,
132135
unwrapBKey: accountResetData.unwrapBKey,
@@ -136,7 +139,7 @@ const CompleteResetPasswordContainer = ({
136139
});
137140
}
138141

139-
// For web integration and sync navigate to settings
142+
// For web integration and sync/relay/smart window navigate to settings
140143
// Sync users will see an account recovery key promotion banner in settings
141144
// if they don't have one configured
142145
alertBar.success(
@@ -226,7 +229,7 @@ const CompleteResetPasswordContainer = ({
226229

227230
// This handles the sync desktop v3 case and the sync oauth_webchannel_v1 case.
228231
// Other oauth flows are handled in the next step.
229-
if (integration.isSync()) {
232+
if (integration.isSync() || integration.isFirefoxNonSync()) {
230233
firefox.fxaLoginSignedInUser({
231234
authAt: accountResetData.authAt,
232235
email,

0 commit comments

Comments
 (0)