leiyun 1ヶ月前
コミット
bfe415d2c0
1個のファイルの変更257行の追加0行の削除
  1. +257
    -0
      tests/audit-logs.spec.ts

+ 257
- 0
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<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')
})
})

読み込み中…
キャンセル
保存