From d3154a48b89f50867a3f61555c2ad648a0d9d895 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 13:52:02 +0800 Subject: [PATCH] =?UTF-8?q?task-23(=E4=BC=9A=E8=AF=9D/=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E6=9D=83=E9=99=90):=20=E5=AE=9E=E7=8E=B0=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=AF=B7=E6=B1=82=E9=80=82=E9=85=8D=20+=20?= =?UTF-8?q?=E7=9C=9F=E5=90=8E=E7=AB=AF=20live=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session-model.parseCurrentUser 兼容 data:{item}/{item}/直接对象信封并校验 id; session.ts fetchCurrentUser 复用;启用 allowImportingTsExtensions;npm run test:live 用 admin 账号真实登录打 18080 验证 current-user 与菜单树(023/024 live 全绿)。 --- admin-frontend-vue/package.json | 1 + admin-frontend-vue/src/api/session-model.ts | 25 ++++++++ admin-frontend-vue/src/api/session.ts | 19 ++++++ .../tests/live/session-live.test.ts | 46 +++++++++++++ admin-frontend-vue/tests/task-23.test.ts | 64 +++++++++++++++++++ admin-frontend-vue/tsconfig.json | 20 ++++++ 6 files changed, 175 insertions(+) create mode 100644 admin-frontend-vue/src/api/session-model.ts create mode 100644 admin-frontend-vue/src/api/session.ts create mode 100644 admin-frontend-vue/tests/live/session-live.test.ts create mode 100644 admin-frontend-vue/tests/task-23.test.ts create mode 100644 admin-frontend-vue/tsconfig.json diff --git a/admin-frontend-vue/package.json b/admin-frontend-vue/package.json index a2c83428..0368c733 100644 --- a/admin-frontend-vue/package.json +++ b/admin-frontend-vue/package.json @@ -8,6 +8,7 @@ "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "test": "node --test tests/*.test.ts", + "test:live": "node --test tests/live/*.test.ts", "test:browser": "playwright test" }, "dependencies": { diff --git a/admin-frontend-vue/src/api/session-model.ts b/admin-frontend-vue/src/api/session-model.ts new file mode 100644 index 00000000..02e6f3bb --- /dev/null +++ b/admin-frontend-vue/src/api/session-model.ts @@ -0,0 +1,25 @@ +import { unwrap } from './envelope.ts' +import type { AdminMenuNode, AdminUser } from '../types/admin' + +/** 解析当前用户响应:支持 data:{item} 与 {item}/{直接对象} 信封。 */ +export function parseCurrentUser(payload: unknown): AdminUser { + const value = unwrap(payload) + const user = + value && typeof value === 'object' && 'item' in value && value.item + ? value.item + : (value as AdminUser) + if (!user || typeof user.id !== 'number') { + throw new Error('当前用户响应缺少有效 id') + } + return user +} + +/** + * 解析菜单树响应:支持 data:{items} 与 {items}/数组 信封。 + * 返回后端树形节点;空/缺省按 [] 处理,与“接口失败”(抛错) 区分开。 + */ +export function parseMenuTree(payload: unknown): AdminMenuNode[] { + const value = unwrap(payload) + if (Array.isArray(value)) return value + return (value && Array.isArray(value.items) ? value.items : []) as AdminMenuNode[] +} diff --git a/admin-frontend-vue/src/api/session.ts b/admin-frontend-vue/src/api/session.ts new file mode 100644 index 00000000..6e73a4b9 --- /dev/null +++ b/admin-frontend-vue/src/api/session.ts @@ -0,0 +1,19 @@ +import { http } from './http' +import { parseCurrentUser, parseMenuTree } from './session-model' +import type { AdminMenuNode, AdminUser, ApiEnvelope } from '../types/admin' + +export async function fetchCurrentUser(): Promise { + const { data } = await http.get | AdminUser>('/api/admin/current-user') + return parseCurrentUser(data) +} + +export async function fetchAdminMenuTree(): Promise { + const { data } = await http.get< + ApiEnvelope<{ items: AdminMenuNode[] }> | { items: AdminMenuNode[] } | AdminMenuNode[] + >('/api/admin/current-user/menus') + return parseMenuTree(data) +} + +export async function logout(): Promise { + await http.post('/api/admin/logout', undefined, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) +} diff --git a/admin-frontend-vue/tests/live/session-live.test.ts b/admin-frontend-vue/tests/live/session-live.test.ts new file mode 100644 index 00000000..6d705741 --- /dev/null +++ b/admin-frontend-vue/tests/live/session-live.test.ts @@ -0,0 +1,46 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { parseCurrentUser, parseMenuTree } from '../../src/api/session-model.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 DEVICE = 'claude-live-device' + +const HAS_CREDS = Boolean(USER && PASS) + +async function loginToken(): Promise { + 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 } } + if (!body.success || !body.data?.token) throw new Error(body.message || 'login failed') + return body.data.token +} + +function authHeaders(token: string): Record { + return { Authorization: `Bearer ${token}` } +} + +test('test_live_023_current_user_real_request', { skip: !HAS_CREDS }, async () => { + const token = await loginToken() + const res = await fetch(`${BASE}/api/admin/current-user`, { headers: authHeaders(token) }) + const body = await res.json() + const user = parseCurrentUser(body) + assert.equal(typeof user.id, 'number') + assert.ok(user.username.length > 0) + assert.ok(['super_admin', 'admin', 'normal'].includes(user.role)) +}) + +test('test_live_024_menu_tree_real_request', { skip: !HAS_CREDS }, async () => { + const token = await loginToken() + const res = await fetch(`${BASE}/api/admin/current-user/menus`, { headers: authHeaders(token) }) + const body = await res.json() + const tree = parseMenuTree(body) + assert.ok(Array.isArray(tree)) + assert.ok(tree.length > 0, 'super_admin 应能拿到全部 admin 菜单树') + const accountGroup = tree.find((n) => n.children?.length) + assert.ok(accountGroup, '菜单树应按分组组织') +}) diff --git a/admin-frontend-vue/tests/task-23.test.ts b/admin-frontend-vue/tests/task-23.test.ts new file mode 100644 index 00000000..8cac5c75 --- /dev/null +++ b/admin-frontend-vue/tests/task-23.test.ts @@ -0,0 +1,64 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseCurrentUser } from '../src/api/session-model.ts' + +test('test_task_023_current_user_adapter_normal_primary_path', () => { + // 正常主路径:Java data:{item} 信封解出当前用户。 + const user = parseCurrentUser({ + success: true, + data: { item: { id: 1, username: 'admin', role: 'super_admin' } }, + }) + assert.equal(user.id, 1) + assert.equal(user.username, 'admin') + assert.equal(user.role, 'super_admin') +}) + +test('test_task_023_current_user_adapter_normal_variant_input', () => { + // 正常变体:无 data 的 {item} 信封同样可解。 + const user = parseCurrentUser({ success: true, item: { id: 3, username: '张伟恒', role: 'admin' } }) + assert.equal(user.username, '张伟恒') + assert.equal(user.role, 'admin') +}) + +test('test_task_023_current_user_adapter_normal_repeated_operation_is_idempotent', () => { + // 正常重复:解析不修改输入、结果稳定。 + const payload = { success: true, data: { item: { id: 2, username: 'u', role: 'normal' } } } + assert.deepEqual(parseCurrentUser(payload), parseCurrentUser(payload)) + assert.deepEqual(payload, { success: true, data: { item: { id: 2, username: 'u', role: 'normal' } } }) +}) + +test('test_task_023_current_user_adapter_boundary_empty_input', () => { + // 边界空值:响应缺 id 视为无效并抛可操作错误。 + assert.throws(() => parseCurrentUser({ success: true, data: { item: {} } }), /缺少有效 id/) + assert.throws(() => parseCurrentUser(null), /缺少有效 id|请求失败/) +}) + +test('test_task_023_current_user_adapter_boundary_single_item', () => { + // 边界单元素:单用户最小字段即可。 + const user = parseCurrentUser({ data: { item: { id: 9, username: 'root', role: 'super_admin' } } }) + assert.equal(user.username, 'root') +}) + +test('test_task_023_current_user_adapter_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:success=false 被拒绝并带后端 message。 + assert.throws( + () => parseCurrentUser({ success: false, message: '未登录', code: 401 }), + /未登录/, + ) +}) + +test('test_task_023_current_user_adapter_invalid_input_rejected', () => { + // 异常输入:缺 role/username 不应崩溃;缺 id 拒绝。 + const user = parseCurrentUser({ data: { item: { id: 5, username: '', role: '' } } }) + assert.equal(user.username, '') + assert.throws(() => parseCurrentUser({ data: { item: { username: 'no-id' } } }), /缺少有效 id/) +}) + +test('test_task_023_current_user_adapter_dependency_failure_returns_actionable_message', () => { + // 依赖失败:会话请求适配消费同一解析器;纯解析模块不含 axios 副作用。 + const session = readSource('src/api/session.ts') + assert.match(session, /parseCurrentUser/) + const model = readSource('src/api/session-model.ts') + assert.match(model, /import \{ unwrap \} from '\.\/envelope(?:\.ts)?'/) +}) diff --git a/admin-frontend-vue/tsconfig.json b/admin-frontend-vue/tsconfig.json new file mode 100644 index 00000000..78a4141e --- /dev/null +++ b/admin-frontend-vue/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "allowImportingTsExtensions": true, + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "baseUrl": ".", + "paths": { "@/*": ["src/*"] } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "vite.config.ts"] +}