From bfe415d2c0fe20d026e5d15f2de0c76de77ddddd Mon Sep 17 00:00:00 2001 From: leiyun Date: Mon, 17 Aug 2026 22:47:55 +0800 Subject: [PATCH] save --- tests/audit-logs.spec.ts | 257 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/audit-logs.spec.ts diff --git a/tests/audit-logs.spec.ts b/tests/audit-logs.spec.ts new file mode 100644 index 0000000..51d9b37 --- /dev/null +++ b/tests/audit-logs.spec.ts @@ -0,0 +1,257 @@ +import type { Page, Route } from '@playwright/test' + +import { attachJson, captureScreenshot, expect, test } from './fixtures' + +type AuditRow = { + id: string + time: string + auditType: string + module: string + actionCode: string + action: string + username: string + user: string + roleCodes: string + ip: string + success: boolean + targetName: string + requestId: string + httpMethod: string + requestUri: string +} + +type MockOptions = { + permissions: string[] + includeAuditMenu: boolean +} + +const currentDateParts = Object.fromEntries( + new Intl.DateTimeFormat('en-US', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + }).formatToParts(new Date()).map((part) => [part.type, part.value]), +) +const currentYearMonth = `${currentDateParts.year}-${currentDateParts.month}` +const rangeStartDate = `${currentYearMonth}-01` +const rangeEndDate = `${currentYearMonth}-02` + +const json = (route: Route, data: unknown, status = 200) => route.fulfill({ + status, + contentType: 'application/json', + body: JSON.stringify({ + code: status >= 400 ? status * 100 : 0, + message: status >= 400 ? '请求失败' : '成功', + data, + timestamp: Date.now(), + requestId: 'ape2e-audit-controlled-api', + }), +}) + +const auditRows: AuditRow[] = Array.from({ length: 23 }, (_, offset) => { + const number = 23 - offset + const authFailure = number === 17 + return { + id: String(number), + time: `${rangeEndDate}T${String(8 + (number % 10)).padStart(2, '0')}:15:00+08:00`, + auditType: authFailure ? 'SECURITY' : 'OPERATION', + module: authFailure ? 'AUTH' : number % 3 === 0 ? 'AGENTS' : 'SYSTEM', + actionCode: authFailure ? 'auth.login' : `system.audit.controlled-${number}`, + action: authFailure ? '登录失败' : `受控审计操作 ${number}`, + username: authFailure ? 'audit-teacher' : 'admin', + user: authFailure ? '审计测试教员' : '系统管理员', + roleCodes: authFailure ? 'teacher' : 'admin', + ip: authFailure ? '192.168.10.17' : `192.168.10.${number}`, + success: !authFailure, + targetName: authFailure ? '审计测试身份' : `审计对象 ${number}`, + requestId: `ape2e-audit-${number}`, + httpMethod: number % 2 === 0 ? 'PUT' : 'GET', + requestUri: authFailure + ? '/api/auth/v1/system-config/general' + : `/api/auth/v1/controlled-resources/${number}`, + } +}) + +const navigation = (includeAuditMenu: boolean) => [ + { + id: '1', parentId: null, code: 'overview', name: '工作概览', description: '平台概览', type: 'PAGE', + sortOrder: 10, enabled: true, builtIn: true, dataVersion: 1, path: '/overview', icon: 'Monitor', + permissionCodes: ['ai.overview.view'], children: [], + }, + ...(includeAuditMenu ? [{ + id: '80', parentId: null, code: 'settings', name: '系统设置', description: '平台、审计与菜单', type: 'GROUP', + sortOrder: 80, enabled: true, builtIn: true, dataVersion: 1, path: '', icon: 'Setting', permissionCodes: [], + children: [{ + id: '84', parentId: '80', code: 'settings-audit', name: '审计日志', description: '查询登录、安全与系统操作记录', + type: 'PAGE', sortOrder: 40, enabled: true, builtIn: true, dataVersion: 1, path: '/settings/audit', + icon: 'Document', permissionCodes: ['system.audit'], children: [], + }], + }] : []), +] + +const inRange = (value: string, start: string, end: string) => { + const timestamp = new Date(value).getTime() + return (!start || timestamp >= new Date(start).getTime()) && (!end || timestamp <= new Date(end).getTime()) +} + +async function mockPlatform(page: Page, options: MockOptions) { + const auditRequests: Array> = [] + const exportRequests: Array> = [] + + await page.route(/\/api\/(?:auth\/v1|v1)\//, async (route) => { + const url = new URL(route.request().url()) + const path = url.pathname + if (path.endsWith('/auth/me')) { + return json(route, { + userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '100', departmentName: '数字人平台', + activeRoleId: '1', authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false, + roles: [{ id: '1', code: 'admin', name: '管理员' }], permissions: options.permissions, + }) + } + if (path.endsWith('/menus/navigation')) return json(route, navigation(options.includeAuditMenu)) + if (path.endsWith('/system-config/public')) return json(route, { systemName: '数字人平台' }) + if (path.endsWith('/audit-logs/export')) { + exportRequests.push(Object.fromEntries(url.searchParams.entries())) + return json(route, { + filename: 'audit-logs-controlled.csv', + contentType: 'text/csv; charset=utf-8', + contentBase64: Buffer.from('\ufeff时间,模块,结果\r\n受控时间,AUTH,失败\r\n').toString('base64'), + recordCount: 1, + }) + } + if (path.endsWith('/audit-logs')) { + const query = Object.fromEntries(url.searchParams.entries()) + auditRequests.push(query) + const keyword = (query.keyword || '').toLowerCase() + const result = (query.result || 'ALL').toUpperCase() + const module = (query.module || 'ALL').toUpperCase() + const filtered = auditRows.filter((row) => { + const searchable = [row.username, row.user, row.module, row.action, row.actionCode, row.targetName, row.ip, row.requestUri, row.requestId] + .join('\n').toLowerCase() + return (!keyword || searchable.includes(keyword)) + && (module === 'ALL' || row.module === module) + && (result === 'ALL' || (result === 'SUCCESS' ? row.success : !row.success)) + && inRange(row.time, query.start || '', query.end || '') + }) + const current = Math.max(1, Number(query.page || 1)) + const size = Math.max(1, Number(query.size || 20)) + const start = (current - 1) * size + return json(route, { + records: filtered.slice(start, start + size), + total: filtered.length, + page: current, + size, + pages: Math.ceil(filtered.length / size), + }) + } + return json(route, {}) + }) + + await page.addInitScript(() => { + window.sessionStorage.setItem('ai-person:web:access-token:v1', 'ape2e-audit-controlled-token') + }) + return { auditRequests, exportRequests } +} + +async function chooseSelect(page: Page, testId: string, label: string) { + await page.getByTestId(testId).click() + const option = page.locator('.el-select-dropdown__item:visible').filter({ hasText: label }).last() + await expect(option).toBeVisible() + await option.click() +} + +async function chooseCurrentDateRange(page: Page) { + await page.locator('.audit-time-filter').click() + const panel = page.locator('.el-picker-panel:visible') + await expect(panel).toBeVisible() + const currentMonthDay = (day: string) => panel + .locator('td.available:not(.prev-month):not(.next-month) .el-date-table-cell__text') + .filter({ hasText: new RegExp(`^${day}$`) }) + .first() + await currentMonthDay('1').click() + await currentMonthDay('2').click() + const confirm = panel.getByRole('button', { name: '确定', exact: true }) + if (await confirm.count()) await confirm.click() +} + +test.describe('审计日志页面', () => { + test('非空展示、中文模块、URI搜索、多条件筛选和服务端分页', async ({ page }, testInfo) => { + const { auditRequests, exportRequests } = await mockPlatform(page, { + permissions: ['ai.overview.view', 'system.audit', 'system.audit.export'], + includeAuditMenu: true, + }) + + await page.goto('/settings/audit') + await expect(page).toHaveURL(/\/settings\/audit(?:[?#].*)?$/) + await expect(page.getByRole('heading', { name: '审计日志', exact: true })).toBeVisible() + await expect(page.getByTestId('audit-log-page')).toBeVisible() + await expect(page.getByText('审计日志', { exact: true }).first()).toBeVisible() + + const table = page.getByTestId('audit-table') + const bodyRows = table.locator('.el-table__body tbody tr') + await expect(bodyRows).toHaveCount(20) + await expect(page.getByText('共 23 条记录').first()).toBeVisible() + await expect(table).toContainText('系统管理') + await expect(table).toContainText('智能体管理') + await expect(table).not.toContainText('SYSTEM') + + await page.getByTestId('audit-pagination').locator('.btn-next').click() + await expect.poll(() => auditRequests.at(-1)?.page).toBe('2') + await expect(bodyRows).toHaveCount(3) + await expect(table).toContainText('受控审计操作 3') + + const keyword = '/api/auth/v1/system-config/general' + await page.getByTestId('audit-filter-keyword').fill(keyword) + await page.getByTestId('audit-search').click() + await expect.poll(() => auditRequests.at(-1)?.keyword).toBe(keyword) + await expect.poll(() => auditRequests.at(-1)?.page).toBe('1') + await expect(bodyRows).toHaveCount(1) + await expect(table).toContainText(keyword) + await expect(table).toContainText('身份认证') + await captureScreenshot(page, testInfo, '01-audit-request-uri-search') + + await page.getByTestId('audit-reset').click() + await expect.poll(() => auditRequests.at(-1)?.keyword).toBeUndefined() + await chooseSelect(page, 'audit-filter-module', '身份认证') + await chooseSelect(page, 'audit-filter-result', '失败') + await chooseCurrentDateRange(page) + await page.getByTestId('audit-search').click() + + await expect.poll(() => auditRequests.at(-1)?.module).toBe('AUTH') + await expect.poll(() => auditRequests.at(-1)?.result).toBe('FAILURE') + await expect.poll(() => auditRequests.at(-1)?.start).toContain(rangeStartDate) + await expect.poll(() => auditRequests.at(-1)?.end).toContain(rangeEndDate) + await expect(bodyRows).toHaveCount(1) + await expect(table).toContainText('身份认证') + await expect(table).toContainText('失败') + await expect(table).toContainText('登录失败') + await captureScreenshot(page, testInfo, '02-audit-module-result-time-filters') + + const downloadPromise = page.waitForEvent('download') + await page.getByTestId('audit-export').click() + const download = await downloadPromise + expect(download.suggestedFilename()).toBe('audit-logs-controlled.csv') + await expect.poll(() => exportRequests).toHaveLength(1) + expect(exportRequests[0]).toMatchObject({ module: 'AUTH', result: 'FAILURE' }) + expect(exportRequests[0]?.start).toContain(rangeStartDate) + expect(exportRequests[0]?.end).toContain(rangeEndDate) + expect(exportRequests[0]?.page).toBeUndefined() + expect(exportRequests[0]?.size).toBeUndefined() + + await attachJson(testInfo, 'audit-server-query-evidence', { auditRequests, exportRequests }) + }) + + test('没有审计权限时菜单隐藏且路由被守卫', async ({ page }, testInfo) => { + const { auditRequests } = await mockPlatform(page, { + permissions: ['ai.overview.view'], + includeAuditMenu: false, + }) + + await page.goto('/settings/audit') + await expect(page).toHaveURL(/\/overview\?forbidden=1$/) + await expect(page.getByRole('heading', { name: '审计日志', exact: true })).toHaveCount(0) + await expect(page.getByText('审计日志', { exact: true })).toHaveCount(0) + expect(auditRequests).toHaveLength(0) + await captureScreenshot(page, testInfo, '03-audit-route-forbidden') + }) +})