diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 6b2499fee8..20f09d1103 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -34,7 +34,11 @@ const useAuth = () => { onSuccess: () => { navigate({ to: "/login" }) - showToast("Success!", "User created successfully.", "success") + showToast( + "Account created.", + "Your account has been created successfully.", + "success", + ) }, onError: (err: ApiError) => { let errDetail = (err.body as any)?.detail diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx index 767451eaae..11bc552d9b 100644 --- a/frontend/src/routes/reset-password.tsx +++ b/frontend/src/routes/reset-password.tsx @@ -60,7 +60,7 @@ function ResetPassword() { const mutation = useMutation({ mutationFn: resetPassword, onSuccess: () => { - showToast("Success!", "Password updated.", "success") + showToast("Success!", "Password updated successfully.", "success") reset() navigate({ to: "/login" }) }, diff --git a/frontend/tests/reset-password.spec.ts b/frontend/tests/reset-password.spec.ts new file mode 100644 index 0000000000..59cbceeefb --- /dev/null +++ b/frontend/tests/reset-password.spec.ts @@ -0,0 +1,119 @@ +import { expect, test } from "@playwright/test" +import { findLastEmail } from "./utils/mailcatcher" +import { randomEmail } from "./utils/random" +import { logInUser, signUpNewUser } from "./utils/user" + +test.use({ storageState: { cookies: [], origins: [] } }) + +test("Password Recovery title is visible", async ({ page }) => { + await page.goto("/recover-password") + + await expect( + page.getByRole("heading", { name: "Password Recovery" }), + ).toBeVisible() +}) + +test("Input is visible, empty and editable", async ({ page }) => { + await page.goto("/recover-password") + + await expect(page.getByPlaceholder("Email")).toBeVisible() + await expect(page.getByPlaceholder("Email")).toHaveText("") + await expect(page.getByPlaceholder("Email")).toBeEditable() +}) + +test("Continue button is visible", async ({ page }) => { + await page.goto("/recover-password") + + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible() +}) + +test("User can reset password successfully using the link", async ({ + page, + request, +}) => { + const full_name = "Test User" + const email = randomEmail() + const password = "changethis" + const new_password = "changethat" + + // Sign up a new user + await signUpNewUser(page, full_name, email, password) + + await page.goto("/recover-password") + await page.getByPlaceholder("Email").fill(email) + + await page.getByRole("button", { name: "Continue" }).click() + + const emailData = await findLastEmail({ + request, + filter: (e) => e.recipients.includes(`<${email}>`), + timeout: 5000, + }) + + await page.goto(`http://localhost:1080/messages/${emailData.id}.html`) + + const selector = 'a[href*="/reset-password?token="]' + + let url = await page.getAttribute(selector, "href") + + // TODO: update var instead of doing a replace + url = url!.replace("http://localhost/", "http://localhost:5173/") + + // Set the new password and confirm it + await page.goto(url) + + await page.getByLabel("Set Password").fill(new_password) + await page.getByLabel("Confirm Password").fill(new_password) + await page.getByRole("button", { name: "Reset Password" }).click() + await expect(page.getByText("Password updated successfully")).toBeVisible() + + // Check if the user is able to login with the new password + await logInUser(page, email, new_password) +}) + +test("Expired or invalid reset link", async ({ page }) => { + const invalidUrl = "/reset-password?token=invalidtoken" + + await page.goto(invalidUrl) + + await page.getByLabel("Set Password").fill("newpassword") + await page.getByLabel("Confirm Password").fill("newpassword") + await page.getByRole("button", { name: "Reset Password" }).click() + + await expect(page.getByText("Invalid token")).toBeVisible() +}) + +test("Weak new password validation", async ({ page, request }) => { + const full_name = "Test User" + const email = randomEmail() + const password = "password" + + // Sign up a new user + await signUpNewUser(page, full_name, email, password) + + await page.goto("/recover-password") + await page.getByPlaceholder("Email").fill(email) + await page.getByRole("button", { name: "Continue" }).click() + + const emailData = await findLastEmail({ + request, + filter: (e) => e.recipients.includes(`<${email}>`), + timeout: 5000, + }) + + await page.goto(`http://localhost:1080/messages/${emailData.id}.html`) + + const selector = 'a[href*="/reset-password?token="]' + let url = await page.getAttribute(selector, "href") + url = url!.replace("http://localhost/", "http://localhost:5173/") + + // Set a weak new password + await page.goto(url) + await page.getByLabel("Set Password").fill("123") + await page.getByLabel("Confirm Password").fill("123") + await page.getByRole("button", { name: "Reset Password" }).click() + + await expect( + page.getByText("Password must be at least 8 characters"), + ).toBeVisible() +}) diff --git a/frontend/tests/utils/mailcatcher.ts b/frontend/tests/utils/mailcatcher.ts new file mode 100644 index 0000000000..601ce434fb --- /dev/null +++ b/frontend/tests/utils/mailcatcher.ts @@ -0,0 +1,59 @@ +import type { APIRequestContext } from "@playwright/test" + +type Email = { + id: number + recipients: string[] + subject: string +} + +async function findEmail({ + request, + filter, +}: { request: APIRequestContext; filter?: (email: Email) => boolean }) { + const response = await request.get("http://localhost:1080/messages") + + let emails = await response.json() + + if (filter) { + emails = emails.filter(filter) + } + + const email = emails[emails.length - 1] + + if (email) { + return email as Email + } + + return null +} + +export function findLastEmail({ + request, + filter, + timeout = 5000, +}: { + request: APIRequestContext + filter?: (email: Email) => boolean + timeout?: number +}) { + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Timeout while trying to get latest email")), + timeout, + ), + ) + + const checkEmails = async () => { + while (true) { + const emailData = await findEmail({ request, filter }) + + if (emailData) { + return emailData + } + // Wait for 100ms before checking again + await new Promise((resolve) => setTimeout(resolve, 100)) + } + } + + return Promise.race([timeoutPromise, checkEmails()]) +} diff --git a/frontend/tests/utils/user.ts b/frontend/tests/utils/user.ts new file mode 100644 index 0000000000..6f02c0e8f3 --- /dev/null +++ b/frontend/tests/utils/user.ts @@ -0,0 +1,38 @@ +import { type Page, expect } from "@playwright/test" + +export async function signUpNewUser( + page: Page, + name: string, + email: string, + password: string, +) { + await page.goto("/signup") + + await page.getByPlaceholder("Full Name").fill(name) + await page.getByPlaceholder("Email").fill(email) + await page.getByPlaceholder("Password", { exact: true }).fill(password) + await page.getByPlaceholder("Repeat Password").fill(password) + await page.getByRole("button", { name: "Sign Up" }).click() + await expect( + page.getByText("Your account has been created successfully"), + ).toBeVisible() + await page.goto("/login") +} + +export async function logInUser(page: Page, email: string, password: string) { + await page.goto("/login") + + await page.getByPlaceholder("Email").fill(email) + await page.getByPlaceholder("Password", { exact: true }).fill(password) + await page.getByRole("button", { name: "Log In" }).click() + await page.waitForURL("/") + await expect( + page.getByText("Welcome back, nice to see you again!"), + ).toBeVisible() +} + +export async function logOutUser(page: Page, name: string) { + await page.getByRole("button", { name: name }).click() + await page.getByRole("menuitem", { name: "Log out" }).click() + await page.goto("/login") +}