Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-user-profile-mutation-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,83 @@ describe('User profile MFA', () => {
const fapi = await renderMfa(fapiUser({ id: 'user_1' }), environment);
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: 'Add verification method' }));
expect(screen.queryByRole('button', { name: /SMS verification/ })).toBeNull();
await user.click(screen.getByRole('button', { name: /Authenticator app/ }));
await user.type(await screen.findByRole('textbox', { name: 'Verification code' }), '123456');
expect(await screen.findByText('CODE0001')).toBeVisible();
expect(fapi.mfa.backupCodeCreations).toBe(0);
});

it('offers only the methods the user has not set up yet', async () => {
const reserved = fapiPhoneNumber({ ...phone, reserved_for_second_factor: true, default_second_factor: true });
await renderMfa(
fapiUser({
id: 'user_1',
phone_numbers: [reserved],
totp_enabled: true,
backup_code_enabled: true,
two_factor_enabled: true,
}),
);
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: 'Add verification method' }));
await waitFor(() => expect(screen.getByRole('button', { name: /^SMS verification/ })).toBeVisible());
expect(screen.queryByRole('button', { name: /^Authenticator app/ })).toBeNull();
expect(screen.queryByRole('button', { name: /^Backup codes/ })).toBeNull();
});

it('lists only phones that are not already used for SMS verification', async () => {
const reserved = fapiPhoneNumber({ ...phone, reserved_for_second_factor: true, default_second_factor: true });
const other = fapiPhoneNumber({ ...phone, id: 'phone_2', phone_number: '+15555550202' });
await renderMfa(fapiUser({ id: 'user_1', phone_numbers: [reserved, other], two_factor_enabled: true }));
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: 'Add verification method' }));
await user.click(screen.getByRole('button', { name: /^SMS verification/ }));
await user.click(screen.getByRole('combobox', { name: /Phone number/ }));
const options = await screen.findAllByRole('option');
expect(options.map(option => option.textContent)).toEqual(['+1 (555) 555-0202']);
});

it('lets either factor be removed when a verified SMS phone backs up the authenticator', async () => {
const environment = mfaEnvironment();
environment.user_settings.sign_up.mfa = { required: true };
const reserved = fapiPhoneNumber({ ...phone, reserved_for_second_factor: true, default_second_factor: true });
await renderMfa(
fapiUser({ id: 'user_1', phone_numbers: [reserved], totp_enabled: true, two_factor_enabled: true }),
environment,
);
expect(screen.getByRole('button', { name: 'Manage Authenticator app' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Manage SMS verification +1 (555) 555-0101' })).toBeVisible();
});

it('does not count an unverified SMS phone toward required verification', async () => {
const environment = mfaEnvironment();
environment.user_settings.sign_up.mfa = { required: true };
const unverified = fapiPhoneNumber({
...phone,
reserved_for_second_factor: true,
verification: { ...phone.verification, status: 'unverified' },
});
await renderMfa(
fapiUser({ id: 'user_1', phone_numbers: [unverified], totp_enabled: true, two_factor_enabled: true }),
environment,
);
expect(screen.getByText('Authenticator app')).toBeVisible();
expect(screen.queryByText('SMS verification')).toBeNull();
expect(screen.queryByRole('button', { name: 'Manage Authenticator app' })).toBeNull();
});

it('hides existing backup codes when the instance turns them off', async () => {
const environment = mfaEnvironment();
environment.user_settings.attributes.backup_code.used_for_second_factor = false;
await renderMfa(
fapiUser({ id: 'user_1', totp_enabled: true, backup_code_enabled: true, two_factor_enabled: true }),
environment,
);
expect(screen.getByText('Authenticator app')).toBeVisible();
expect(screen.queryByText('Backup codes')).toBeNull();
});

it('shows a localized incorrect TOTP code and allows a retry with the same secret', async () => {
const fapi = await renderMfa();
const user = userEvent.setup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,53 @@ describe('the user profile profile section', () => {
const dialog = screen.getByRole('dialog', { name: 'Edit name' });
await actor.clear(within(dialog).getByLabelText('First name'));
await actor.type(within(dialog).getByLabelText('First name'), 'Alicia');
await actor.clear(within(dialog).getByLabelText('Last name'));
await actor.type(within(dialog).getByLabelText('Last name'), 'Jones');
await actor.click(within(dialog).getByRole('button', { name: 'Save changes' }));

await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(screen.getByText('Alicia Smith')).toBeInTheDocument();
expect(screen.getByText('Alicia Jones')).toBeInTheDocument();
});

it('puts a last name the server rejects on the last name field', async () => {
const { actor } = await renderSection();
failsWith(
'/v1/me',
{
code: 'form_param_format_invalid',
message: 'Invalid',
long_message: 'Last name contains invalid characters.',
meta: { param_name: 'last_name' },
},
422,
);

await actor.click(screen.getByRole('button', { name: 'Edit name' }));
const dialog = screen.getByRole('dialog', { name: 'Edit name' });
await actor.type(within(dialog).getByLabelText('Last name'), '!');
await actor.click(within(dialog).getByRole('button', { name: 'Save changes' }));

const lastName = within(dialog).getByLabelText('Last name');
await waitFor(() => expect(lastName).toHaveAccessibleDescription('Last name contains invalid characters.'));
expect(lastName).toHaveAttribute('aria-invalid', 'true');
expect(within(dialog).getByLabelText('First name')).not.toHaveAttribute('aria-invalid', 'true');
});

it('keeps Save disabled while a required half of the name is empty', async () => {
const { actor } = await renderSection(
signedIn(fapiEnvironment({ attributes: { first_name: { required: true }, last_name: { required: true } } })),
);

await actor.click(screen.getByRole('button', { name: 'Edit name' }));
const dialog = screen.getByRole('dialog', { name: 'Edit name' });
const save = within(dialog).getByRole('button', { name: 'Save changes' });
await actor.clear(within(dialog).getByLabelText(/^First name/));
expect(save).toHaveAttribute('aria-disabled', 'true');
await actor.type(within(dialog).getByLabelText(/^First name/), 'Alicia');
await actor.clear(within(dialog).getByLabelText(/^Last name/));
expect(save).toHaveAttribute('aria-disabled', 'true');
await actor.type(within(dialog).getByLabelText(/^Last name/), 'Jones');
expect(save).not.toHaveAttribute('aria-disabled', 'true');
});

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

it('keeps Save disabled while a required username is empty', async () => {
const { actor } = await renderSection(withUsername('alicesmith', { required: true }));

await actor.click(within(profile()).getByRole('button', { name: 'Edit username' }));
const dialog = screen.getByRole('dialog', { name: 'Edit username' });
await actor.clear(within(dialog).getByLabelText(/^Username/));

expect(within(dialog).getByRole('button', { name: 'Save changes' })).toHaveAttribute('aria-disabled', 'true');
});

it('tells the user the username length the instance allows', async () => {
const { actor } = await renderSection({
...withUsername('alicesmith'),
environment: fapiEnvironment({ user_settings: { username_settings: { min_length: 5, max_length: 20 } } }),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
failsWith(
'/v1/me',
{ code: 'form_username_invalid_length', message: 'Invalid length', meta: { param_name: 'username' } },
422,
);

await actor.click(within(profile()).getByRole('button', { name: 'Edit username' }));
const dialog = screen.getByRole('dialog', { name: 'Edit username' });
await actor.type(within(dialog).getByLabelText('Username'), '2');
await actor.click(within(dialog).getByRole('button', { name: 'Save changes' }));

await waitFor(() =>
expect(within(dialog).getByLabelText('Username')).toHaveAccessibleDescription(
'Your username must be between 5 and 20 characters long.',
),
);
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
it('offers to set a username the user has not picked yet', async () => {
await renderSection(withUsername(null));

Expand Down
Loading