vdi-cloud/console/e2e/audits.spec.ts

93 lines
8.0 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
async function fixture(page: Page, options: { fail?: boolean; oversized?: boolean; exportFail?: boolean } = {}) {
const queries: URLSearchParams[] = [], exports: URLSearchParams[] = [], writes: string[] = []
const now = new Date().toISOString()
await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'audit-fixture'))
await page.route('**/api/v1/**', async route => {
const request = route.request(), url = new URL(request.url()), path = url.pathname.replace('/api/v1', '')
const list = (items: unknown[], extra = {}) => route.fulfill({ json: { items, total: items.length, page: 1, pageSize: 100, requestId: 'audit-ui', ...extra } })
if (request.method() !== 'GET') { writes.push(path); await route.fulfill({ status: 403, json: { message: '不应发送写请求' } }); return }
if (path === '/auth/me') { await route.fulfill({ json: { id: 'reader', username: 'reader', displayName: '只读人员', role: 'viewer' } }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', capabilities: ['audit'], pveConfigured: false } }); return }
if (path === '/overview') { await route.fulfill({ json: { terminalCount: 0, onlineTerminals: 0, desktopCount: 0, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return }
if (path === '/audit-logs/export') {
exports.push(url.searchParams)
if (options.exportFail) { await route.fulfill({ status: 413, json: { message: '匹配记录超过 10000 条,请缩小时间范围', code: 'export_too_large', requestId: 'audit-ui' } }); return }
await route.fulfill({ contentType: 'text/csv; charset=utf-8', body: '\ufeff时间,操作者\r\n2026-11-11T00:00:00Z,root\r\n' }); return
}
if (path === '/audit-logs') {
queries.push(url.searchParams)
if (options.fail && url.searchParams.get('keyword')) { await route.fulfill({ status: 500, json: { message: '审计查询暂不可用', code: 'database_error', requestId: 'audit-ui' } }); return }
const p = Number(url.searchParams.get('page') || 1), filtered = !!url.searchParams.get('actor'), total = options.oversized ? 10001 : filtered ? 1 : 45
const items = Array.from({ length: Math.min(20, total - (p - 1) * 20) }, (_, i) => ({ id: `audit-${(p - 1) * 20 + i + 1}`, actor: filtered ? 'operator_%' : 'root', action: 'save_alert_rule', target: `node-${(p - 1) * 20 + i + 1}`, result: 'success', requestId: `req-${p}-${i}`, createdAt: now }))
await list(items, { total, page: p, pageSize: 20 }); return
}
if (['/resources', '/desktops', '/users', '/terminals', '/tasks'].includes(path)) { await list([]); return }
await route.fulfill({ status: 404, json: { message: '未定义的测试接口' } })
})
return { queries, exports, writes }
}
test('按需加载审计,真实分页参数和固定截止时间,浅色移动布局', async ({ page }, info) => {
const calls = await fixture(page); await page.goto('/tasks')
await expect(page.getByRole('tab', { name: '执行任务' })).toBeVisible()
expect(calls.queries).toHaveLength(0)
await page.getByRole('tab', { name: '操作审计' }).click()
await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toBeVisible()
await page.locator('.el-pagination .btn-next').click()
await expect(page.getByRole('cell', { name: 'node-21', exact: true })).toBeVisible()
expect(calls.queries[calls.queries.length - 1]?.get('page')).toBe('2')
expect(calls.queries[calls.queries.length - 1]?.get('to')).toBe(calls.queries[0]?.get('to'))
await page.screenshot({ path: info.outputPath('audit-dark.png'), fullPage: true, animations: 'disabled' })
await page.getByRole('button', { name: '切换浅色主题' }).click(); await page.setViewportSize({ width: 390, height: 844 })
await page.screenshot({ path: info.outputPath('audit-mobile-light.png'), fullPage: true, animations: 'disabled' })
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
expect(calls.writes).toEqual([])
})
test('操作人、动作、结果与时间筛选传到后端,导出使用已查询条件而非未提交的输入', async ({ page }) => {
const calls = await fixture(page); await page.goto('/tasks'); await page.getByRole('tab', { name: '操作审计' }).click()
await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toBeVisible()
await page.getByRole('textbox', { name: '审计操作者', exact: true }).fill('operator_%')
await page.getByRole('textbox', { name: '审计关键词' }).fill('req_trace')
await page.getByRole('combobox', { name: '审计动作' }).click(); await page.getByRole('option', { name: '保存告警规则', exact: true }).click()
await page.getByRole('combobox', { name: '审计结果' }).click(); await page.getByRole('option', { name: '成功', exact: true }).click()
await page.getByRole('combobox', { name: '审计时间范围' }).first().click(); await page.getByRole('button', { name: '最近 7 天', exact: true }).click()
await page.getByRole('button', { name: '查询审计', exact: true }).click()
await expect(page.getByRole('cell', { name: 'operator_%', exact: true })).toBeVisible()
const q = calls.queries[calls.queries.length - 1]!
expect(Object.fromEntries(q)).toMatchObject({ actor: 'operator_%', keyword: 'req_trace', action: 'save_alert_rule', result: 'success', page: '1', pageSize: '20' })
expect(q.get('from')).toBeTruthy(); expect(q.get('to')).toBeTruthy()
await page.getByRole('textbox', { name: '审计关键词' }).fill('还未查询的输入')
const downloaded = page.waitForEvent('download'); await page.getByRole('button', { name: '导出查询结果 CSV' }).click()
expect((await downloaded).suggestedFilename()).toMatch(/操作审计.*\.csv$/)
expect(Object.fromEntries(calls.exports[0]!)).toMatchObject({ actor: 'operator_%', keyword: 'req_trace', action: 'save_alert_rule', result: 'success', from: q.get('from'), to: q.get('to') })
expect(calls.exports[0]?.get('page')).toBeNull()
await page.getByRole('button', { name: '重置', exact: true }).click()
await expect(page.getByRole('cell', { name: 'root', exact: true }).first()).toBeVisible()
expect(calls.queries[calls.queries.length - 1]?.get('actor')).toBe('')
expect(calls.writes).toEqual([])
})
test('查询失败清除旧结果并禁用导出,允许重试', async ({ page }) => {
const calls = await fixture(page, { fail: true }); await page.goto('/tasks'); await page.getByRole('tab', { name: '操作审计' }).click()
await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toBeVisible()
await page.getByRole('textbox', { name: '审计关键词' }).fill('failure'); await page.getByRole('button', { name: '查询审计', exact: true }).click()
await expect(page.getByText(/审计查询暂不可用/)).toBeVisible(); await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toHaveCount(0)
await expect(page.getByRole('button', { name: '导出查询结果 CSV' })).toBeDisabled()
await page.getByRole('button', { name: '重置', exact: true }).click(); await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toBeVisible()
expect(calls.exports).toHaveLength(0)
})
test('导出超量有明确提示,服务端导出错误保留查询结果', async ({ page }) => {
await fixture(page, { oversized: true }); await page.goto('/tasks'); await page.getByRole('tab', { name: '操作审计' }).click()
await expect(page.getByText(/匹配记录超过 10000 条/)).toBeVisible()
await expect(page.getByRole('button', { name: '导出查询结果 CSV' })).toBeDisabled()
await page.unrouteAll(); await fixture(page, { exportFail: true }); await page.reload(); await page.getByRole('tab', { name: '操作审计' }).click()
await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toBeVisible()
await page.getByRole('button', { name: '导出查询结果 CSV' }).click()
await expect(page.getByText(/匹配记录超过 10000 条/)).toBeVisible()
await expect(page.getByRole('cell', { name: 'node-1', exact: true })).toBeVisible()
})