2026-06-21 20:03:32 -06:00
|
|
|
import * as path from 'path';
|
|
|
|
|
import { test, expect } from './coverage-helpers';
|
|
|
|
|
import { apiLogin, apiAdminCreateUser } from '../scenarios/helpers';
|
|
|
|
|
|
|
|
|
|
const PLUGIN_ZIP = path.join(__dirname, '..', 'fixtures', 'plugin-hello.zip');
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Admin panel — walk every tab and open a couple of forms. The admin route is
|
|
|
|
|
* one of the largest source files; exercising each tab pulls in a lot of it
|
|
|
|
|
* plus the admin endpoint module.
|
|
|
|
|
*/
|
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
|
await apiLogin(page);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('walk every admin tab', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await expect(page.getByTestId('admin-dashboard-tab')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
// Dashboard is the default tab.
|
|
|
|
|
await expect(page.getByTestId('admin-dashboard-registration-checkbox')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.getByTestId('admin-users-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-users-create-btn')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.getByTestId('admin-oidc-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-oidc-form')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.getByTestId('admin-storage-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-storage-form')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.getByTestId('admin-smtp-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-smtp-send-btn')).toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.getByTestId('admin-plugins-tab').click();
|
|
|
|
|
// Plugins panel content is conditional; assert the tab became active.
|
|
|
|
|
await expect(page.getByTestId('admin-plugins-tab')).toHaveAttribute('aria-selected', 'true');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('open the create-user form', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-users-tab').click();
|
|
|
|
|
await page.getByTestId('admin-users-create-btn').click();
|
|
|
|
|
await expect(page.getByTestId('admin-create-user-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('storage tab: change backend select', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-storage-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-storage-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-storage-backend-select').selectOption({ index: 1 }).catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('storage tab: save the local backend settings', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-storage-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-storage-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
// Keep the (safe) local backend and save — exercises the save handler without
|
|
|
|
|
// reconfiguring storage to a remote backend.
|
|
|
|
|
await page.getByTestId('admin-storage-backend-select').selectOption('local').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-save-btn').click().catch(() => {});
|
|
|
|
|
await expect(page.getByTestId('admin-storage-form')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('oidc tab: toggle enabled and fill issuer', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-oidc-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-oidc-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-oidc-enabled-checkbox').check().catch(() => {});
|
|
|
|
|
await page
|
|
|
|
|
.getByTestId('admin-oidc-issuer-input')
|
|
|
|
|
.fill('https://example.test/issuer', { timeout: 2_000 })
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function uniqUser(): string {
|
|
|
|
|
return `u${Date.now()}${Math.floor(Math.random() * 1e6)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a user via the admin form, dismiss the (modal-backdrop) create dialog,
|
|
|
|
|
* and return a locator for that user's table row.
|
|
|
|
|
*/
|
|
|
|
|
async function createUserRow(
|
|
|
|
|
page: import('@playwright/test').Page,
|
|
|
|
|
uname: string,
|
|
|
|
|
): Promise<ReturnType<import('@playwright/test').Page['locator']>> {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-users-tab').click();
|
|
|
|
|
await page.getByTestId('admin-users-create-btn').click();
|
|
|
|
|
await expect(page.getByTestId('admin-create-user-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-create-user-username-input').fill(uname);
|
|
|
|
|
await page.getByTestId('admin-create-user-email-input').fill(`${uname}@example.test`);
|
|
|
|
|
await page.getByTestId('admin-create-user-password-input').fill('TestPassword1!');
|
|
|
|
|
await page.getByTestId('admin-create-user-submit-btn').click();
|
|
|
|
|
|
|
|
|
|
const row = page.locator('tr').filter({ hasText: uname });
|
|
|
|
|
await expect(row).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
|
|
|
|
|
// The create flow can leave a modal open whose backdrop blocks row actions.
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
const cancel = page.getByTestId('admin-create-user-cancel-btn');
|
|
|
|
|
if (await cancel.isVisible().catch(() => false)) await cancel.click();
|
|
|
|
|
await expect(page.locator('.modal__backdrop')).toHaveCount(0, { timeout: 10_000 });
|
|
|
|
|
return row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('create and delete a user', async ({ page }) => {
|
|
|
|
|
const uname = uniqUser();
|
|
|
|
|
const row = await createUserRow(page, uname);
|
|
|
|
|
|
2026-07-19 18:08:07 +02:00
|
|
|
// Delete now goes through a dedicated typed-email confirmation modal
|
|
|
|
|
// (destructive-action guard — see `.btn--danger` gate in +page.svelte).
|
|
|
|
|
// The Delete button stays disabled until the admin re-types the
|
|
|
|
|
// target's email.
|
2026-06-21 20:03:32 -06:00
|
|
|
await row.locator('[data-testid^="admin-user-delete-"]').first().click();
|
2026-07-19 18:08:07 +02:00
|
|
|
await expect(page.getByTestId('admin-delete-user-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-delete-user-email-input').fill(`${uname}@example.test`);
|
|
|
|
|
await page.getByTestId('admin-delete-user-confirm-btn').click();
|
2026-06-21 20:03:32 -06:00
|
|
|
await expect(page.locator('tr').filter({ hasText: uname })).toHaveCount(0, { timeout: 15_000 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('toggle a user role and active state', async ({ page }) => {
|
|
|
|
|
const uname = uniqUser();
|
|
|
|
|
const row = await createUserRow(page, uname);
|
|
|
|
|
|
|
|
|
|
// Each of these confirms through the admin confirm modal.
|
|
|
|
|
await row.locator('[data-testid^="admin-user-toggle-role-"]').first().click();
|
|
|
|
|
await page.getByTestId('admin-confirm-ok-btn').click();
|
|
|
|
|
await row.locator('[data-testid^="admin-user-toggle-active-"]').first().click(); // deactivate
|
|
|
|
|
await page.getByTestId('admin-confirm-ok-btn').click();
|
|
|
|
|
await row.locator('[data-testid^="admin-user-toggle-active-"]').first().click(); // reactivate
|
|
|
|
|
await page.getByTestId('admin-confirm-ok-btn').click();
|
|
|
|
|
|
|
|
|
|
await expect(row).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('reset a user password', async ({ page }) => {
|
|
|
|
|
const uname = uniqUser();
|
|
|
|
|
const row = await createUserRow(page, uname);
|
|
|
|
|
|
|
|
|
|
await row.locator('[data-testid^="admin-user-reset-password-"]').first().click();
|
|
|
|
|
await expect(page.getByTestId('admin-reset-password-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-reset-password-input').fill('NewPassword1!');
|
|
|
|
|
await page.getByTestId('admin-reset-password-submit-btn').click();
|
|
|
|
|
await expect(page.getByTestId('admin-reset-password-form')).toHaveCount(0, { timeout: 15_000 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('save a user quota and deactivate the user', async ({ page }) => {
|
|
|
|
|
const uname = uniqUser();
|
|
|
|
|
const row = await createUserRow(page, uname);
|
|
|
|
|
|
2026-07-19 18:08:07 +02:00
|
|
|
// The quota row-button and the QuotaEditor modal now share the
|
|
|
|
|
// `admin-user-quota-` prefix — the button is `admin-user-quota-<id>`
|
|
|
|
|
// and the modal's form / save button are `admin-user-quota-form` /
|
|
|
|
|
// `admin-user-quota-save-btn`. Scope the row lookup to the button
|
|
|
|
|
// shape (trailing UUID) so the modal's siblings don't match first.
|
|
|
|
|
await row.locator('button[data-testid^="admin-user-quota-"]').first().click();
|
|
|
|
|
await expect(page.getByTestId('admin-user-quota-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-user-quota-save-btn').click();
|
|
|
|
|
await expect(page.getByTestId('admin-user-quota-form')).toHaveCount(0, { timeout: 15_000 });
|
2026-06-21 20:03:32 -06:00
|
|
|
|
|
|
|
|
// Deactivate (admin's own confirm modal).
|
|
|
|
|
await row.locator('[data-testid^="admin-user-toggle-active-"]').first().click();
|
|
|
|
|
await page.getByTestId('admin-confirm-ok-btn').click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('save oidc settings', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-oidc-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-oidc-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-oidc-issuer-input').fill('https://example.test/issuer').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-oidc-client-id-input').fill('client-123').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-oidc-save-btn').click().catch(() => {});
|
|
|
|
|
await expect(page.getByTestId('admin-oidc-form')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('install, toggle, and delete a plugin', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-plugins-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-plugins-tab')).toHaveAttribute('aria-selected', 'true');
|
|
|
|
|
|
|
|
|
|
// Install the example hello plugin (plugins are enabled in the coverage env).
|
|
|
|
|
await page.getByTestId('admin-plugins-install-input').setInputFiles(PLUGIN_ZIP);
|
|
|
|
|
|
|
|
|
|
// The installed plugin shows up with toggle/delete controls.
|
|
|
|
|
const toggle = page.locator('[data-testid^="admin-plugin-toggle-"]').first();
|
|
|
|
|
await expect(toggle).toBeVisible({ timeout: 20_000 });
|
|
|
|
|
await toggle.click(); // disable
|
|
|
|
|
await page.waitForTimeout(400);
|
|
|
|
|
await toggle.click(); // re-enable
|
|
|
|
|
await page.waitForTimeout(400);
|
|
|
|
|
|
|
|
|
|
// Delete it (admin confirm modal).
|
|
|
|
|
await page.locator('[data-testid^="admin-plugin-delete-"]').first().click();
|
|
|
|
|
const ok = page.getByTestId('admin-confirm-ok-btn');
|
|
|
|
|
if (await ok.isVisible().catch(() => false)) await ok.click();
|
|
|
|
|
await expect(page.locator('[data-testid^="admin-plugin-toggle-"]')).toHaveCount(0, {
|
|
|
|
|
timeout: 15_000,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('view plugin logs and details', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-plugins-tab').click();
|
|
|
|
|
await page.getByTestId('admin-plugins-install-input').setInputFiles(PLUGIN_ZIP);
|
|
|
|
|
await expect(page.locator('[data-testid^="admin-plugin-details-"]').first()).toBeVisible({
|
|
|
|
|
timeout: 20_000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Open the logs/details view and exercise its controls, then close.
|
|
|
|
|
await page.locator('[data-testid^="admin-plugin-details-"]').first().click();
|
|
|
|
|
await page.getByTestId('admin-plugin-logs-level-select').selectOption({ index: 1 }).catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-plugin-logs-search-input').fill('hello').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-plugin-logs-search-btn').click({ timeout: 2_000 }).catch(() => {});
|
|
|
|
|
// Note: the live-tail checkbox opens an SSE stream that prevents the page from
|
|
|
|
|
// settling, so it's left untested here.
|
|
|
|
|
await page.getByTestId('admin-plugin-logs-close-btn').click({ timeout: 3_000 }).catch(() => {});
|
|
|
|
|
|
|
|
|
|
// Clean up: delete the plugin.
|
|
|
|
|
await page.locator('[data-testid^="admin-plugin-delete-"]').first().click();
|
|
|
|
|
const ok = page.getByTestId('admin-confirm-ok-btn');
|
|
|
|
|
if (await ok.isVisible().catch(() => false)) await ok.click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('plugins tab: save retention settings', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-plugins-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-plugins-tab')).toHaveAttribute('aria-selected', 'true');
|
|
|
|
|
// The retention form is conditional; fill + save it when present.
|
|
|
|
|
const retention = page.getByTestId('admin-plugin-retention-form');
|
|
|
|
|
if (await retention.isVisible().catch(() => false)) {
|
|
|
|
|
await page.getByTestId('admin-plugin-retention-days-input').fill('30').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-plugin-retention-save-btn').click().catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('toggle the dashboard registration setting', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await expect(page.getByTestId('admin-dashboard-registration-checkbox')).toBeVisible({
|
|
|
|
|
timeout: 15_000,
|
|
|
|
|
});
|
|
|
|
|
await page.getByTestId('admin-dashboard-registration-checkbox').click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('send a test email from the smtp tab', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-smtp-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-smtp-to-input')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-smtp-to-input').fill('test@example.test');
|
|
|
|
|
await page.getByTestId('admin-smtp-send-btn').click();
|
|
|
|
|
await expect(page.getByTestId('admin-smtp-send-btn')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('storage tab: fill the S3 backend fields', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-storage-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-storage-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
|
|
|
|
|
// Switch to S3 to reveal + fill the conditional fields (no save — that would
|
|
|
|
|
// reconfigure storage to an unreachable backend).
|
|
|
|
|
await page.getByTestId('admin-storage-backend-select').selectOption('s3').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-endpoint-input').fill('https://s3.example.test', { timeout: 2_000 }).catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-bucket-input').fill('e2e-bucket', { timeout: 2_000 }).catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-region-input').fill('us-east-1', { timeout: 2_000 }).catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-access-key-input').fill('AKIA', { timeout: 2_000 }).catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-secret-key-input').fill('secret', { timeout: 2_000 }).catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-storage-path-style-checkbox').click({ timeout: 2_000 }).catch(() => {});
|
|
|
|
|
// Switch back to the safe local backend.
|
|
|
|
|
await page.getByTestId('admin-storage-backend-select').selectOption('local').catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('oidc tab: run discovery against a bogus issuer', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-oidc-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-oidc-form')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-oidc-issuer-input').fill('https://idp.example.test').catch(() => {});
|
|
|
|
|
// Discovery fails (no real IdP) — exercises the discover + error path.
|
|
|
|
|
await page.getByTestId('admin-oidc-discover-btn').click().catch(() => {});
|
|
|
|
|
await page.waitForTimeout(800);
|
|
|
|
|
await expect(page.getByTestId('admin-oidc-form')).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('users tab: paginate the user list', async ({ page }) => {
|
|
|
|
|
// The list paginates at PAGE_SIZE (25); create enough to get a second page.
|
|
|
|
|
await apiLogin(page);
|
|
|
|
|
for (let i = 0; i < 26; i++) {
|
|
|
|
|
await apiAdminCreateUser(page, `pageu${Date.now()}${i}`);
|
|
|
|
|
}
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-users-tab').click();
|
|
|
|
|
await expect(page.getByTestId('admin-users-pager-next-btn')).toBeVisible({ timeout: 15_000 });
|
|
|
|
|
await page.getByTestId('admin-users-pager-next-btn').click();
|
|
|
|
|
await page.waitForTimeout(400);
|
|
|
|
|
await page.getByTestId('admin-users-pager-prev-btn').click().catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('plugins tab: install then save retention settings', async ({ page }) => {
|
|
|
|
|
await page.goto('/admin');
|
|
|
|
|
await page.getByTestId('admin-plugins-tab').click();
|
|
|
|
|
await page.getByTestId('admin-plugins-install-input').setInputFiles(PLUGIN_ZIP);
|
|
|
|
|
await expect(page.locator('[data-testid^="admin-plugin-delete-"]').first()).toBeVisible({
|
|
|
|
|
timeout: 20_000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Retention form is shown once a plugin exists.
|
|
|
|
|
const retention = page.getByTestId('admin-plugin-retention-form');
|
|
|
|
|
if (await retention.isVisible().catch(() => false)) {
|
|
|
|
|
await page.getByTestId('admin-plugin-retention-days-input').fill('30').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-plugin-retention-max-input').fill('100').catch(() => {});
|
|
|
|
|
await page.getByTestId('admin-plugin-retention-save-btn').click().catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up.
|
|
|
|
|
await page.locator('[data-testid^="admin-plugin-delete-"]').first().click();
|
|
|
|
|
const ok = page.getByTestId('admin-confirm-ok-btn');
|
|
|
|
|
if (await ok.isVisible().catch(() => false)) await ok.click();
|
|
|
|
|
});
|