From 8e5fc0094566ed258f4ef2be342a0ec4c5763db2 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 15:25:13 +0800 Subject: [PATCH] =?UTF-8?q?task-60(=E8=B4=A6=E5=8F=B7/=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2):=20=E5=AE=8C=E6=88=90=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E6=9D=83=E9=99=90=E4=B8=89=E9=A1=B5=E9=9D=A2=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=20API=20=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 live 验收 task-60-account-live.test.ts:无凭据即可真实校验服务在线/登录页、 未登录 401、伪造 token 拒登(本机已跑通 3 例);带 AIIMAGE_LIVE_* 凭据时校验真实用户列表解析、 permission-menus 含 admin_users/admin_columns/admin_group_manage、分组列表、以及普通用户 创建→出现→删除闭环与授权 columnIds 读取。435 单测 + build 绿,live 目录 0 失败。 --- .../tests/live/task-60-account-live.test.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 admin-frontend-vue/tests/live/task-60-account-live.test.ts diff --git a/admin-frontend-vue/tests/live/task-60-account-live.test.ts b/admin-frontend-vue/tests/live/task-60-account-live.test.ts new file mode 100644 index 00000000..97c65395 --- /dev/null +++ b/admin-frontend-vue/tests/live/task-60-account-live.test.ts @@ -0,0 +1,148 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { parseMenuManageList, buildMenuManageTree } from '../../src/pages/account/menu-manage-model.ts' +import { parseUserPage } from '../../src/api/users-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 HAS_CREDS = Boolean(USER && PASS) +const DEVICE = 'claude-task60-account-acceptance' + +let sessionCookie = '' +let bearerToken = '' + +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 cookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [] + const setCookie = cookies[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') +} + +async function authedFetch(path: string, init: RequestInit = {}): Promise { + const headers: Record = { ...(init.headers as Record) } + if (sessionCookie) headers.Cookie = sessionCookie + else if (bearerToken) headers.Authorization = `Bearer ${bearerToken}` + if (init.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json' + return fetch(`${BASE}${path}`, { ...init, headers }) +} + +test('test_task_060_account_live_reachability_normal_primary_path', async () => { + // 正常主路径(无需凭据):真实服务在线,登录页可访问(200 + HTML)。 + const res = await fetch(`${BASE}/login`) + assert.equal(res.status, 200) + const html = await res.text() + assert.ok(html.length > 0) +}) + +test('test_task_060_account_live_reachability_boundary_empty_input', async () => { + // 边界空值(无需凭据):无会话请求用户列表 → 401 未登录,前端据此跳登录。 + const res = await fetch(`${BASE}/api/admin/users`) + const body = (await res.json()) as { success?: boolean; code?: number } + assert.equal(body.success, false) + assert.equal(body.code, 401) +}) + +test('test_task_060_account_live_users_list_normal_primary_path', { skip: !HAS_CREDS }, async () => { + // 正常主路径:真实登录后拉取用户列表并经同一解析器归一(super 可见全量)。 + await ensureSession() + const res = await authedFetch('/api/admin/users?page=1&page_size=15') + const page = parseUserPage(await res.json()) + assert.equal(res.status, 200) + assert.ok(Array.isArray(page.items)) + assert.equal(typeof page.total, 'number') + if (page.items.length) assert.equal(typeof page.items[0].id, 'number') +}) + +test('test_task_060_account_live_menus_list_normal_variant_input', { skip: !HAS_CREDS }, async () => { + // 正常变体:permission-menus(admin) 真实列表含后台页面 key,解析+建树可用。 + await ensureSession() + const res = await authedFetch('/api/admin/permission-menus?menuType=admin') + const list = parseMenuManageList(await res.json()) + const keys = new Set(list.map((n) => n.columnKey)) + for (const key of ['admin_users', 'admin_columns', 'admin_group_manage']) { + assert.ok(keys.has(key), `菜单管理列表应含 key: ${key}`) + } + const tree = buildMenuManageTree(list) + assert.ok(tree.length > 0) +}) + +test('test_task_060_account_live_groups_list_normal_single_item', { skip: !HAS_CREDS }, async () => { + // 边界单元素:真实分组列表为数组(super 返回全部),条目带分组名/组员数。 + const res = await authedFetch('/api/admin/shop-manages/groups') + const body = (await res.json()) as { success?: boolean; data?: Array<{ groupName?: string; memberCount?: number }> } + assert.equal(res.status, 200) + assert.equal(body.success, true) + assert.ok(Array.isArray(body.data)) + if (body.data.length) { + assert.equal(typeof body.data[0].groupName, 'string') + assert.equal(typeof body.data[0].memberCount, 'number') + } +}) + +test('test_task_060_account_live_user_cycle_normal_primary_path', { skip: !HAS_CREDS }, async () => { + // 正常主路径(写):真实创建普通用户 → 列表含新 id → 删除清理(结果来自真实业务非硬编码)。 + await ensureSession() + const username = `acc_live_${Date.now()}` + let createdId: number | null = null + try { + const create = await authedFetch('/api/admin/user', { + method: 'POST', + body: JSON.stringify({ username, password: 'secret1', role: 'normal', columnIds: [] }), + }) + const created = (await create.json()) as { success?: boolean; message?: string; data?: number } + assert.equal(create.status, 200) + assert.equal(created.success, true, created.message || '') + assert.equal(typeof created.data, 'number') + createdId = created.data as number + const list = parseUserPage(await (await authedFetch('/api/admin/users?page=1&page_size=200')).json()) + const found = list.items.find((u) => u.id === createdId) + assert.ok(found, '创建的用户应出现在列表') + assert.equal(found?.username, username) + } finally { + if (createdId !== null) { + await authedFetch(`/api/admin/user/${createdId}`, { method: 'DELETE' }) + } + } +}) + +test('test_task_060_account_live_user_columns_boundary_limit_or_missing_field', { skip: !HAS_CREDS }, async () => { + // 边界上限/缺字段:真实授权读取返回 columnIds 数组(刚建普通用户默认空授权)。 + await ensureSession() + const username = `acc_cols_${Date.now()}` + let createdId: number | null = null + try { + const create = await authedFetch('/api/admin/user', { + method: 'POST', + body: JSON.stringify({ username, password: 'secret1', role: 'normal', columnIds: [] }), + }) + const created = (await create.json()) as { data?: number } + createdId = created.data as number + const res = await authedFetch(`/api/admin/permission-users/${createdId}/columns?menuType=admin`) + const body = (await res.json()) as { success?: boolean; data?: { columnIds?: number[] } } + assert.equal(body.success, true) + assert.ok(Array.isArray(body.data?.columnIds)) + } finally { + if (createdId !== null) await authedFetch(`/api/admin/user/${createdId}`, { method: 'DELETE' }) + } +}) + +test('test_task_060_account_live_invalid_input_rejected', async () => { + // 异常输入(无需凭据):伪造 Bearer token 被拒,返回 401 而非业务数据。 + const res = await fetch(`${BASE}/api/admin/users`, { + 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) +})