import { test, expect } from './fixtures'; import { giveCustomerAPasskey, readPasswordResetToken } from './support/db'; const NEW_PASSWORD = 'a-brand-new-password'; test.describe('Password reset', () => { test('the login page offers a way to recover a forgotten password', async ({ page, authModal, passwordReset }) => { await authModal.gotoLogIn(); // Recovery is reached from the login modal rather than a link on a page, // and its title is the modal's rather than a heading. await authModal.forgotPasswordButton.click(); await expect(page).toHaveURL(/\/forgot-password/); await expect(passwordReset.requestDialog).toBeVisible(); }); test('requesting a reset confirms without revealing whether the account exists', async ({ page, passwordReset }) => { await passwordReset.gotoRequest(); await passwordReset.requestLinkFor('definitely-nobody@example.com'); // Identical wording either way; a differing message would make this an // account-enumeration oracle. await expect(page.getByText(/If an account exists/)).toBeVisible(); }); test('a reset link with no token explains itself instead of failing on submit', async ({ page, passwordReset }) => { await passwordReset.gotoReset(); await expect(page.getByText('This link is incomplete')).toBeVisible(); await expect(passwordReset.setNewPasswordButton).toHaveCount(0); }); test('rejects a mismatched confirmation before contacting the server', async ({ page, passwordReset }) => { await passwordReset.gotoReset('whatever'); await passwordReset.setNewPassword(NEW_PASSWORD, 'something-else-entirely'); await expect(page.getByText('The passwords do not match')).toBeVisible(); }); test('reports an invalid token rather than appearing to succeed', async ({ page, passwordReset }) => { await passwordReset.gotoReset('not-a-real-token'); await passwordReset.setNewPassword(NEW_PASSWORD); await expect(page.getByText('invalid or expired token')).toBeVisible(); await expect(page).toHaveURL(/\/reset-password/); }); test('a customer can reset their password and sign in with the new one', async ({ request, customer, accountModal, header, authModal, passwordReset }) => { await accountModal.openAndLogOut(); await expect(header.logInButton).toBeVisible(); // The reset link arrives by email, which the tests can't read. Request the // reset through the real endpoint, then read the issued token the way the // customer's mail client would deliver it. const requested = await request.post('/api/customers/request-password-reset', { data: { email: customer.email } }); expect(requested.ok()).toBeTruthy(); await passwordReset.gotoReset(await readPasswordResetToken(customer.email)); await passwordReset.setNewPassword(NEW_PASSWORD); // The reset signs them in and closes back to the storefront — the link came // from an email, so there is no page behind it to return to. await header.waitForSignedIn(); await accountModal.open(); await expect(accountModal.emailText(customer.email)).toBeVisible(); // And the new password actually works on a fresh sign-in. await accountModal.logOut(); await expect(header.logInButton).toBeVisible(); await authModal.gotoLogIn(); await authModal.logIn(customer.email, NEW_PASSWORD); await header.waitForSignedIn(); }); test('the old password stops working after a reset', async ({ page, request, customer, accountModal, header, authModal }) => { await accountModal.openAndLogOut(); await expect(header.logInButton).toBeVisible(); await request.post('/api/customers/request-password-reset', { data: { email: customer.email } }); const token = await readPasswordResetToken(customer.email); await request.post('/api/customers/reset-password', { data: { token, password: NEW_PASSWORD } }); await authModal.gotoLogIn(); await authModal.logIn(customer.email, customer.password); await expect(page.getByText('invalid email or password')).toBeVisible(); await expect(header.myAccountButton).toHaveCount(0); }); // #42. A reset removes every passkey on the account, which is the one thing // it does that a customer cannot undo and might have chosen differently // about, so both halves of telling them are asserted here. test('the reset form says passkeys will be removed before the customer commits', async ({ passwordReset, page }) => { await passwordReset.gotoReset('any-token-will-do'); // Said unconditionally, and it has to be: this form has no session and is // never told whether the account has passkeys, because answering that would // make the reset page an oracle for it. So the warning shows on a token // that was never issued, exactly as it would on a real one. await expect(page.getByText(/passkeys saved on this account will be removed/i)).toBeVisible(); }); test('a reset that removed a passkey says so, and waits to be acknowledged', async ({ page, request, customer, accountModal, header, passwordReset }) => { await giveCustomerAPasskey(customer.email); await accountModal.openAndLogOut(); await expect(header.logInButton).toBeVisible(); await request.post('/api/customers/request-password-reset', { data: { email: customer.email } }); await passwordReset.gotoReset(await readPasswordResetToken(customer.email)); await passwordReset.setNewPassword(NEW_PASSWORD); // A toast would be the wrong shape: it dismisses itself, and a customer // told that a credential they do not remember registering has just been // deleted needs to still be looking at that when they decide what to do. await expect(page.getByText('Your saved passkey was removed')).toBeVisible(); await page.getByRole('button', { name: 'Done' }).click(); // The reset still succeeded — this was a notice, not a step that failed. await header.waitForSignedIn(); }); });