|
- 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<Record<string, string>> = []
- const exportRequests: Array<Record<string, string>> = []
-
- 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')
- })
- })
|