import { test, expect, Page } from '@playwright/test'; const PASSWORD = 'supersecret123'; const uniqueEmail = () => `disable-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}@example.com`; async function register(page: Page, email: string) { await page.goto('/register'); await page.getByRole('textbox', { name: 'Email' }).fill(email); await page.getByLabel('Password').fill(PASSWORD); await page.getByRole('button', { name: 'Create account' }).click(); // Registering now closes the auth modal and returns to the page behind it, so // the header rather than the URL is what proves the session exists. The wait // is generous because this is a bcrypt round-trip rather than a render. await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 }); } async function customerRow(page: Page, email: string) { await page.goto('/admin'); await page.getByRole('tab', { name: 'Customers' }).click(); const row = page.getByRole('row').filter({ hasText: email }); await expect(row).toBeVisible(); return row; } test.describe('Disabling a customer account', () => { test('an admin can disable an account and the customer is told at sign-in', async ({ page }) => { const email = uniqueEmail(); await register(page, email); const row = await customerRow(page, email); await expect(row.getByText('ACTIVE')).toBeVisible(); await row.getByRole('button', { name: 'Disable' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Disable' }).click(); await expect(page.getByText('Account disabled')).toBeVisible(); const updated = page.getByRole('row').filter({ hasText: email }); await expect(updated.getByText('DISABLED')).toBeVisible(); // A generic credential error would send a real customer round the // password-reset loop forever. await page.goto('/login'); await page.getByRole('textbox', { name: 'Email' }).fill(email); await page.getByLabel('Password').fill(PASSWORD); // Scoped to the modal: the storefront rendered behind it has a "Log in" // button of its own, which is what opened this one. await page.getByRole('dialog', { name: 'Log in' }).getByRole('button', { name: 'Log in' }).click(); await expect(page.getByText(/disabled/i)).toBeVisible(); }); test('an existing session stops working immediately', async ({ page, request }) => { const email = uniqueEmail(); await register(page, email); // Still signed in from registration, in this same browser context. await page.goto('/account'); await expect(page.getByText(email)).toBeVisible(); const customers = await (await request.get('/api/admin/customers')).json(); const id = customers.find((c: { email: string }) => c.email === email).id; await request.post(`/api/admin/customers/${id}/disable`); // The cookie is unchanged, so this proves the server rejects it rather // than the browser having discarded it. await page.goto('/account'); await expect(page).toHaveURL(/\/login/); }); test('re-enabling restores sign-in', async ({ page, request }) => { const email = uniqueEmail(); await register(page, email); const customers = await (await request.get('/api/admin/customers')).json(); const id = customers.find((c: { email: string }) => c.email === email).id; await request.post(`/api/admin/customers/${id}/disable`); const row = await customerRow(page, email); await row.getByRole('button', { name: 'Re-enable' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Re-enable' }).click(); await expect(page.getByText('Account re-enabled')).toBeVisible(); await page.goto('/login'); await page.getByRole('textbox', { name: 'Email' }).fill(email); await page.getByLabel('Password').fill(PASSWORD); // Scoped to the modal: the storefront rendered behind it has a "Log in" // button of its own, which is what opened this one. await page.getByRole('dialog', { name: 'Log in' }).getByRole('button', { name: 'Log in' }).click(); await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 }); }); test('the confirmation warns that held items will be released', async ({ page, request }) => { const email = uniqueEmail(); const itemName = `Held ${Date.now().toString(36)}`; const created = await request.post('/api/admin/items', { multipart: { name: itemName, description: '', price: '40', category_id: '', tags: '[]' } }); const itemId = (await created.json()).id as number; await register(page, email); expect((await page.request.post(`/api/cart/items/${itemId}`)).status()).toBe(201); const row = await customerRow(page, email); await row.getByRole('button', { name: 'Disable' }).click(); // The consequence has to be visible at the moment of the decision. await expect(page.getByText(/1 reserved item/)).toBeVisible(); await page.getByRole('dialog').getByRole('button', { name: 'Disable' }).click(); await expect(page.getByText('Account disabled')).toBeVisible(); const item = await (await request.get(`/api/items/${itemId}`)).json(); expect(item.status).toBe('available'); }); });