Skip to content

Commit 1e9e561

Browse files
authored
✨ Add Reset Password e2e tests (#1270)
1 parent 68b86b1 commit 1e9e561

File tree

5 files changed

+222
-2
lines changed

5 files changed

+222
-2
lines changed

frontend/src/hooks/useAuth.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ const useAuth = () => {
3434

3535
onSuccess: () => {
3636
navigate({ to: "/login" })
37-
showToast("Success!", "User created successfully.", "success")
37+
showToast(
38+
"Account created.",
39+
"Your account has been created successfully.",
40+
"success",
41+
)
3842
},
3943
onError: (err: ApiError) => {
4044
let errDetail = (err.body as any)?.detail

frontend/src/routes/reset-password.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function ResetPassword() {
6060
const mutation = useMutation({
6161
mutationFn: resetPassword,
6262
onSuccess: () => {
63-
showToast("Success!", "Password updated.", "success")
63+
showToast("Success!", "Password updated successfully.", "success")
6464
reset()
6565
navigate({ to: "/login" })
6666
},
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { expect, test } from "@playwright/test"
2+
import { findLastEmail } from "./utils/mailcatcher"
3+
import { randomEmail } from "./utils/random"
4+
import { logInUser, signUpNewUser } from "./utils/user"
5+
6+
test.use({ storageState: { cookies: [], origins: [] } })
7+
8+
test("Password Recovery title is visible", async ({ page }) => {
9+
await page.goto("/recover-password")
10+
11+
await expect(
12+
page.getByRole("heading", { name: "Password Recovery" }),
13+
).toBeVisible()
14+
})
15+
16+
test("Input is visible, empty and editable", async ({ page }) => {
17+
await page.goto("/recover-password")
18+
19+
await expect(page.getByPlaceholder("Email")).toBeVisible()
20+
await expect(page.getByPlaceholder("Email")).toHaveText("")
21+
await expect(page.getByPlaceholder("Email")).toBeEditable()
22+
})
23+
24+
test("Continue button is visible", async ({ page }) => {
25+
await page.goto("/recover-password")
26+
27+
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible()
28+
})
29+
30+
test("User can reset password successfully using the link", async ({
31+
page,
32+
request,
33+
}) => {
34+
const full_name = "Test User"
35+
const email = randomEmail()
36+
const password = "changethis"
37+
const new_password = "changethat"
38+
39+
// Sign up a new user
40+
await signUpNewUser(page, full_name, email, password)
41+
42+
await page.goto("/recover-password")
43+
await page.getByPlaceholder("Email").fill(email)
44+
45+
await page.getByRole("button", { name: "Continue" }).click()
46+
47+
const emailData = await findLastEmail({
48+
request,
49+
filter: (e) => e.recipients.includes(`<${email}>`),
50+
timeout: 5000,
51+
})
52+
53+
await page.goto(`http://localhost:1080/messages/${emailData.id}.html`)
54+
55+
const selector = 'a[href*="/reset-password?token="]'
56+
57+
let url = await page.getAttribute(selector, "href")
58+
59+
// TODO: update var instead of doing a replace
60+
url = url!.replace("http://localhost/", "http://localhost:5173/")
61+
62+
// Set the new password and confirm it
63+
await page.goto(url)
64+
65+
await page.getByLabel("Set Password").fill(new_password)
66+
await page.getByLabel("Confirm Password").fill(new_password)
67+
await page.getByRole("button", { name: "Reset Password" }).click()
68+
await expect(page.getByText("Password updated successfully")).toBeVisible()
69+
70+
// Check if the user is able to login with the new password
71+
await logInUser(page, email, new_password)
72+
})
73+
74+
test("Expired or invalid reset link", async ({ page }) => {
75+
const invalidUrl = "/reset-password?token=invalidtoken"
76+
77+
await page.goto(invalidUrl)
78+
79+
await page.getByLabel("Set Password").fill("newpassword")
80+
await page.getByLabel("Confirm Password").fill("newpassword")
81+
await page.getByRole("button", { name: "Reset Password" }).click()
82+
83+
await expect(page.getByText("Invalid token")).toBeVisible()
84+
})
85+
86+
test("Weak new password validation", async ({ page, request }) => {
87+
const full_name = "Test User"
88+
const email = randomEmail()
89+
const password = "password"
90+
91+
// Sign up a new user
92+
await signUpNewUser(page, full_name, email, password)
93+
94+
await page.goto("/recover-password")
95+
await page.getByPlaceholder("Email").fill(email)
96+
await page.getByRole("button", { name: "Continue" }).click()
97+
98+
const emailData = await findLastEmail({
99+
request,
100+
filter: (e) => e.recipients.includes(`<${email}>`),
101+
timeout: 5000,
102+
})
103+
104+
await page.goto(`http://localhost:1080/messages/${emailData.id}.html`)
105+
106+
const selector = 'a[href*="/reset-password?token="]'
107+
let url = await page.getAttribute(selector, "href")
108+
url = url!.replace("http://localhost/", "http://localhost:5173/")
109+
110+
// Set a weak new password
111+
await page.goto(url)
112+
await page.getByLabel("Set Password").fill("123")
113+
await page.getByLabel("Confirm Password").fill("123")
114+
await page.getByRole("button", { name: "Reset Password" }).click()
115+
116+
await expect(
117+
page.getByText("Password must be at least 8 characters"),
118+
).toBeVisible()
119+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { APIRequestContext } from "@playwright/test"
2+
3+
type Email = {
4+
id: number
5+
recipients: string[]
6+
subject: string
7+
}
8+
9+
async function findEmail({
10+
request,
11+
filter,
12+
}: { request: APIRequestContext; filter?: (email: Email) => boolean }) {
13+
const response = await request.get("http://localhost:1080/messages")
14+
15+
let emails = await response.json()
16+
17+
if (filter) {
18+
emails = emails.filter(filter)
19+
}
20+
21+
const email = emails[emails.length - 1]
22+
23+
if (email) {
24+
return email as Email
25+
}
26+
27+
return null
28+
}
29+
30+
export function findLastEmail({
31+
request,
32+
filter,
33+
timeout = 5000,
34+
}: {
35+
request: APIRequestContext
36+
filter?: (email: Email) => boolean
37+
timeout?: number
38+
}) {
39+
const timeoutPromise = new Promise<never>((_, reject) =>
40+
setTimeout(
41+
() => reject(new Error("Timeout while trying to get latest email")),
42+
timeout,
43+
),
44+
)
45+
46+
const checkEmails = async () => {
47+
while (true) {
48+
const emailData = await findEmail({ request, filter })
49+
50+
if (emailData) {
51+
return emailData
52+
}
53+
// Wait for 100ms before checking again
54+
await new Promise((resolve) => setTimeout(resolve, 100))
55+
}
56+
}
57+
58+
return Promise.race([timeoutPromise, checkEmails()])
59+
}

frontend/tests/utils/user.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { type Page, expect } from "@playwright/test"
2+
3+
export async function signUpNewUser(
4+
page: Page,
5+
name: string,
6+
email: string,
7+
password: string,
8+
) {
9+
await page.goto("/signup")
10+
11+
await page.getByPlaceholder("Full Name").fill(name)
12+
await page.getByPlaceholder("Email").fill(email)
13+
await page.getByPlaceholder("Password", { exact: true }).fill(password)
14+
await page.getByPlaceholder("Repeat Password").fill(password)
15+
await page.getByRole("button", { name: "Sign Up" }).click()
16+
await expect(
17+
page.getByText("Your account has been created successfully"),
18+
).toBeVisible()
19+
await page.goto("/login")
20+
}
21+
22+
export async function logInUser(page: Page, email: string, password: string) {
23+
await page.goto("/login")
24+
25+
await page.getByPlaceholder("Email").fill(email)
26+
await page.getByPlaceholder("Password", { exact: true }).fill(password)
27+
await page.getByRole("button", { name: "Log In" }).click()
28+
await page.waitForURL("/")
29+
await expect(
30+
page.getByText("Welcome back, nice to see you again!"),
31+
).toBeVisible()
32+
}
33+
34+
export async function logOutUser(page: Page, name: string) {
35+
await page.getByRole("button", { name: name }).click()
36+
await page.getByRole("menuitem", { name: "Log out" }).click()
37+
await page.goto("/login")
38+
}

0 commit comments

Comments
 (0)