Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add Reset Password e2e tests #1270

Merged
merged 5 commits into from
Jul 30, 2024
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
6 changes: 5 additions & 1 deletion frontend/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
},
Expand Down
119 changes: 119 additions & 0 deletions frontend/tests/reset-password.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
59 changes: 59 additions & 0 deletions frontend/tests/utils/mailcatcher.ts
Original file line number Diff line number Diff line change
@@ -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<never>((_, 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()])
}
38 changes: 38 additions & 0 deletions frontend/tests/utils/user.ts
Original file line number Diff line number Diff line change
@@ -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")
}