test: add E2E tests for admin pages
This commit is contained in:
parent
7ad794170d
commit
5a24a9c70b
2 changed files with 260 additions and 0 deletions
238
tests/e2e/admin.spec.js
Normal file
238
tests/e2e/admin.spec.js
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
// @ts-check
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
|
||||
|
||||
/** Log in as the admin user and land on /manage/credentials. */
|
||||
async function loginAsAdmin(page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('#username', fixtures.admin_username);
|
||||
await page.fill('#password', fixtures.admin_password);
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
||||
}
|
||||
|
||||
/** Log in as a regular (non-admin) user. */
|
||||
async function loginAsRegularUser(page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('#username', fixtures.login_username);
|
||||
await page.fill('#password', fixtures.login_password);
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
||||
}
|
||||
|
||||
test.describe('Admin pages', () => {
|
||||
// ---------------------------------------------------------------
|
||||
// 1 & 2. Auth guards
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Auth guard', () => {
|
||||
test('unauthenticated user visiting /admin/users is redirected to /login', async ({ page }) => {
|
||||
await page.goto('/admin/users');
|
||||
await page.waitForURL('**/login', { timeout: 5000 });
|
||||
expect(page.url()).toContain('/login');
|
||||
});
|
||||
|
||||
test('non-admin logged-in user gets 403', async ({ page }) => {
|
||||
await loginAsRegularUser(page);
|
||||
const response = await page.goto('/admin/users');
|
||||
expect(response.status()).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 3 & 4 & 5. User list page
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('User list page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/users');
|
||||
});
|
||||
|
||||
test('has correct page structure', async ({ page }) => {
|
||||
await expect(page.locator('h1')).toHaveText('Users');
|
||||
await expect(page.locator('.admin-table thead th')).toHaveCount(6);
|
||||
await expect(page.locator('input[name="q"]')).toBeVisible();
|
||||
await expect(page.locator('form[hx-post="/admin/invite"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('table headers are correct', async ({ page }) => {
|
||||
const headers = page.locator('.admin-table thead th');
|
||||
await expect(headers.nth(0)).toHaveText('Username');
|
||||
await expect(headers.nth(1)).toHaveText('Name');
|
||||
await expect(headers.nth(2)).toHaveText('Email');
|
||||
await expect(headers.nth(3)).toHaveText('Groups');
|
||||
await expect(headers.nth(4)).toHaveText('Status');
|
||||
await expect(headers.nth(5)).toHaveText('Created');
|
||||
});
|
||||
|
||||
test('shows seeded users', async ({ page }) => {
|
||||
const tableBody = page.locator('#user-table-body');
|
||||
await expect(tableBody).toContainText('testuser');
|
||||
await expect(tableBody).toContainText('adminuser');
|
||||
});
|
||||
|
||||
test('search filters results', async ({ page }) => {
|
||||
const searchInput = page.locator('input[name="q"]');
|
||||
await searchInput.fill('admin');
|
||||
// Wait for htmx debounce (300ms) and response
|
||||
await expect(page.locator('#user-table-body')).toContainText('adminuser', { timeout: 5000 });
|
||||
// Other users should be filtered out
|
||||
await expect(page.locator('#user-table-body')).not.toContainText('testuser', { timeout: 3000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 6. User detail page structure
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('User detail page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/users');
|
||||
});
|
||||
|
||||
test('clicking a user link shows detail page', async ({ page }) => {
|
||||
await page.click(`a:has-text("adminuser")`);
|
||||
await page.waitForURL('**/admin/users/**', { timeout: 5000 });
|
||||
|
||||
await expect(page.locator('h1')).toHaveText('adminuser');
|
||||
// Profile section
|
||||
await expect(page.locator('h2:has-text("Profile")')).toBeVisible();
|
||||
await expect(page.locator('#given_name')).toBeVisible();
|
||||
// Groups section
|
||||
await expect(page.locator('h2:has-text("Groups")')).toBeVisible();
|
||||
await expect(page.locator('#groups')).toBeVisible();
|
||||
// Credentials section
|
||||
await expect(page.locator('h2:has-text("Credentials")')).toBeVisible();
|
||||
// Actions section
|
||||
await expect(page.locator('h2:has-text("Actions")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 7. Profile update
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Profile update', () => {
|
||||
test('fill in profile fields and save', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`/admin/users/${fixtures.admin_userid}`);
|
||||
|
||||
await page.fill('#given_name', 'Updated');
|
||||
await page.fill('#family_name', 'Name');
|
||||
await page.fill('#email', 'updated@example.com');
|
||||
|
||||
await page.click('section:has(h2:has-text("Profile")) button[type="submit"]');
|
||||
|
||||
const status = page.locator('#profile-status [role="status"]');
|
||||
await expect(status).toBeVisible({ timeout: 5000 });
|
||||
await expect(status).toContainText('Profile updated');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 8. Groups update
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Groups update', () => {
|
||||
test('change groups and save', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`/admin/users/${fixtures.admin_userid}`);
|
||||
|
||||
await page.fill('#groups', 'admin, users, editors');
|
||||
await page.click('section:has(h2:has-text("Groups")) button[type="submit"]');
|
||||
|
||||
const status = page.locator('#groups-status [role="status"]');
|
||||
await expect(status).toBeVisible({ timeout: 5000 });
|
||||
await expect(status).toContainText('Groups updated');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 9. Activate/deactivate toggle
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Activate/deactivate toggle', () => {
|
||||
test('deactivate then activate user', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
// Use a non-admin user for this test
|
||||
await page.goto(`/admin/users/${fixtures.admin_userid}`);
|
||||
|
||||
// Click deactivate
|
||||
await page.click('button:has-text("Deactivate user")');
|
||||
const deactivatedStatus = page.locator('#actions-section [role="status"]');
|
||||
await expect(deactivatedStatus).toBeVisible({ timeout: 5000 });
|
||||
await expect(deactivatedStatus).toContainText('User deactivated');
|
||||
|
||||
// Now an activate button should appear
|
||||
await expect(page.locator('button:has-text("Activate user")')).toBeVisible();
|
||||
|
||||
// Click activate
|
||||
await page.click('button:has-text("Activate user")');
|
||||
const activatedStatus = page.locator('#actions-section [role="status"]');
|
||||
await expect(activatedStatus).toBeVisible({ timeout: 5000 });
|
||||
await expect(activatedStatus).toContainText('User activated');
|
||||
|
||||
// Deactivate button should reappear
|
||||
await expect(page.locator('button:has-text("Deactivate user")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 10. Create invite from user list
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Create invite', () => {
|
||||
test('fill username and submit to get invite URL', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/admin/users');
|
||||
|
||||
await page.fill('form[hx-post="/admin/invite"] input[name="username"]', 'inviteduser');
|
||||
await page.click('form[hx-post="/admin/invite"] button[type="submit"]');
|
||||
|
||||
const inviteStatus = page.locator('#invite-status');
|
||||
await expect(inviteStatus.locator('.invite-url')).toBeVisible({ timeout: 5000 });
|
||||
const inviteUrl = await inviteStatus.locator('.invite-url').textContent();
|
||||
expect(inviteUrl).toContain('/register/');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 11. Re-invite from user detail
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Re-invite from user detail', () => {
|
||||
test('generate invite link from user detail page', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto(`/admin/users/${fixtures.admin_userid}`);
|
||||
|
||||
await page.click('button:has-text("Generate invite link")');
|
||||
|
||||
const inviteResult = page.locator('#invite-result');
|
||||
await expect(inviteResult.locator('.invite-url')).toBeVisible({ timeout: 5000 });
|
||||
const inviteUrl = await inviteResult.locator('.invite-url').textContent();
|
||||
expect(inviteUrl).toContain('/register/');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 12. Delete user
|
||||
// ---------------------------------------------------------------
|
||||
test.describe('Delete user', () => {
|
||||
test('delete a user and verify redirect to user list', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
|
||||
// Use a disposable user seeded specifically for this test
|
||||
await page.goto(`/admin/users/${fixtures.disposable_userid}`);
|
||||
await expect(page.locator('h1')).toHaveText('disposableuser');
|
||||
|
||||
// Set up dialog handler before clicking delete
|
||||
page.on('dialog', async (dialog) => {
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await page.click('button:has-text("Delete user")');
|
||||
|
||||
// htmx processes the HX-Redirect header and navigates to /admin/users
|
||||
await page.waitForURL('**/admin/users', { timeout: 5000 });
|
||||
await expect(page.locator('h1')).toHaveText('Users');
|
||||
|
||||
// The deleted user should no longer appear in the list
|
||||
await expect(page.locator('#user-table-body')).not.toContainText('disposableuser');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -100,6 +100,28 @@ async def seed() -> None:
|
|||
result["profile_username"] = "profileuser"
|
||||
result["profile_password"] = "profilepass123"
|
||||
|
||||
# 6. Admin user for admin page tests
|
||||
admin_user = User(
|
||||
userid="test-user-05",
|
||||
username="adminuser",
|
||||
given_name="Admin",
|
||||
family_name="User",
|
||||
email="admin@example.com",
|
||||
groups=["admin", "users"],
|
||||
)
|
||||
await user_repo.create(admin_user)
|
||||
admin_password_hash = password_service.hash("adminpass123")
|
||||
await cred_repo.create_password(PasswordCredential(user_id=admin_user.userid, password_hash=admin_password_hash))
|
||||
result["admin_username"] = "adminuser"
|
||||
result["admin_password"] = "adminpass123"
|
||||
result["admin_userid"] = "test-user-05"
|
||||
|
||||
# 7. Disposable user for admin delete test (not used by any other tests)
|
||||
disposable_user = User(userid="test-user-06", username="disposableuser", groups=["users"])
|
||||
await user_repo.create(disposable_user)
|
||||
result["disposable_userid"] = "test-user-06"
|
||||
result["disposable_username"] = "disposableuser"
|
||||
|
||||
await db.commit()
|
||||
await db.close()
|
||||
print(json.dumps(result))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue