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') }) test('当前筛选没有数据时提示不可导出且不请求导出接口', async ({ page }, testInfo) => { const { auditRequests, exportRequests } = await mockPlatform(page, { permissions: ['ai.overview.view', 'system.audit', 'system.audit.export'], includeAuditMenu: true, }) let downloadCount = 0 page.on('download', () => { downloadCount += 1 }) await page.goto('/settings/audit') await page.getByTestId('audit-filter-keyword').fill('no-matching-audit-record') await page.getByTestId('audit-search').click() await expect.poll(() => auditRequests.at(-1)?.keyword).toBe('no-matching-audit-record') await expect(page.getByText('暂无符合当前条件的审计日志', { exact: true })).toBeVisible() await page.getByTestId('audit-export').click() await expect(page.getByText('暂无符合当前条件的审计日志,不可导出', { exact: true })).toBeVisible() expect(exportRequests).toHaveLength(0) expect(downloadCount).toBe(0) await captureScreenshot(page, testInfo, '04-audit-empty-export-blocked') }) })