Skip to content

Commit 8c5c4e8

Browse files
test(mosaic): cover user profile MFA and profile paths that no test caught (#10472)
1 parent 2409217 commit 8c5c4e8

3 files changed

Lines changed: 150 additions & 1 deletion

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa.feature.test.tsx‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,83 @@ describe('User profile MFA', () => {
178178
const fapi = await renderMfa(fapiUser({ id: 'user_1' }), environment);
179179
const user = userEvent.setup();
180180
await user.click(screen.getByRole('button', { name: 'Add verification method' }));
181+
expect(screen.queryByRole('button', { name: /SMS verification/ })).toBeNull();
181182
await user.click(screen.getByRole('button', { name: /Authenticator app/ }));
182183
await user.type(await screen.findByRole('textbox', { name: 'Verification code' }), '123456');
183184
expect(await screen.findByText('CODE0001')).toBeVisible();
184185
expect(fapi.mfa.backupCodeCreations).toBe(0);
185186
});
186187

188+
it('offers only the methods the user has not set up yet', async () => {
189+
const reserved = fapiPhoneNumber({ ...phone, reserved_for_second_factor: true, default_second_factor: true });
190+
await renderMfa(
191+
fapiUser({
192+
id: 'user_1',
193+
phone_numbers: [reserved],
194+
totp_enabled: true,
195+
backup_code_enabled: true,
196+
two_factor_enabled: true,
197+
}),
198+
);
199+
const user = userEvent.setup();
200+
await user.click(screen.getByRole('button', { name: 'Add verification method' }));
201+
await waitFor(() => expect(screen.getByRole('button', { name: /^SMS verification/ })).toBeVisible());
202+
expect(screen.queryByRole('button', { name: /^Authenticator app/ })).toBeNull();
203+
expect(screen.queryByRole('button', { name: /^Backup codes/ })).toBeNull();
204+
});
205+
206+
it('lists only phones that are not already used for SMS verification', async () => {
207+
const reserved = fapiPhoneNumber({ ...phone, reserved_for_second_factor: true, default_second_factor: true });
208+
const other = fapiPhoneNumber({ ...phone, id: 'phone_2', phone_number: '+15555550202' });
209+
await renderMfa(fapiUser({ id: 'user_1', phone_numbers: [reserved, other], two_factor_enabled: true }));
210+
const user = userEvent.setup();
211+
await user.click(screen.getByRole('button', { name: 'Add verification method' }));
212+
await user.click(screen.getByRole('button', { name: /^SMS verification/ }));
213+
await user.click(screen.getByRole('combobox', { name: /Phone number/ }));
214+
const options = await screen.findAllByRole('option');
215+
expect(options.map(option => option.textContent)).toEqual(['+1 (555) 555-0202']);
216+
});
217+
218+
it('lets either factor be removed when a verified SMS phone backs up the authenticator', async () => {
219+
const environment = mfaEnvironment();
220+
environment.user_settings.sign_up.mfa = { required: true };
221+
const reserved = fapiPhoneNumber({ ...phone, reserved_for_second_factor: true, default_second_factor: true });
222+
await renderMfa(
223+
fapiUser({ id: 'user_1', phone_numbers: [reserved], totp_enabled: true, two_factor_enabled: true }),
224+
environment,
225+
);
226+
expect(screen.getByRole('button', { name: 'Manage Authenticator app' })).toBeVisible();
227+
expect(screen.getByRole('button', { name: 'Manage SMS verification +1 (555) 555-0101' })).toBeVisible();
228+
});
229+
230+
it('does not count an unverified SMS phone toward required verification', async () => {
231+
const environment = mfaEnvironment();
232+
environment.user_settings.sign_up.mfa = { required: true };
233+
const unverified = fapiPhoneNumber({
234+
...phone,
235+
reserved_for_second_factor: true,
236+
verification: { ...phone.verification, status: 'unverified' },
237+
});
238+
await renderMfa(
239+
fapiUser({ id: 'user_1', phone_numbers: [unverified], totp_enabled: true, two_factor_enabled: true }),
240+
environment,
241+
);
242+
expect(screen.getByText('Authenticator app')).toBeVisible();
243+
expect(screen.queryByText('SMS verification')).toBeNull();
244+
expect(screen.queryByRole('button', { name: 'Manage Authenticator app' })).toBeNull();
245+
});
246+
247+
it('hides existing backup codes when the instance turns them off', async () => {
248+
const environment = mfaEnvironment();
249+
environment.user_settings.attributes.backup_code.used_for_second_factor = false;
250+
await renderMfa(
251+
fapiUser({ id: 'user_1', totp_enabled: true, backup_code_enabled: true, two_factor_enabled: true }),
252+
environment,
253+
);
254+
expect(screen.getByText('Authenticator app')).toBeVisible();
255+
expect(screen.queryByText('Backup codes')).toBeNull();
256+
});
257+
187258
it('shows a localized incorrect TOTP code and allows a retry with the same secret', async () => {
188259
const fapi = await renderMfa();
189260
const user = userEvent.setup();

‎packages/mosaic/src/features/user-profile/user-profile-profile-section/__tests__/user-profile-profile-section.feature.test.tsx‎

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,53 @@ describe('the user profile profile section', () => {
7878
const dialog = screen.getByRole('dialog', { name: 'Edit name' });
7979
await actor.clear(within(dialog).getByLabelText('First name'));
8080
await actor.type(within(dialog).getByLabelText('First name'), 'Alicia');
81+
await actor.clear(within(dialog).getByLabelText('Last name'));
82+
await actor.type(within(dialog).getByLabelText('Last name'), 'Jones');
8183
await actor.click(within(dialog).getByRole('button', { name: 'Save changes' }));
8284

8385
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
84-
expect(screen.getByText('Alicia Smith')).toBeInTheDocument();
86+
expect(screen.getByText('Alicia Jones')).toBeInTheDocument();
87+
});
88+
89+
it('puts a last name the server rejects on the last name field', async () => {
90+
const { actor } = await renderSection();
91+
failsWith(
92+
'/v1/me',
93+
{
94+
code: 'form_param_format_invalid',
95+
message: 'Invalid',
96+
long_message: 'Last name contains invalid characters.',
97+
meta: { param_name: 'last_name' },
98+
},
99+
422,
100+
);
101+
102+
await actor.click(screen.getByRole('button', { name: 'Edit name' }));
103+
const dialog = screen.getByRole('dialog', { name: 'Edit name' });
104+
await actor.type(within(dialog).getByLabelText('Last name'), '!');
105+
await actor.click(within(dialog).getByRole('button', { name: 'Save changes' }));
106+
107+
const lastName = within(dialog).getByLabelText('Last name');
108+
await waitFor(() => expect(lastName).toHaveAccessibleDescription('Last name contains invalid characters.'));
109+
expect(lastName).toHaveAttribute('aria-invalid', 'true');
110+
expect(within(dialog).getByLabelText('First name')).not.toHaveAttribute('aria-invalid', 'true');
111+
});
112+
113+
it('keeps Save disabled while a required half of the name is empty', async () => {
114+
const { actor } = await renderSection(
115+
signedIn(fapiEnvironment({ attributes: { first_name: { required: true }, last_name: { required: true } } })),
116+
);
117+
118+
await actor.click(screen.getByRole('button', { name: 'Edit name' }));
119+
const dialog = screen.getByRole('dialog', { name: 'Edit name' });
120+
const save = within(dialog).getByRole('button', { name: 'Save changes' });
121+
await actor.clear(within(dialog).getByLabelText(/^First name/));
122+
expect(save).toHaveAttribute('aria-disabled', 'true');
123+
await actor.type(within(dialog).getByLabelText(/^First name/), 'Alicia');
124+
await actor.clear(within(dialog).getByLabelText(/^Last name/));
125+
expect(save).toHaveAttribute('aria-disabled', 'true');
126+
await actor.type(within(dialog).getByLabelText(/^Last name/), 'Jones');
127+
expect(save).not.toHaveAttribute('aria-disabled', 'true');
85128
});
86129

87130
it('leaves the name out when the instance collects neither half of it', async () => {
@@ -223,6 +266,39 @@ describe('the user profile profile section', () => {
223266
expect(profile()).toHaveTextContent('alicia');
224267
});
225268

269+
it('keeps Save disabled while a required username is empty', async () => {
270+
const { actor } = await renderSection(withUsername('alicesmith', { required: true }));
271+
272+
await actor.click(within(profile()).getByRole('button', { name: 'Edit username' }));
273+
const dialog = screen.getByRole('dialog', { name: 'Edit username' });
274+
await actor.clear(within(dialog).getByLabelText(/^Username/));
275+
276+
expect(within(dialog).getByRole('button', { name: 'Save changes' })).toHaveAttribute('aria-disabled', 'true');
277+
});
278+
279+
it('tells the user the username length the instance allows', async () => {
280+
const { actor } = await renderSection({
281+
...withUsername('alicesmith'),
282+
environment: fapiEnvironment({ user_settings: { username_settings: { min_length: 5, max_length: 20 } } }),
283+
});
284+
failsWith(
285+
'/v1/me',
286+
{ code: 'form_username_invalid_length', message: 'Invalid length', meta: { param_name: 'username' } },
287+
422,
288+
);
289+
290+
await actor.click(within(profile()).getByRole('button', { name: 'Edit username' }));
291+
const dialog = screen.getByRole('dialog', { name: 'Edit username' });
292+
await actor.type(within(dialog).getByLabelText('Username'), '2');
293+
await actor.click(within(dialog).getByRole('button', { name: 'Save changes' }));
294+
295+
await waitFor(() =>
296+
expect(within(dialog).getByLabelText('Username')).toHaveAccessibleDescription(
297+
'Your username must be between 5 and 20 characters long.',
298+
),
299+
);
300+
});
301+
226302
it('offers to set a username the user has not picked yet', async () => {
227303
await renderSection(withUsername(null));
228304

0 commit comments

Comments
 (0)