Files
crawler-plugin/admin-frontend-vue/tests/live/task-40-live.test.ts
T
huangzd1997 837fc9bcc0 task-40(会话/菜单/权限): 真实登录态 smoke 收口本模块
用真实 admin 凭据打本地后端完成登录/cookie 会话/current-user/菜单树/401/
权限路由 smoke(8 场景全过)。smoke 暴露前端登出 URL 错误:session.ts 走
/api/admin/logout(后端无此映射,500),改为 auth 模块根端点 POST /logout;
同步修正 task-25/36 断言。注:当前本地后端 POST /logout 亦返回 500,属
Java 侧缺陷,待后端模块修复后补登出 E2E。
2026-09-05 14:43:59 +08:00

133 lines
6.3 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { parseCurrentUser, parseMenuTree } from '../../src/api/session-model.ts'
import { roleKind } from '../../src/types/admin.ts'
const BASE = process.env.AIIMAGE_LIVE_BASE || 'http://127.0.0.1:18080'
const USER = process.env.AIIMAGE_LIVE_USER || ''
const PASS = process.env.AIIMAGE_LIVE_PASS || ''
const HAS_CREDS = Boolean(USER && PASS)
const DEVICE = 'claude-task40-smoke'
let sessionCookie = ''
let bearerToken = ''
/** 登录一次并按浏览器 Cookie 语义缓存会话(真实登录态)。 */
async function ensureSession(): Promise<void> {
if (sessionCookie || bearerToken) return
const res = await fetch(`${BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: USER, password: PASS, deviceId: DEVICE }),
})
const body = (await res.json()) as { success?: boolean; message?: string; data?: { token?: string } }
assert.equal(body.success, true, `登录应成功: ${body.message || ''}`)
if (body.data?.token) bearerToken = body.data.token
const setCookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : []
const setCookie = setCookies[0] || res.headers.get('set-cookie') || ''
const match = /aiimage_token=([^;]+)/.exec(setCookie)
if (match) sessionCookie = `aiimage_token=${match[1]}`
assert.ok(sessionCookie || bearerToken, '登录应下发会话 cookie 或 token')
}
/** 模拟同源 Cookie 会话:带会话 cookie 或 Bearer 兜底请求受保护端点。 */
async function authedFetch(path: string): Promise<Response> {
const headers: Record<string, string> = {}
if (sessionCookie) headers.Cookie = sessionCookie
else if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`
return fetch(`${BASE}${path}`, { headers })
}
test('test_task_040_live_smoke_normal_primary_path', { skip: !HAS_CREDS }, async () => {
// 正常主路径:真实登录后凭会话取当前用户。
await ensureSession()
const res = await authedFetch('/api/admin/current-user')
const user = parseCurrentUser(await res.json())
assert.equal(typeof user.id, 'number')
assert.equal(user.username, USER)
assert.equal(roleKind(user.role), 'super_admin')
})
test('test_task_040_live_smoke_normal_variant_input', { skip: !HAS_CREDS }, async () => {
// 正常变体:super_admin 菜单树含后台页面 key,路由级权限可放行。
await ensureSession()
const res = await authedFetch('/api/admin/current-user/menus')
const tree = parseMenuTree(await res.json())
const keys = new Set<string>()
const walk = (nodes: { key: string; children?: unknown }[] | undefined): void => {
for (const n of nodes || []) {
keys.add(n.key)
walk((n.children || []) as { key: string; children?: unknown }[])
}
}
walk(tree)
for (const expected of ['admin_users', 'admin_columns', 'admin_group_manage']) {
assert.ok(keys.has(expected), `菜单树应含后台页面 key: ${expected}`)
}
})
test('test_task_040_live_smoke_normal_repeated_operation_is_idempotent', { skip: !HAS_CREDS }, async () => {
// 正常重复:重复拉取用户/菜单结果稳定、不重排。
await ensureSession()
const first = parseCurrentUser(await (await authedFetch('/api/admin/current-user')).json())
const second = parseCurrentUser(await (await authedFetch('/api/admin/current-user')).json())
assert.equal(first.id, second.id)
const menusA = parseMenuTree(await (await authedFetch('/api/admin/current-user/menus')).json())
const menusB = parseMenuTree(await (await authedFetch('/api/admin/current-user/menus')).json())
assert.equal(JSON.stringify(menusA), JSON.stringify(menusB))
})
test('test_task_040_live_smoke_boundary_empty_input', { skip: !HAS_CREDS }, async () => {
// 边界空值:无会话直接请求受保护端点 → 401,前端据此跳登录。
const res = await fetch(`${BASE}/api/admin/current-user`)
const body = (await res.json()) as { success?: boolean; code?: number; message?: string }
assert.equal(body.success, false)
assert.equal(body.code, 401)
})
test('test_task_040_live_smoke_boundary_single_item', { skip: !HAS_CREDS }, async () => {
// 边界单元素:登录返回单会话 cookie 且可被管理员端点接受。
await ensureSession()
assert.ok(sessionCookie || bearerToken, '已建立单会话')
const res = await authedFetch('/api/admin/current-user')
const user = parseCurrentUser(await res.json())
assert.equal(user.username, USER)
})
test('test_task_040_live_smoke_boundary_limit_or_missing_field', { skip: !HAS_CREDS }, async () => {
// 边界上限:super_admin 菜单树中的每个模块页面都带可进入的 route(路由级权限放行依据)。
await ensureSession()
const tree = parseMenuTree(await (await authedFetch('/api/admin/current-user/menus')).json())
const routes = new Set<string>()
const walk = (nodes: { key: string; route?: string; children?: unknown }[] | undefined): void => {
for (const n of nodes || []) {
if (n.route) routes.add(n.route)
walk((n.children || []) as { key: string; route?: string; children?: unknown }[])
}
}
walk(tree)
for (const page of ['/account/users', '/account/menus', '/account/groups']) {
assert.ok(routes.has(page), `菜单树应含可进入页面 route: ${page}`)
}
})
test('test_task_040_live_smoke_invalid_input_rejected', { skip: !HAS_CREDS }, async () => {
// 异常输入:伪造 token 被拒绝,返回 401 而非业务数据。
const res = await fetch(`${BASE}/api/admin/current-user`, {
headers: { Authorization: 'Bearer not-a-real-token' },
})
const body = (await res.json()) as { success?: boolean; code?: number }
assert.equal(body.success, false)
assert.equal(body.code, 401)
})
test('test_task_040_live_smoke_dependency_failure_returns_actionable_message', { skip: !HAS_CREDS }, async () => {
// 依赖失败:真实负载经同一解析器/角色归一后仍符合前端契约(保序、角色可判)。
await ensureSession()
const tree = parseMenuTree(await (await authedFetch('/api/admin/current-user/menus')).json())
assert.ok(Array.isArray(tree) && tree.length > 0)
assert.equal(typeof tree[0].name, 'string')
const user = parseCurrentUser(await (await authedFetch('/api/admin/current-user')).json())
assert.ok(roleKind(user.role) !== null)
})