import type { Page } from '@playwright/test'; import { test, expect } from '../fixtures'; import { LoginPage } from '../pages/LoginPage'; /** * Real Manage Users CRUD through the admin UI (`admin.manage-users.spec.ts` * only opens/closes the panel — it never exercises create/edit/delete), plus * a causal cross-session check: an admin deactivating a user immediately * signs that user out of their *already open* session, because the NextAuth * `jwt` callback re-checks `isActive` from the DB on every request (see * `viewer-frontend/app/api/auth/[...nextauth]/route.ts`, issue #57 fix). * * punimtagdev shares its Postgres DB with MirrorMatch and reuses the same * pre-seeded accounts as other specs (E2E_ADMIN_EMAIL, E2E_VIEWER_EMAIL), so * every test here creates its own disposable `e2e-manage-test-*` account and * deletes it in a `finally`, regardless of outcome. */ function throwawayEmail(tag: string): string { return `e2e-manage-test-${tag}-${Date.now()}@e2e.invalid`; } const THROWAWAY_PASSWORD = 'E2eThrowaway!123'; interface AdminUser { id: number; email: string; } /** Best-effort cleanup — looks the account up by email since we may not * have its id if the test failed before/during creation. */ async function deleteUserByEmail(page: Page, baseUrl: string, email: string): Promise { const res = await page.request.get(`${baseUrl}/api/users?status=all`); if (!res.ok()) return; const body = (await res.json()) as { users?: AdminUser[] }; const match = body.users?.find((u) => u.email === email); if (match) { await page.request.delete(`${baseUrl}/api/users/${match.id}`); } } test.describe('admin manage users: create/edit/delete @smoke', () => { test('admin can create, edit, and delete a user through the panel', async ({ page, playkitConfig, loginPage, accountMenu, manageUsersPanel, e2eCredentials, timings, }) => { test.skip(!e2eCredentials, 'E2E_ADMIN_EMAIL/PASSWORD required (admin user)'); const email = throwawayEmail('crud'); try { await loginPage.openLogin(); await loginPage.signIn(e2eCredentials!.email, e2eCredentials!.password); await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 30_000 }); await page.getByLabel('Account menu').waitFor({ state: 'visible', timeout: 30_000 }); await accountMenu.openManageUsers(); await timings.measure('create_user', () => manageUsersPanel.createUser({ email, password: THROWAWAY_PASSWORD, name: 'E2E Throwaway' }), ); expect(await manageUsersPanel.statusBadgeText(email)).toMatch(/active/i); expect(await manageUsersPanel.roleBadgeText(email)).toMatch(/user/i); expect(await manageUsersPanel.writeAccessText(email)).toMatch(/no/i); await timings.measure('edit_user', async () => { await manageUsersPanel.openEditDialog(email); await manageUsersPanel.setWriteAccess(true); await manageUsersPanel.saveEdit(); }); expect(await manageUsersPanel.writeAccessText(email)).toMatch(/yes/i); await timings.measure('delete_user', () => manageUsersPanel.deleteUser(email)); await manageUsersPanel.waitForRowGone(email); await accountMenu.closeManageUsers(); } finally { await deleteUserByEmail(page, playkitConfig.baseUrl, email).catch(() => undefined); } }); }); test.describe('admin manage users: causal cross-session effect @smoke', () => { test('deactivating a user via admin UI immediately revokes their open session', async ({ browser, page, playkitConfig, loginPage, accountMenu, manageUsersPanel, e2eCredentials, timings, }) => { test.skip(!e2eCredentials, 'E2E_ADMIN_EMAIL/PASSWORD required (admin user)'); // Two full UI logins + create/edit/verify against a shared-CI DEV LXC push // this past the default 60s under load (observed: a single login step // alone took ~90s during a concurrent CI burst) — give it more headroom. test.setTimeout(120_000); const email = throwawayEmail('deactivate'); let userContext: import('@playwright/test').BrowserContext | undefined; try { await loginPage.openLogin(); await loginPage.signIn(e2eCredentials!.email, e2eCredentials!.password); await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 30_000 }); await page.getByLabel('Account menu').waitFor({ state: 'visible', timeout: 30_000 }); await accountMenu.openManageUsers(); await timings.measure('create_throwaway', () => manageUsersPanel.createUser({ email, password: THROWAWAY_PASSWORD, name: 'E2E Throwaway' }), ); await accountMenu.closeManageUsers(); // Independent second session/actor — the throwaway user, logged in // for real, concurrently with the admin. userContext = await browser.newContext(); const userPage = await userContext.newPage(); const userLogin = new LoginPage(userPage, playkitConfig.baseUrl); await timings.measure('throwaway_login', async () => { await userLogin.openLogin(); await userLogin.signIn(email, THROWAWAY_PASSWORD); await userPage.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 30_000 }); await userPage.getByLabel('Account menu').waitFor({ state: 'visible', timeout: 30_000 }); }); const preRes = await userPage.request.get(`${playkitConfig.baseUrl}/api/auth/session`); const preBody = await preRes.json(); expect(preBody?.user?.email).toBe(email); // Admin deactivates the throwaway user. The throwaway user's session // is never touched directly — no sign-out, no reload triggered by us. await accountMenu.openManageUsers(); await timings.measure('deactivate_user', async () => { await manageUsersPanel.openEditDialog(email); await manageUsersPanel.setActive(false); await manageUsersPanel.saveEdit(); }); // The table defaults to (and refetches with) the "Active only" filter, // so the just-deactivated row drops out of view immediately — switch to // "All" before reading its badge, or this hangs forever waiting for a // row that the current filter will never show again. await manageUsersPanel.showAllUsers(); expect(await manageUsersPanel.statusBadgeText(email)).toMatch(/inactive/i); await accountMenu.closeManageUsers(); // The already-open session must now read as signed-out: the jwt // callback re-checks `isActive` against the DB on every request. await timings.measure('session_revoked', async () => { const postRes = await userPage.request.get(`${playkitConfig.baseUrl}/api/auth/session`); const postBody = await postRes.json(); expect(postBody?.user).toBeFalsy(); }); } finally { await userContext?.close(); await deleteUserByEmail(page, playkitConfig.baseUrl, email).catch(() => undefined); } }); });