From 837fc9bcc0fa911a40f30e7e704636490a29b10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 14:26:05 +0800 Subject: [PATCH] =?UTF-8?q?task-40(=E4=BC=9A=E8=AF=9D/=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E6=9D=83=E9=99=90):=20=E7=9C=9F=E5=AE=9E=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=80=81=20smoke=20=E6=94=B6=E5=8F=A3=E6=9C=AC=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用真实 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。 --- admin-frontend-vue/src/api/session.ts | 3 +- .../tests/live/task-40-live.test.ts | 132 ++++++++++++++++++ admin-frontend-vue/tests/task-25.test.ts | 3 +- admin-frontend-vue/tests/task-36.test.ts | 3 +- 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 admin-frontend-vue/tests/live/task-40-live.test.ts diff --git a/admin-frontend-vue/src/api/session.ts b/admin-frontend-vue/src/api/session.ts index 6e73a4b9..fade30c9 100644 --- a/admin-frontend-vue/src/api/session.ts +++ b/admin-frontend-vue/src/api/session.ts @@ -15,5 +15,6 @@ export async function fetchAdminMenuTree(): Promise { } export async function logout(): Promise { - await http.post('/api/admin/logout', undefined, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + // 登出走 auth 模块根端点 POST /logout(下发清除会话 cookie);旧 api/admin 前缀路径后端无映射。 + await http.post('/logout', undefined, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) } diff --git a/admin-frontend-vue/tests/live/task-40-live.test.ts b/admin-frontend-vue/tests/live/task-40-live.test.ts new file mode 100644 index 00000000..55fcf8e5 --- /dev/null +++ b/admin-frontend-vue/tests/live/task-40-live.test.ts @@ -0,0 +1,132 @@ +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 { + 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 { + const headers: Record = {} + 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() + 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() + 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) +}) diff --git a/admin-frontend-vue/tests/task-25.test.ts b/admin-frontend-vue/tests/task-25.test.ts index 7248de57..22e72f0f 100644 --- a/admin-frontend-vue/tests/task-25.test.ts +++ b/admin-frontend-vue/tests/task-25.test.ts @@ -43,7 +43,8 @@ test('test_task_025_cookie_session_config_boundary_single_item', () => { // 边界单元素:登出是单次 POST,以 AJAX 标识发到共享实例,后端据此返回 JSON 而非页面。 assert.match(sessionSrc, /X-Requested-With/, '登出走 AJAX 标识以便后端识别为 JSON 请求') assert.match(sessionSrc, /http\.post/) - assert.match(sessionSrc, /\/api\/admin\/logout/) + assert.match(sessionSrc, /\/logout/, '登出走 auth 模块根端点 POST /logout') + assert.equal(sessionSrc.includes('/api/admin/logout'), false, '不应指向不存在的 /api/admin/logout') }) test('test_task_025_cookie_session_config_boundary_limit_or_missing_field', () => { diff --git a/admin-frontend-vue/tests/task-36.test.ts b/admin-frontend-vue/tests/task-36.test.ts index a04bb8c0..bd975015 100644 --- a/admin-frontend-vue/tests/task-36.test.ts +++ b/admin-frontend-vue/tests/task-36.test.ts @@ -61,5 +61,6 @@ test('test_task_036_logout_clear_state_dependency_failure_returns_actionable_mes assert.match(store, /\$reset\(\)/) assert.match(store, /location\.assign\('\/login'\)/) const session = readSource('src/api/session.ts') - assert.match(session, /\/api\/admin\/logout/) + assert.match(session, /\/logout/, '登出走 auth 模块根端点 POST /logout') + assert.equal(session.includes('/api/admin/logout'), false, '不应指向不存在的 /api/admin/logout') })