From 1ef5deeb9841e81ccba0e432a5a596db8c0fe831 Mon Sep 17 00:00:00 2001 From: AI Agent Date: Sun, 22 Feb 2026 21:27:41 +0000 Subject: [PATCH] e2e) add admin auth source and login flows --- e2e/admin-auth-source.test.ts | 55 +++++++++++++++++++++++++++++ e2e/admin-local-user-flow.test.ts | 58 +++++++++++++++++++++++++++++++ e2e/auth-callback.test.ts | 25 +++++++++++++ e2e/trip-planning.test.ts | 2 +- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 e2e/admin-auth-source.test.ts create mode 100644 e2e/admin-local-user-flow.test.ts create mode 100644 e2e/auth-callback.test.ts diff --git a/e2e/admin-auth-source.test.ts b/e2e/admin-auth-source.test.ts new file mode 100644 index 0000000..af37ffd --- /dev/null +++ b/e2e/admin-auth-source.test.ts @@ -0,0 +1,55 @@ +import { test, expect, type Locator, type Page } from '@playwright/test'; +import { TEST_USERS } from './setup/test-users.js'; + +const LOGIN_URL = '/trips/login'; +const ADMIN_USERS_URL = '/trips/admin/users'; + +async function loginAsLocalUser(page: Page, username: string, password: string): Promise { + await page.goto(LOGIN_URL); + await page.fill('input[name="identifier"]', username); + await page.fill('input[name="password"]', password); + await page.click('button[type="submit"]:has-text("Sign in locally")'); + await page.waitForURL('**/trips/dashboard', { timeout: 15_000 }); +} + +function rowForUser(page: Page, username: string): Locator { + return page.getByRole('cell', { name: username }).locator('..'); +} + +test.beforeEach(async ({ context }) => { + await context.clearCookies(); +}); + +test('admin sees auth source and can set local password', async ({ page }) => { + await loginAsLocalUser(page, TEST_USERS.admin.username, TEST_USERS.admin.password); + await page.goto(ADMIN_USERS_URL); + await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible(); + + const regularRow = rowForUser(page, TEST_USERS.regular.username); + await expect(regularRow.getByText('Local', { exact: true })).toBeVisible(); + + const adminRow = rowForUser(page, TEST_USERS.admin.username); + await expect(adminRow.getByText('Local', { exact: true })).toBeVisible(); + + await regularRow.getByRole('button', { name: 'Set local password' }).click(); + const dialog = page.getByRole('dialog', { name: 'Set local password' }); + await expect(dialog).toBeVisible(); + + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog.getByText('Password is required')).toBeVisible(); + + await dialog.locator('input[type="password"]').nth(0).fill('short'); + await dialog.locator('input[type="password"]').nth(1).fill('short'); + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog.getByText('Password must be at least 12 characters')).toBeVisible(); + + await dialog.locator('input[type="password"]').nth(0).fill('long-enough-password'); + await dialog.locator('input[type="password"]').nth(1).fill('long-enough-password-mismatch'); + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog.getByText('Passwords do not match')).toBeVisible(); + + await dialog.locator('input[type="password"]').nth(0).fill('local-password-123'); + await dialog.locator('input[type="password"]').nth(1).fill('local-password-123'); + await dialog.getByRole('button', { name: 'Save password' }).click(); + await expect(dialog).toBeHidden(); +}); diff --git a/e2e/admin-local-user-flow.test.ts b/e2e/admin-local-user-flow.test.ts new file mode 100644 index 0000000..163ff92 --- /dev/null +++ b/e2e/admin-local-user-flow.test.ts @@ -0,0 +1,58 @@ +import { test, expect, type Page } from '@playwright/test'; +import { TEST_USERS } from './setup/test-users.js'; + +const LOGIN_URL = '/trips/login'; +const ADMIN_USERS_URL = '/trips/admin/users'; +const DASHBOARD_URL = '/trips/dashboard'; + +async function loginAsLocalUser(page: Page, username: string, password: string): Promise { + await page.goto(LOGIN_URL); + await page.fill('input[name="identifier"]', username); + await page.fill('input[name="password"]', password); + await page.click('button[type="submit"]:has-text("Sign in locally")'); + await page.waitForURL(`**${DASHBOARD_URL}`, { timeout: 15_000 }); +} + +async function clearSessionState(page: Page): Promise { + await page.context().clearCookies(); + await page.goto('about:blank'); + await page.evaluate(() => { + localStorage.clear(); + sessionStorage.clear(); + }); +} + +test.beforeEach(async ({ context }) => { + await context.clearCookies(); +}); + +test('admin can create a local user who can sign in', async ({ page }) => { + const suffix = Date.now(); + const username = `e2e_new_user_${suffix}`; + const fullName = `E2E New User ${suffix}`; + const email = `e2e_new_user_${suffix}@test.local`; + const password = `TempPass-${suffix}-123`; + + await loginAsLocalUser(page, TEST_USERS.admin.username, TEST_USERS.admin.password); + await page.goto(ADMIN_USERS_URL); + await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible(); + await page.getByRole('button', { name: 'Add User' }).click(); + + const addDialog = page.getByRole('dialog', { name: 'Add user' }); + await expect(addDialog).toBeVisible(); + await addDialog.locator('input[type="text"]').nth(0).fill(username); + await addDialog.locator('input[type="text"]').nth(1).fill(fullName); + await addDialog.locator('input[type="email"]').fill(email); + await addDialog.locator('input[type="password"]').nth(0).fill(password); + await addDialog.locator('input[type="password"]').nth(1).fill(password); + await addDialog.getByRole('button', { name: 'Create user' }).click(); + + await expect(page.getByRole('cell', { name: username })).toBeVisible(); + + await clearSessionState(page); + await loginAsLocalUser(page, username, password); + await expect(page).toHaveURL(/\/trips\/dashboard/); + + await page.goto(ADMIN_USERS_URL); + await expect(page).toHaveURL(/\/trips\/dashboard/); +}); diff --git a/e2e/auth-callback.test.ts b/e2e/auth-callback.test.ts new file mode 100644 index 0000000..1be5b81 --- /dev/null +++ b/e2e/auth-callback.test.ts @@ -0,0 +1,25 @@ +import { test, expect, type Page } from '@playwright/test'; +import { TEST_USERS } from './setup/test-users.js'; + +const LOGIN_URL = '/trips/login'; +const UPCOMING_URL = '/trips/upcoming'; + +async function submitLocalLogin(page: Page, username: string, password: string): Promise { + await page.fill('input[name="identifier"]', username); + await page.fill('input[name="password"]', password); + await page.click('button[type="submit"]:has-text("Sign in locally")'); +} + +test.beforeEach(async ({ context }) => { + await context.clearCookies(); +}); + +test('redirects back to protected route after local login', async ({ page }) => { + await page.goto(UPCOMING_URL); + await expect(page).toHaveURL(/\/trips\/login/); + + await submitLocalLogin(page, TEST_USERS.regular.username, TEST_USERS.regular.password); + await page.waitForURL(/\/trips\/(upcoming|dashboard)/, { timeout: 15_000 }); + await expect(page).not.toHaveURL(/\/trips\/auth/); + await expect(page).not.toHaveURL(/\/trips\/login/); +}); diff --git a/e2e/trip-planning.test.ts b/e2e/trip-planning.test.ts index bc2f09f..8cafcef 100644 --- a/e2e/trip-planning.test.ts +++ b/e2e/trip-planning.test.ts @@ -150,7 +150,7 @@ test.beforeEach(async ({ context }) => { await context.clearCookies(); }); -test('regular user can plan a detailed future trip and see it in upcoming', async ({ page }) => { +test('regular user can plan a detailed future trip with no end date', async ({ page }) => { const startDate = formatDate(30); const tripName = `E2E Future Trip ${Date.now()}`; const tripDescription = 'Future trip created in Playwright e2e scenario';