From 365babe9cdbf463ef700f0973bd999c13e84a4f3 Mon Sep 17 00:00:00 2001 From: Moshfegh Hamedani Date: Fri, 13 Feb 2026 10:18:44 -0800 Subject: [PATCH] Write automated tests for authentication --- .gitignore | 2 +- e2e/fixtures/auth.ts | 62 +++++ e2e/tests/auth.spec.ts | 443 ++++++++++++++++++++++++++++++++++++ playwright.config.ts | 3 +- test-results/.last-run.json | 4 - 5 files changed, 508 insertions(+), 6 deletions(-) create mode 100644 e2e/fixtures/auth.ts create mode 100644 e2e/tests/auth.spec.ts delete mode 100644 test-results/.last-run.json diff --git a/.gitignore b/.gitignore index 59c975a..7fc6973 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules e2e/test-results/ -playwright-report/ +e2e/playwright-report/ diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts new file mode 100644 index 0000000..689b30b --- /dev/null +++ b/e2e/fixtures/auth.ts @@ -0,0 +1,62 @@ +import { Page, expect } from "@playwright/test"; + +/** + * Test credentials based on seeded data from server/prisma/seed.ts + */ +export const TEST_USERS = { + admin: { + email: "admin@example.com", + password: "password123", + name: "Admin", + role: "admin", + }, + // Currently only admin is seeded, but we can add agent users here if seeded +} as const; + +/** + * Logs in a user with the provided credentials + */ +export async function login( + page: Page, + credentials: { email: string; password: string } +) { + await page.goto("/login"); + await page.getByLabel("Email").fill(credentials.email); + await page.getByLabel("Password").fill(credentials.password); + await page.getByRole("button", { name: /sign in/i }).click(); +} + +/** + * Logs in as the seeded admin user + */ +export async function loginAsAdmin(page: Page) { + await login(page, TEST_USERS.admin); + // Wait for redirect to home page after successful login + await expect(page).toHaveURL("/"); +} + +/** + * Logs out the current user + */ +export async function logout(page: Page) { + await page.getByRole("button", { name: /sign out/i }).click(); + // Wait for redirect to login page + await expect(page).toHaveURL("/login"); +} + +/** + * Checks if the user is on the login page + */ +export async function expectLoginPage(page: Page) { + await expect(page).toHaveURL("/login"); + await expect(page.getByText("Helpdesk")).toBeVisible(); + await expect(page.getByText(/sign in to your account/i)).toBeVisible(); +} + +/** + * Checks if the user is on the home page (authenticated) + */ +export async function expectHomePage(page: Page) { + await expect(page).toHaveURL("/"); + await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible(); +} diff --git a/e2e/tests/auth.spec.ts b/e2e/tests/auth.spec.ts new file mode 100644 index 0000000..3f4f376 --- /dev/null +++ b/e2e/tests/auth.spec.ts @@ -0,0 +1,443 @@ +import { test, expect } from "@playwright/test"; +import { + TEST_USERS, + login, + loginAsAdmin, + logout, + expectLoginPage, + expectHomePage, +} from "../fixtures/auth"; + +test.describe("Authentication", () => { + test.describe("Login Page", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/login"); + }); + + test("should display login form with all elements", async ({ page }) => { + // Check page title and description + await expect(page.getByText("Helpdesk")).toBeVisible(); + await expect(page.getByText(/sign in to your account/i)).toBeVisible(); + + // Check form fields + await expect(page.getByLabel("Email")).toBeVisible(); + await expect(page.getByLabel("Password")).toBeVisible(); + + // Check submit button + await expect( + page.getByRole("button", { name: /sign in/i }) + ).toBeVisible(); + }); + + test("should login successfully with valid admin credentials", async ({ + page, + }) => { + await login(page, TEST_USERS.admin); + + // Should redirect to home page + await expectHomePage(page); + + // Should display user name in navigation + await expect(page.getByText(TEST_USERS.admin.name)).toBeVisible(); + }); + + test("should show error with invalid email format", async ({ page }) => { + await page.getByLabel("Email").fill("invalid-email"); + await page.getByLabel("Password").fill("password123"); + await page.getByRole("button", { name: /sign in/i }).click(); + + // Should show client-side validation error + await expect( + page.getByText(/please enter a valid email/i) + ).toBeVisible(); + + // Should remain on login page + await expectLoginPage(page); + }); + + test("should show error with empty email", async ({ page }) => { + await page.getByLabel("Password").fill("password123"); + await page.getByRole("button", { name: /sign in/i }).click(); + + // Should show client-side validation error + await expect( + page.getByText(/please enter a valid email/i) + ).toBeVisible(); + + // Should remain on login page + await expectLoginPage(page); + }); + + test("should show error with empty password", async ({ page }) => { + await page.getByLabel("Email").fill(TEST_USERS.admin.email); + await page.getByRole("button", { name: /sign in/i }).click(); + + // Should show client-side validation error + await expect(page.getByText(/password is required/i)).toBeVisible(); + + // Should remain on login page + await expectLoginPage(page); + }); + + test("should show error with empty email and password", async ({ + page, + }) => { + await page.getByRole("button", { name: /sign in/i }).click(); + + // Should show both validation errors + await expect( + page.getByText(/please enter a valid email/i) + ).toBeVisible(); + await expect(page.getByText(/password is required/i)).toBeVisible(); + + // Should remain on login page + await expectLoginPage(page); + }); + + test("should show error with invalid email (non-existent user)", async ({ + page, + }) => { + await login(page, { + email: "nonexistent@example.com", + password: "password123", + }); + + // Should show server error message + await expect(page.getByRole("alert")).toBeVisible(); + await expect(page.getByRole("alert")).toContainText( + /invalid email or password/i + ); + + // Should remain on login page + await expectLoginPage(page); + }); + + test("should show error with incorrect password", async ({ page }) => { + await login(page, { + email: TEST_USERS.admin.email, + password: "wrongpassword", + }); + + // Should show server error message + await expect(page.getByRole("alert")).toBeVisible(); + await expect(page.getByRole("alert")).toContainText( + /invalid email or password/i + ); + + // Should remain on login page + await expectLoginPage(page); + }); + + test("should show loading state during login", async ({ page }) => { + await page.getByLabel("Email").fill(TEST_USERS.admin.email); + await page.getByLabel("Password").fill(TEST_USERS.admin.password); + + // Start login + const loginPromise = page + .getByRole("button", { name: /sign in/i }) + .click(); + + // Check loading state appears (button disabled with loading text) + await expect(page.getByText(/signing in.../i)).toBeVisible(); + + await loginPromise; + }); + + test("should redirect to home if already authenticated", async ({ + page, + }) => { + // Login first + await loginAsAdmin(page); + + // Try to visit login page again + await page.goto("/login"); + + // Should redirect to home page + await expectHomePage(page); + }); + + test("should clear server error on new submission", async ({ page }) => { + // First submission with wrong password + await login(page, { + email: TEST_USERS.admin.email, + password: "wrongpassword", + }); + + // Error should be visible + await expect(page.getByRole("alert")).toBeVisible(); + + // Try again with correct password + await page.getByLabel("Email").clear(); + await page.getByLabel("Password").clear(); + await login(page, TEST_USERS.admin); + + // Should redirect to home (error cleared) + await expectHomePage(page); + }); + }); + + test.describe("Session Persistence", () => { + test("should maintain session after page reload", async ({ page }) => { + // Login + await loginAsAdmin(page); + + // Reload the page + await page.reload(); + + // Should still be authenticated and on home page + await expectHomePage(page); + await expect(page.getByText(TEST_USERS.admin.name)).toBeVisible(); + }); + + test("should maintain session when navigating directly to protected route", async ({ + page, + }) => { + // Login + await loginAsAdmin(page); + + // Navigate to another protected route + await page.goto("/users"); + + // Should be able to access it + await expect(page).toHaveURL("/users"); + await expect( + page.getByRole("heading", { name: /users/i }) + ).toBeVisible(); + }); + + test("should maintain session across multiple page navigations", async ({ + page, + }) => { + // Login + await loginAsAdmin(page); + + // Navigate to different routes + await page.goto("/users"); + await expect(page).toHaveURL("/users"); + + await page.goto("/"); + await expectHomePage(page); + + // Should still show user in nav + await expect(page.getByText(TEST_USERS.admin.name)).toBeVisible(); + }); + }); + + test.describe("Logout", () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test("should logout successfully", async ({ page }) => { + await logout(page); + + // Should redirect to login page + await expectLoginPage(page); + }); + + test("should not be able to access protected routes after logout", async ({ + page, + }) => { + await logout(page); + + // Try to access home page + await page.goto("/"); + + // Should redirect to login + await expectLoginPage(page); + }); + + test("should not be able to access admin routes after logout", async ({ + page, + }) => { + await logout(page); + + // Try to access admin page + await page.goto("/users"); + + // Should redirect to login + await expectLoginPage(page); + }); + + test("should require login again after logout", async ({ page }) => { + await logout(page); + + // Try to access home page + await page.goto("/"); + + // Should be on login page + await expectLoginPage(page); + + // Login again + await loginAsAdmin(page); + + // Should be able to access home page + await expectHomePage(page); + }); + }); + + test.describe("Protected Routes", () => { + test("should redirect unauthenticated user to login when accessing root", async ({ + page, + }) => { + await page.goto("/"); + await expectLoginPage(page); + }); + + test("should redirect unauthenticated user to login when accessing admin route", async ({ + page, + }) => { + await page.goto("/users"); + await expectLoginPage(page); + }); + + test("should redirect unauthenticated user from home to login", async ({ + page, + }) => { + await page.goto("/"); + + // Should redirect to login + await expectLoginPage(page); + }); + + test("should allow access to protected route after login", async ({ + page, + }) => { + // Try to access protected route first + await page.goto("/"); + await expectLoginPage(page); + + // Login + await loginAsAdmin(page); + + // Should now be on home page + await expectHomePage(page); + }); + }); + + test.describe("Admin Route Protection", () => { + test("should allow admin to access admin routes", async ({ page }) => { + await loginAsAdmin(page); + + // Navigate to admin route + await page.goto("/users"); + + // Should be able to access it + await expect(page).toHaveURL("/users"); + await expect( + page.getByRole("heading", { name: /users/i }) + ).toBeVisible(); + }); + + test("should show 'Users' link in navigation for admin", async ({ + page, + }) => { + await loginAsAdmin(page); + + // Users link should be visible in navigation + await expect(page.getByRole("link", { name: /users/i })).toBeVisible(); + }); + + test("should navigate to users page when clicking Users link", async ({ + page, + }) => { + await loginAsAdmin(page); + + // Click Users link + await page.getByRole("link", { name: /users/i }).click(); + + // Should navigate to users page + await expect(page).toHaveURL("/users"); + await expect( + page.getByRole("heading", { name: /users/i }) + ).toBeVisible(); + }); + + test("should maintain access to home route while on admin route", async ({ + page, + }) => { + await loginAsAdmin(page); + + // Go to admin route + await page.goto("/users"); + await expect(page).toHaveURL("/users"); + + // Manually navigate back to home + await page.goto("/"); + + // Should be able to access home + await expectHomePage(page); + }); + + // Note: Currently only admin user is seeded. If agent users are added to seed.ts, + // uncomment and update these tests: + // + // test("should redirect agent to home when accessing admin route", async ({ + // page, + // }) => { + // await login(page, TEST_USERS.agent); + // await page.goto("/users"); + // await expectHomePage(page); + // }); + // + // test("should not show 'Users' link in navigation for agent", async ({ + // page, + // }) => { + // await login(page, TEST_USERS.agent); + // await expect(page.getByRole("link", { name: /users/i })).not.toBeVisible(); + // }); + }); + + test.describe("URL Handling", () => { + test("should redirect unknown routes to home for authenticated user", async ({ + page, + }) => { + await loginAsAdmin(page); + + // Visit unknown route + await page.goto("/unknown-route"); + + // Should redirect to home + await expectHomePage(page); + }); + + test("should redirect unknown routes to login for unauthenticated user", async ({ + page, + }) => { + // Visit unknown route without being logged in + await page.goto("/unknown-route"); + + // Should redirect to login (via home redirect) + await expectLoginPage(page); + }); + }); + + test.describe("Navigation Bar", () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test("should display correct user information", async ({ page }) => { + // User name should be visible + await expect(page.getByText(TEST_USERS.admin.name)).toBeVisible(); + + // Sign out button should be visible + await expect( + page.getByRole("button", { name: /sign out/i }) + ).toBeVisible(); + }); + + test("should display Helpdesk branding", async ({ page }) => { + await expect(page.getByText("Helpdesk").first()).toBeVisible(); + }); + + test("should show navigation appropriate to user role", async ({ + page, + }) => { + // Admin should see Users link + await expect(page.getByRole("link", { name: /users/i })).toBeVisible(); + + // When we have agent users, we would test they don't see it + }); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index c811259..bbb23cc 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,8 @@ export default defineConfig({ fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - reporter: "html", + outputDir: "./e2e/test-results", + reporter: [["html", { outputFolder: "./e2e/playwright-report" }]], use: { baseURL: "http://localhost:5174", trace: "on-first-retry", diff --git a/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index 5fca3f8..0000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "failed", - "failedTests": [] -} \ No newline at end of file