230 lines
8.7 KiB
TypeScript
230 lines
8.7 KiB
TypeScript
import { expect } from '@playwright/test';
|
|
import { test } from './helpers/fixtures';
|
|
import { mock } from './helpers/mock';
|
|
|
|
// ─── Shared mock data ────────────────────────────────────────────────────────
|
|
|
|
const makeUser = (id: string, username: string, level: number) => ({
|
|
user_id: id,
|
|
username,
|
|
nickname: username,
|
|
email: `${username}@test.local`,
|
|
avatar: null,
|
|
permission_level: level
|
|
});
|
|
|
|
const users = [makeUser('u1', 'alice', 10), makeUser('u2', 'bob', 30), makeUser('u3', 'carol', 15)];
|
|
|
|
function overrideUserList(items = users, total = items.length) {
|
|
return mock.override('GET', '/user/list', {
|
|
status: 200,
|
|
body: { status: 200, data: { items, total } }
|
|
});
|
|
}
|
|
|
|
// ─── List rendering ──────────────────────────────────────────────────────────
|
|
|
|
test('users list renders usernames', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
await overrideUserList();
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.getByText('alice').first()).toBeVisible();
|
|
await expect(page.getByText('bob').first()).toBeVisible();
|
|
await expect(page.getByText('carol').first()).toBeVisible();
|
|
});
|
|
|
|
test('users list shows total count', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
await overrideUserList(users, 42);
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.getByText(/共 42 人/)).toBeVisible();
|
|
});
|
|
|
|
// ─── Sort ────────────────────────────────────────────────────────────────────
|
|
|
|
test('clicking 权限等级 header flips sort_order in URL', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
await overrideUserList();
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// First click → sort_by=permission_level&sort_order=asc
|
|
await overrideUserList();
|
|
await page.getByRole('link', { name: /权限等级/ }).click();
|
|
await page.waitForURL(/sort_by=permission_level/);
|
|
expect(page.url()).toContain('sort_by=permission_level');
|
|
expect(page.url()).toContain('sort_order=asc');
|
|
|
|
// Second click on same column → sort_order=desc
|
|
await overrideUserList();
|
|
await page.getByRole('link', { name: /权限等级/ }).click();
|
|
await page.waitForURL(/sort_order=desc/);
|
|
expect(page.url()).toContain('sort_order=desc');
|
|
});
|
|
|
|
// ─── Filter ──────────────────────────────────────────────────────────────────
|
|
|
|
test('level filter navigates with level= param', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
await overrideUserList();
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await overrideUserList([makeUser('u2', 'bob', 30)], 1);
|
|
await page.selectOption('#level-filter', '30');
|
|
await page.waitForURL(/level=30/);
|
|
|
|
expect(page.url()).toContain('level=30');
|
|
await expect(page.getByText('bob').first()).toBeVisible();
|
|
});
|
|
|
|
// ─── Pagination ──────────────────────────────────────────────────────────────
|
|
|
|
test('prev page link is absent on page 1', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
await overrideUserList(users, 3); // 3 items, page size 20 → only 1 page
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.getByRole('link', { name: '上一页' })).not.toBeVisible();
|
|
});
|
|
|
|
test('next page link appears when there are more items', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
// Return 20 items but total=25 → next page exists
|
|
const manyUsers = Array.from({ length: 20 }, (_, i) => makeUser(`u${i}`, `user${i}`, 10));
|
|
await overrideUserList(manyUsers, 25);
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.getByRole('link', { name: '下一页' })).toBeVisible();
|
|
});
|
|
|
|
// ─── Inline edit ─────────────────────────────────────────────────────────────
|
|
|
|
test('Edit button absent for own row', async ({ page, superAdminUser }) => {
|
|
// superAdminUser has user_id: '1' (set in fixtures.ts via loginAsMockUser default)
|
|
const selfUser = makeUser('1', 'superadmin', 40);
|
|
void superAdminUser;
|
|
await overrideUserList([selfUser, makeUser('u2', 'bob', 10)]);
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// The row for 'superadmin' (own row) should have no Edit button
|
|
const superadminRow = page.getByRole('row', { name: /superadmin/ });
|
|
await expect(superadminRow.getByRole('button', { name: /编辑/ })).not.toBeVisible();
|
|
|
|
// The row for 'bob' should have an Edit button (Lv40 can assign Lv10)
|
|
const bobRow = page.getByRole('row', { name: /bob/ });
|
|
await expect(bobRow.getByRole('button', { name: /编辑/ })).toBeVisible();
|
|
});
|
|
|
|
test('inline edit happy path: confirm updates permission level', async ({
|
|
page,
|
|
superAdminUser
|
|
}) => {
|
|
void superAdminUser;
|
|
const targetUser = makeUser('u2', 'bob', 10);
|
|
await overrideUserList([targetUser]);
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Set up the PATCH mock before clicking Edit
|
|
await mock.override('PATCH', '/user/update/u2', {
|
|
status: 200,
|
|
body: { status: 200, data: {} }
|
|
});
|
|
|
|
// Click Edit on bob's row
|
|
const bobRow = page.getByRole('row', { name: /bob/ });
|
|
await bobRow.getByRole('button', { name: /编辑/ }).click();
|
|
|
|
// Select Lv30 in the inline select
|
|
await bobRow.getByRole('combobox').selectOption('30');
|
|
|
|
// Set up updated list for the re-fetch after success
|
|
await overrideUserList([{ ...targetUser, permission_level: 30 }]);
|
|
|
|
// Confirm
|
|
await bobRow.getByRole('button', { name: '确认' }).click();
|
|
|
|
// Wait for inline edit to close (success: onResult sets editingUserId = null)
|
|
await expect(bobRow.getByRole('button', { name: '确认' })).not.toBeVisible();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify PATCH was called with correct body
|
|
const requests = await mock.requests({ method: 'PATCH', path: '/user/update/u2' });
|
|
expect(requests[0].body).toMatchObject({ permission_level: 30 });
|
|
});
|
|
|
|
test('permission-matrix violation shows inline error', async ({ page, superAdminUser }) => {
|
|
void superAdminUser;
|
|
// Use a Lv10 target so the chosen level (30) is within the editor's assignable set;
|
|
// the backend mock returns 403 to simulate a policy violation at the API layer.
|
|
const targetUser = makeUser('u5', 'eve', 10);
|
|
await overrideUserList([targetUser]);
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await mock.override('PATCH', '/user/update/u5', {
|
|
status: 403,
|
|
body: { status: 403, msg: '权限矩阵限制' }
|
|
});
|
|
|
|
// 403 from PATCH does NOT trigger the 401 interceptor — no /auth/refresh needed
|
|
|
|
const eveRow = page.getByRole('row', { name: /eve/ });
|
|
await eveRow.getByRole('button', { name: /编辑/ }).click();
|
|
await expect(eveRow.getByRole('button', { name: '确认' })).toBeVisible();
|
|
// Pick a valid level so the server-side guard passes and the PATCH fires
|
|
await eveRow.getByRole('combobox').selectOption('30');
|
|
await eveRow.getByRole('button', { name: '确认' }).click();
|
|
|
|
await expect(page.getByText('权限矩阵限制')).toBeVisible();
|
|
});
|
|
|
|
// ─── Permission gate ─────────────────────────────────────────────────────────
|
|
|
|
test('Lv30 user is denied access to users page', async ({ page, _autoResetMock }) => {
|
|
void _autoResetMock;
|
|
await page.context().addCookies([
|
|
{
|
|
name: 'access_token',
|
|
value: 'test-access',
|
|
domain: 'localhost',
|
|
path: '/app',
|
|
httpOnly: true,
|
|
sameSite: 'Lax'
|
|
},
|
|
{
|
|
name: 'refresh_token',
|
|
value: 'test-refresh',
|
|
domain: 'localhost',
|
|
path: '/app',
|
|
httpOnly: true,
|
|
sameSite: 'Lax'
|
|
}
|
|
]);
|
|
await mock.override('GET', '/user/info', {
|
|
status: 200,
|
|
body: {
|
|
status: 200,
|
|
data: {
|
|
user_id: '99',
|
|
email: 'organizer@test.local',
|
|
username: 'organizer',
|
|
permission_level: 30,
|
|
allow_public: false
|
|
}
|
|
}
|
|
});
|
|
await page.goto('/app/admin/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.getByText('权限不足')).toBeVisible();
|
|
});
|