diff --git a/ghibli-gif.gif b/ghibli-gif.gif new file mode 100644 index 0000000..0396b38 Binary files /dev/null and b/ghibli-gif.gif differ diff --git a/playwright/helpers/recycle.ts b/playwright/helpers/recycle.ts new file mode 100644 index 0000000..3394207 --- /dev/null +++ b/playwright/helpers/recycle.ts @@ -0,0 +1,31 @@ +import { test, expect, Page } from '@playwright/test'; +import {getAuthFileByProjectName, + getEmailAddressForProject, + restoreAuthState, + letCookiesExpire, + restoreExpiredCookies, + MailpitCodeFetcher} from "../utils/functions.ts" + + +export async function loginUser(page: Page, testInfo: any, password: string,){ + const emailAddress = getEmailAddressForProject(testInfo); + await page.goto("https://audaf-testing.onrender.com") + await page.getByPlaceholder("Enter email").click(); + await page.getByPlaceholder("Enter email").fill(emailAddress); + await page.getByPlaceholder("Password").click(); + await page.getByPlaceholder("Password").fill(password); + + // Handle reCAPTCHA + if (await page.$('iframe[src*="recaptcha"]')) { + await page.waitForFunction(() => {return document.querySelector('iframe[src*="recaptcha"]');}); + const frames = page.frames(); + const recaptchaFrame = frames.find(f => f.url().includes('recaptcha')); + if (recaptchaFrame) {await recaptchaFrame.getByRole('checkbox', { name: "I'm not a robot" }).click();} + }else{ + await page.waitForSelector('iframe[src*="recaptcha"]', { timeout: 10000 }); + const recaptchaFrame = page.frameLocator('iframe[src*="recaptcha"]'); + await recaptchaFrame.getByRole('checkbox', { name: "I'm not a robot" }).click(); + } + + await page.getByRole('button', { name: 'Sign In' }).click(); +} \ No newline at end of file diff --git a/playwright/helper/functions.ts b/playwright/utils/functions.ts similarity index 83% rename from playwright/helper/functions.ts rename to playwright/utils/functions.ts index ac14471..07ba24f 100644 --- a/playwright/helper/functions.ts +++ b/playwright/utils/functions.ts @@ -12,6 +12,9 @@ export function getAuthFileByProjectName(projectName: string) { } export function getAuthFileTamperedByProjectName(projectName: string) { + if(projectName !== "chromium" || "firefox"){ + projectName = "chromium" + } return path.join(authDir, `userTampered-${projectName.toLowerCase()}.json`); } @@ -73,7 +76,7 @@ export const restoreAuthState = async function(page: any, filePath: string) { } } -export const setUserTamperedToSameAsUser = async function(page: any, projectName: string) { +export const setUserTamperedToSameAsUser = async function(projectName: string) { const userFilePath = getAuthFileByProjectName(projectName); const tamperedFilePath = getAuthFileTamperedByProjectName(projectName); // Implementation for setting tampered user state to same as regular user @@ -83,6 +86,31 @@ export const setUserTamperedToSameAsUser = async function(page: any, projectName } } +const tampering = function(projectName: string, cookieName: string){ + const toBeTamperedFilePath = getAuthFileTamperedByProjectName(projectName); + + if (existsSync(toBeTamperedFilePath)) { + const raw = readFileSync(toBeTamperedFilePath, 'utf8'); + const data = JSON.parse(raw); + + const tokenCookie = data.cookies.find((el: any) => el.name === cookieName); + + if (tokenCookie) { + tokenCookie.value = "wrongtoken"; + writeFileSync(toBeTamperedFilePath, JSON.stringify(data, null, 2), 'utf8'); + } + } else { + console.log('File does not exist at path:', toBeTamperedFilePath); + } +} +export const tamperWithJWTToken = async function(projectName: string) { + tampering(projectName, "token"); +}; + +export const tamperWithCSRFToken = async function(projectName: string) { + tampering(projectName,"csrf-token-prod") +}; + export const letCookiesExpire = async function(page: any) { const context = page.context(); const cookies = await context.cookies(); diff --git a/tests/01-createAccount.spec.ts b/tests/01-createAccount.spec.ts index e1a39f0..9ffc050 100644 --- a/tests/01-createAccount.spec.ts +++ b/tests/01-createAccount.spec.ts @@ -7,7 +7,7 @@ import { MailpitCodeFetcher, readEmailData, saveEmailAddressForProject, getAuthFileByProjectName, - getAuthFileUnverified} from "../playwright/helper/functions.ts" + getAuthFileUnverified} from "../playwright/utils/functions.ts" @@ -79,6 +79,89 @@ test.describe.serial('create accounts and testing account creation with already await expect(page.getByText('Email already in use')).toBeVisible({ timeout: 15000 }); }); + test("trying to create account with mismatched password and confirm password", async({page}, testInfo)=>{ + const projectName = testInfo.project.name; + const emailAddress = `test-user-${projectName.toLowerCase()}-${Date.now()}@gamil.com`; + + await page.goto('https://audaf-testing.onrender.com/signup'); + await page.getByRole('textbox', { name: 'Password', exact: true }).click(); + await page.getByRole('textbox', { name: 'Password', exact: true }).fill('securepassword123'); + await page.getByRole('textbox', { name: 'Confirm Password' }).click(); + await page.getByRole('textbox', { name: 'Confirm Password' }).fill('mismatched1'); //<- mismatching + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('testusername2'); + await page.getByRole('textbox', { name: 'Email Address' }).click(); + await page.getByRole('textbox', { name: 'Email Address' }).fill(emailAddress); + await page.locator('input[type="file"]').setInputFiles('jellyfishWallpaper.jpg'); + await page.getByRole('button', { name: '-- Select country --' }).click(); + await page.getByRole('button', { name: 'Albania flag Albania' }).click(); + await expect(page.getByRole('button', { name: 'Create Account' })).toBeDisabled(); + }); + + test("trying to create account with invalid email format", async({page}, testInfo)=>{ + const projectName = testInfo.project.name; + const emailAddress = `test-user-${projectName.toLowerCase()}-${Date.now()}gamil.com`; // <- invalid format + + await page.goto('https://audaf-testing.onrender.com/signup'); + await page.getByRole('textbox', { name: 'Password', exact: true }).click(); + await page.getByRole('textbox', { name: 'Password', exact: true }).fill('securepassword123'); + await page.getByRole('textbox', { name: 'Confirm Password' }).click(); + await page.getByRole('textbox', { name: 'Confirm Password' }).fill('securepassword123'); + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('testusername2'); + await page.getByRole('textbox', { name: 'Email Address' }).click(); + await page.getByRole('textbox', { name: 'Email Address' }).fill(emailAddress); + await page.locator('input[type="file"]').setInputFiles('jellyfishWallpaper.jpg'); + await page.getByRole('button', { name: '-- Select country --' }).click(); + await page.getByRole('button', { name: 'Albania flag Albania' }).click(); + await expect(page.getByRole('button', { name: 'Create Account' })).toBeDisabled(); + }); + + test("trying to create account with weak password", async({page}, testInfo)=>{ + const projectName = testInfo.project.name; + const emailAddress = `test-user-${projectName.toLowerCase()}-${Date.now()}@gamil.com`; + + await page.goto('https://audaf-testing.onrender.com/signup'); + await page.getByRole('textbox', { name: 'Password', exact: true }).click(); + await page.getByRole('textbox', { name: 'Password', exact: true }).fill('weak'); // <- weak password + await page.getByRole('textbox', { name: 'Confirm Password' }).click(); + await page.getByRole('textbox', { name: 'Confirm Password' }).fill('weak'); + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('testusername2'); + await page.getByRole('textbox', { name: 'Email Address' }).click(); + await page.getByRole('textbox', { name: 'Email Address' }).fill(emailAddress); + await page.locator('input[type="file"]').setInputFiles('jellyfishWallpaper.jpg'); + await page.getByRole('button', { name: '-- Select country --' }).click(); + await page.getByRole('button', { name: 'Albania flag Albania' }).click(); + await expect(page.getByRole('button', { name: 'Create Account' })).toBeDisabled(); + }); + + test("trying to create account with a profile picture in the wrong format", async({page}, testInfo)=>{ + const projectName = testInfo.project.name; + const emailAddress = `test-user-${projectName.toLowerCase()}-${Date.now()}@gamil.com`; + + await page.goto('https://audaf-testing.onrender.com/signup'); + await page.getByRole('textbox', { name: 'Password', exact: true }).click(); + await page.getByRole('textbox', { name: 'Password', exact: true }).fill('securepassword123'); + await page.getByRole('textbox', { name: 'Confirm Password' }).click(); + await page.getByRole('textbox', { name: 'Confirm Password' }).fill('securepassword123'); + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('testusername2'); + await page.getByRole('textbox', { name: 'Email Address' }).click(); + await page.getByRole('textbox', { name: 'Email Address' }).fill(emailAddress); + await page.locator('input[type="file"]').setInputFiles('ghibli-gif.gif'); + await page.getByRole('button', { name: '-- Select country --' }).click(); + await page.getByRole('button', { name: 'Albania flag Albania' }).click(); + await page.getByRole('button', { name: 'Create Account' }).click(); + await page.waitForURL("https://audaf-testing.onrender.com/verify-email", { timeout: 30000 }); + const allMessages = await page.consoleMessages(); + const uploadFailed = allMessages.some(msg => + msg.text().includes("avatar upload failed") + ); + + expect(uploadFailed).toBe(true); + }); + // creates new user without verifying email and saves session/ cookies in userUnverified.json file for further testing test("create account and don't verify email", async({page}, testInfo)=>{ const projectName = testInfo.project.name; @@ -108,14 +191,6 @@ test.describe.serial('create accounts and testing account creation with already // NEED TO CREATE FOLLOWING TESTS: /* -mismatched password and confirm password -invalid email format -missing fields -dublicate username -weak password rejection -size limit for pic uploads -deleting account but enter the wrong email + verify deletion by trying to log in with deleted account -invalid data when updating profile testing with expired verification code 10 min testing with exipred/missing reset token -> hitting /reset-password directlty without going through the forgot password flow testing 15 min tempEmail expiration for signup flow diff --git a/tests/02-authAndSecurity.spec.ts b/tests/02-authAndSecurity.spec.ts index 6ad1ac2..2c63b64 100644 --- a/tests/02-authAndSecurity.spec.ts +++ b/tests/02-authAndSecurity.spec.ts @@ -1,5 +1,14 @@ +// @ts-check import { test, expect } from '@playwright/test'; -import { restoreAuthState, getAuthFileTamperedByProjectName, setUserTamperedToSameAsUser, removeCookiesFromFile} from "../playwright/helper/functions.ts" +import { restoreAuthState, + getAuthFileTamperedByProjectName, + setUserTamperedToSameAsUser, + removeCookiesFromFile, + tamperWithJWTToken, + tamperWithCSRFToken, + letCookiesExpire, + restoreExpiredCookies, + getAuthFileByProjectName} from "../playwright/utils/functions.ts" test.describe('Authentication and Security Tests', () => { @@ -15,29 +24,41 @@ test.describe('Authentication and Security Tests', () => { await page.goto('https://audaf-testing.onrender.com/home'); await expect(page).toHaveURL('https://audaf-testing.onrender.com/'); await expect(page.getByText('Welcome Back!', { exact: true})).toBeVisible(); - await setUserTamperedToSameAsUser(page, testInfo.project.name); // Reset the tampered state to match the regular user state for further tests + await setUserTamperedToSameAsUser(testInfo.project.name); // Reset the tampered state to match the regular user state for further tests }); - test("session with tampered JWT should be rejected", async({page}, testInfo)=>{ - // logic for the test - await setUserTamperedToSameAsUser(page, testInfo.project.name); + test("session with tampered JWT-token should be rejected", async({page}, testInfo)=>{ + await page.goto('https://audaf-testing.onrender.com/home'); + await expect(page.getByText('Welcome Home, testusername!', { exact: true})).toBeVisible(); + await tamperWithJWTToken(getAuthFileTamperedByProjectName(testInfo.project.name)) + await page.goto('https://audaf-testing.onrender.com/home'); + await expect(page.getByText('Welcome Home, testusername!', { exact: true})).not.toBeVisible(); + await setUserTamperedToSameAsUser(testInfo.project.name); }) - test("session with expired JWT should be rejected", async({page}, testInfo)=>{ - // logic for the test - await setUserTamperedToSameAsUser(page, testInfo.project.name); - }) + test("session with expired cookies should be rejected when navigating home", async({page}, testInfo)=>{ + await page.goto('https://audaf-testing.onrender.com/home'); + await expect(page.getByText('Welcome Home, testusername!', { exact: true})).toBeVisible(); + await letCookiesExpire(page); + await page.goto('https://audaf-testing.onrender.com/home'); + await expect(page.getByText('Welcome Back!', { exact: true})).toBeVisible(); + await restoreExpiredCookies(page, getAuthFileByProjectName(testInfo.project.name)); + }); - test("session with missing CSRF token should be rejected", async({page}, testInfo)=>{ - // logic for the test - await setUserTamperedToSameAsUser(page, testInfo.project.name); - }) - test("session with invalid CSRF token should be rejected", async({page}, testInfo)=>{ - // logic for the test - await setUserTamperedToSameAsUser(page, testInfo.project.name); + await page.goto('https://audaf-testing.onrender.com/home'); + await expect(page.getByText('Welcome Home, testusername!', { exact: true})).toBeVisible(); + await tamperWithCSRFToken(getAuthFileTamperedByProjectName(testInfo.project.name)) + await page.goto('https://audaf-testing.onrender.com/home'); + await expect(page.getByText('Welcome Home, testusername!', { exact: true})).not.toBeVisible(); + await setUserTamperedToSameAsUser(testInfo.project.name); }) }); -// NEED TO CREATE TEST FOR RATE-LIMITING AND BRUTE-FORCE ATTACKS, BUT IT'S NOT POSSIBLE TO TEST IT WITH PLAYWRIGHT. NEED TO TEST IT MANUALLY OR WITH A DIFFERENT TOOL. \ No newline at end of file +// NEED TO CREATE TEST FOR RATE-LIMITING AND BRUTE-FORCE ATTACKS, BUT IT'S NOT POSSIBLE TO TEST IT WITH PLAYWRIGHT. NEED TO TEST IT MANUALLY OR WITH A DIFFERENT TOOL. +/* +testing with expired verification code 10 min +testing with exipred/missing reset token -> hitting /reset-password directlty without going through the forgot password flow +testing 15 min tempEmail expiration for signup flow +*/ \ No newline at end of file diff --git a/tests/03-CRUDActionsAndNavigation.spec.ts b/tests/03-CRUDActionsAndNavigation.spec.ts index 290b2c8..da7e555 100644 --- a/tests/03-CRUDActionsAndNavigation.spec.ts +++ b/tests/03-CRUDActionsAndNavigation.spec.ts @@ -1,10 +1,10 @@ +// @ts-check import { test, expect } from '@playwright/test'; import {getAuthFileByProjectName, getEmailAddressForProject, restoreAuthState, - letCookiesExpire, - restoreExpiredCookies, - MailpitCodeFetcher} from "../playwright/helper/functions.ts" + MailpitCodeFetcher} from "../playwright/utils/functions.ts" +import {loginUser} from "../playwright/helpers/recycle.ts" test.describe.serial('CRUD Actions and Navigation', () => { @@ -22,25 +22,21 @@ test.describe.serial('CRUD Actions and Navigation', () => { await page.goto('https://audaf-testing.onrender.com/home'); await page.getByRole('button', { name: 'Edit Profile' }).click(); await page.getByRole('textbox').click(); - await page.getByRole('textbox').fill('newUsername'); + // trying to update profile with invalid username first + await page.getByRole('textbox').fill('x'); // <- too short await page.getByRole('button', { name: 'Albania' }).click(); await page.getByRole('button', { name: 'Austria flag Austria' }).click(); await page.getByRole('button', { name: 'Austria' }).click(); await page.getByRole('button', { name: 'Albania flag Albania' }).click(); await page.locator('input[type="file"]').setInputFiles('jellyfishWallpaper.jpg'); await page.getByRole('button', { name: 'Submit Changes' }).click(); + await expect(page.getByText("Failed to update profile. Please try again.", { exact: true})).toBeVisible(); + // now the valid username + await page.getByRole('textbox').fill('newUsername'); + await page.getByRole('button', { name: 'Submit Changes' }).click(); await expect(page.getByText("Profile updated successfully!", { exact: true})).toBeVisible(); }) - test("let session expire by expiring cookies and trying to navigate home", async({page}, testInfo)=>{ - await page.goto('https://audaf-testing.onrender.com/home'); - await expect(page.getByText('Welcome Home, newUsername!', { exact: true})).toBeVisible(); - await letCookiesExpire(page); - await page.goto('https://audaf-testing.onrender.com/home'); - await expect(page.getByText('Welcome Back!', { exact: true})).toBeVisible(); - await restoreExpiredCookies(page, getAuthFileByProjectName(testInfo.project.name)); - }); - test("logging out", async({page}, testInfo)=>{ await page.goto('https://audaf-testing.onrender.com/home'); await page.getByRole('button', { name: 'Logout' }).click(); @@ -51,30 +47,8 @@ test.describe.serial('CRUD Actions and Navigation', () => { }) test("logging in with wrong credentials", async({page}, testInfo)=>{ - const emailAddress = getEmailAddressForProject(testInfo); - await page.goto("https://audaf-testing.onrender.com") - await page.getByPlaceholder("Enter email").click(); - await page.getByPlaceholder("Enter email").fill(emailAddress); - await page.getByPlaceholder("Password").click(); - await page.getByPlaceholder("Password").fill('wrongPassword'); - - // Handle reCAPTCHA - if (await page.$('iframe[src*="recaptcha"]')) { - await page.waitForFunction(() => {return document.querySelector('iframe[src*="recaptcha"]');}); - const frames = page.frames(); - const recaptchaFrame = frames.find(f => f.url().includes('recaptcha')); - if (recaptchaFrame) {await recaptchaFrame.getByRole('checkbox', { name: "I'm not a robot" }).click();} - }else{ - // Handle reCAPTCHA – works in both Firefox and Chromium - // Wait for the reCAPTCHA iframe to appear in the DOM - await page.waitForSelector('iframe[src*="recaptcha"]', { timeout: 10000 }); - // Create a frame locator for that iframe - const recaptchaFrame = page.frameLocator('iframe[src*="recaptcha"]'); - // Click the "I'm not a robot" checkbox inside the iframe - await recaptchaFrame.getByRole('checkbox', { name: "I'm not a robot" }).click(); - } - await page.getByRole('button', { name: 'Sign In' }).click(); + await loginUser(page, testInfo, 'wrongPassword') await page.waitForTimeout(3000); await expect(page.locator('form')).toBeVisible({ timeout: 10000 }); @@ -119,30 +93,8 @@ test.describe.serial('CRUD Actions and Navigation', () => { }); test("logging in with correct credentials and verified email", async({page}, testInfo)=>{ - const emailAddress = getEmailAddressForProject(testInfo); - await page.goto("https://audaf-testing.onrender.com") - await page.getByPlaceholder("Enter email").click(); - await page.getByPlaceholder("Enter email").fill(emailAddress); - await page.getByPlaceholder("Password").click(); - await page.getByPlaceholder("Password").fill('newSecurePassword123'); - - // Handle reCAPTCHA - if (await page.$('iframe[src*="recaptcha"]')) { - await page.waitForFunction(() => {return document.querySelector('iframe[src*="recaptcha"]');}); - const frames = page.frames(); - const recaptchaFrame = frames.find(f => f.url().includes('recaptcha')); - if (recaptchaFrame) {await recaptchaFrame.getByRole('checkbox', { name: "I'm not a robot" }).click();} - }else{ - // Handle reCAPTCHA – works in both Firefox and Chromium - // Wait for the reCAPTCHA iframe to appear in the DOM - await page.waitForSelector('iframe[src*="recaptcha"]', { timeout: 10000 }); - // Create a frame locator for that iframe - const recaptchaFrame = page.frameLocator('iframe[src*="recaptcha"]'); - // Click the "I'm not a robot" checkbox inside the iframe - await recaptchaFrame.getByRole('checkbox', { name: "I'm not a robot" }).click(); - } - await page.getByRole('button', { name: 'Sign In' }).click(); + await loginUser(page, testInfo, "newSecurePassword123") await page.waitForTimeout(3000); await expect(page.locator('form')).toBeVisible({ timeout: 10000 }); @@ -155,6 +107,10 @@ test.describe.serial('CRUD Actions and Navigation', () => { await page.goto('https://audaf-testing.onrender.com/home'); await page.getByRole('button', { name: 'Delete Account' }).click(); await page.getByRole('textbox', { name: 'Enter your email' }).click(); + // trying to delete with wrong email and failing + await page.getByRole('textbox', { name: 'Enter your email' }).fill("wrongemail@gmail.com"); + await expect(page.getByRole('button', { name: 'Yes, Delete My Account' })).toBeDisabled(); + // deleting with correct email await page.getByRole('textbox', { name: 'Enter your email' }).fill(emailAddress); await page.getByRole('button', { name: 'Yes, Delete My Account' }).click(); await expect(page.getByText("Account deleted successfully! Redirecting to login page...")).toBeVisible({timeout: 10000}); @@ -162,6 +118,8 @@ test.describe.serial('CRUD Actions and Navigation', () => { await page.context().storageState({ path: getAuthFileByProjectName(testInfo.project.name) }); await page.goto("https://audaf-testing.onrender.com/home"); await expect(page.getByText('Welcome Home, newUsername!', { exact: true})).not.toBeVisible(); + await loginUser(page, testInfo, "newSecurePassword123"); + await expect(page.getByText("Invalid credentials")).toBeVisible(); }); }); \ No newline at end of file diff --git a/tests/04-cleanUpTestAccounts.spec.ts b/tests/04-cleanUpTestAccounts.spec.ts index 301570e..0a249a1 100644 --- a/tests/04-cleanUpTestAccounts.spec.ts +++ b/tests/04-cleanUpTestAccounts.spec.ts @@ -1,3 +1,4 @@ +// @ts-check import { test, expect } from '@playwright/test'; import path from "path"; import { exec } from 'child_process'; diff --git a/tests/05-unverifiedEmailCRUDActions.spec.ts b/tests/05-unverifiedEmailCRUDActions.spec.ts index ede9eb5..6444f84 100644 --- a/tests/05-unverifiedEmailCRUDActions.spec.ts +++ b/tests/05-unverifiedEmailCRUDActions.spec.ts @@ -1,3 +1,4 @@ +// @ts-check import { test, expect } from '@playwright/test'; import path from "path"; import { existsSync, readFileSync } from 'fs';