// services/api/test/admin-users.e2e-spec.ts —— P4a Task 6 账号管理 e2e(先写后 // 实现)。真实 app + MySQL。覆盖 GET /admin/users 管理列表模式(双模式另一半, // 前缀模式语义由 users-lookup.e2e-spec 守护)与 PATCH / reset-password / // activate / deactivate 四个治理端点: // ① 管理列表:无参数 admin → 全量用户(含 pending/disabled/locked——与前缀 // 模式「仅激活资格」相反,治理列表要看见所有人);createdAt+ID 稳定倒序 + // 游标分页(limit=2 走完 5 条,hasMore/nextCursor 成对不变式); // ② query 过滤:account 或 displayName 二进制前缀(大小写敏感;'AGOV' 空、 // '岳飞' 命中 displayName);空串/超长 → 400; // ③ status 过滤矩阵:归类优先级 pending > disabled > locked > active // (pending+disabled+locked → pending;disabled+locked → disabled); // 非法取值 → 400; // ④ 访问策略:staff 无前缀(管理列表)→ 403;staff 带 accountPrefix → 200 // (前缀模式保留);匿名 → 401; // ⑤ PATCH:role staff→admin→staff 往返 + localLocked 锁定(撤全部会话)/ // 解锁(可再登录)+ displayName;sourceStatus 只读(出现即 400);空体/ // role 非法值 → 400;自我保护:自改角色/自锁 → 400(admin 自锁会把唯一 // 管理员锁在门外的安全事故面,displayName 自改允许);audit user.update // 仅记变化字段 from→to(幂等 no-op 不重复落审计,同相册域先例); // ⑥ reset-password:指定 newPassword → 旧密码 401 新密码 200;目标用户全部 // 既有会话撤销(执行方 admin 自身会话不受影响);audit user.password_reset // 绝不含密码明文;缺省 → 服务端生成(长度 ≥12、字母数字字符集、仅返回 // 一次、两次生成不同、可登录);7/129 长度 → 400; // ⑦ activate:pending→active;对 active 重复激活幂等 200(不重复落审计); // 组合流:activate → reset-password(生成)→ 用生成密码登录 200 // (§10 待激活用户闭环); // ⑧ deactivate:localLocked=true + 目标全部会话撤销(双令牌均 401)+ 正确 // 密码登录 401(守卫资格拒);audit user.deactivate;重复停用幂等无重复 // 审计;重新启用路径 = PATCH localLocked=false(activate 只管 // credentialStatus,解锁是 localLocked 维度)→ 原密码可再登录; // ⑨ 权限与目标校验:非 admin 全端点 403;幽灵用户 404 NOT_FOUND。 import { randomBytes } from 'node:crypto'; import { type INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import request from 'supertest'; import { PrismaClient } from '@photo/db-cn'; import { ErrorCodes } from '@photo/contracts'; import { AppCnModule } from '../src/app-cn.module'; import { hashPassword } from '../src/auth/password'; import { hashSessionToken } from '../src/auth/session.guard'; import { API_PREFIX } from '../src/shared/constants'; import { resetDb } from './reset'; jest.setTimeout(120_000); describe('账号管理 /admin/users 治理端点 (e2e)', () => { let app: INestApplication; let prisma: PrismaClient; let adminToken: string; let adminId: string; let staffToken: string; // 治理操作目标(各用例独立用户,避免用例间状态交叉)。 let patchTargetId: string; // ⑤ PATCH role/localLocked/displayName let resetTargetId: string; // ⑥ reset-password(指定 + 生成两路径) let pendingTargetId: string; // ⑦ activate 组合流(credentialStatus=pending) let deactivateTargetId: string; // ⑧ deactivate(双会话撤销) // 显式 createdAt(T0 + i 分钟):管理列表 createdAt 倒序的确定性断言基础。 const T0 = Date.UTC(2026, 8, 20, 8, 0, 0); const at = (minutes: number) => new Date(T0 + minutes * 60_000); /** 分页组:agov-page-01..05(createdAt 递增 → 列表倒序 05..01)。 */ const PAGE_ACCOUNTS = Array.from( { length: 5 }, (_, i) => `agov-page-${String(i + 1).padStart(2, '0')}`, ); beforeAll(async () => { await resetDb(); const moduleRef = await Test.createTestingModule({ imports: [AppCnModule], }).compile(); app = moduleRef.createNestApplication(); app.setGlobalPrefix(API_PREFIX); await app.init(); prisma = new PrismaClient(); const mkUser = ( account: string, displayName: string, extra: { role?: string; sourceStatus?: string; credentialStatus?: string; localLocked?: boolean; passwordHash?: string; createdAt?: Date; } = {}, ) => prisma.users.create({ data: { account, passwordHash: 'x', displayName, ...extra }, select: { id: true }, }); // ① 分页组(互不重叠的前缀,隔离断言域)。 for (let i = 0; i < PAGE_ACCOUNTS.length; i++) { await mkUser(PAGE_ACCOUNTS[i], `分页${i + 1}`, { createdAt: at(i + 1) }); } // ② displayName 前缀命中组(account 与查询前缀无关)。 await mkUser('agov-dn', '岳飞鹏', { createdAt: at(30) }); // ③ status 归类组:active / pending / disabled / locked 各一。 await mkUser('agov-status-a', '状态活', { createdAt: at(40) }); await mkUser('agov-status-p', '待激活', { credentialStatus: 'pending', createdAt: at(41), }); await mkUser('agov-status-d', '上游停', { sourceStatus: 'disabled', createdAt: at(42), }); await mkUser('agov-status-l', '本地锁', { localLocked: true, createdAt: at(43), }); // ③ 归类优先级组:pending+disabled+locked → pending;disabled+locked → disabled。 await mkUser('agov-prio-b', '多态备', { credentialStatus: 'pending', sourceStatus: 'disabled', localLocked: true, createdAt: at(44), }); await mkUser('agov-prio-dl', '双态锁', { sourceStatus: 'disabled', localLocked: true, createdAt: at(45), }); // ⑤ PATCH 目标:staff + 真实密码 + 一个会话(锁定撤会话断言)。 const patchTarget = await mkUser('agov-patch', '.patch目标', { passwordHash: await hashPassword('Patch-Old-123'), createdAt: at(50), }); patchTargetId = patchTarget.id; // ⑥ reset 目标:真实密码 + 会话(旧密码失效/会话撤销断言)。 const resetTarget = await mkUser('agov-reset', '.reset目标', { passwordHash: await hashPassword('Reset-Old-123'), createdAt: at(51), }); resetTargetId = resetTarget.id; // ⑦ activate 目标:同步创建语义的待激活用户(真实密码,激活后经重置出凭据)。 const pendingTarget = await mkUser('agov-pend2', '.待激活目标', { credentialStatus: 'pending', passwordHash: await hashPassword('Pending-Old-9'), createdAt: at(52), }); pendingTargetId = pendingTarget.id; // ⑧ deactivate 目标:双会话(全部撤销断言)。 const deactivateTarget = await mkUser('agov-deact', '.停用目标', { passwordHash: await hashPassword('Deact-Old-123'), createdAt: at(53), }); deactivateTargetId = deactivateTarget.id; // 请求方:admin 与非 admin staff(createdAt 最晚,不出现在过滤断言域)。 adminId = ( await mkUser('agov-admin', '管理员', { role: 'admin', createdAt: at(90), }) ).id; const staffId = ( await mkUser('agov-staff', '普通员工', { createdAt: at(91) }) ).id; const mkToken = async (userId: string): Promise => { const token = randomBytes(32).toString('hex'); await prisma.sessions.create({ data: { userId, tokenHash: hashSessionToken(token), expiresAt: new Date(Date.now() + 3_600_000), }, }); return token; }; adminToken = await mkToken(adminId); staffToken = await mkToken(staffId); // ⑤ PATCH 锁定撤会话:目标自带一个会话。 patchTargetToken = await mkToken(patchTargetId); // ⑥ reset:目标会话(应撤销,admin 自身不受影响)。 resetTargetToken = await mkToken(resetTargetId); // ⑧ deactivate:目标双会话(两条都应 401)。 deactivateToken1 = await mkToken(deactivateTargetId); deactivateToken2 = await mkToken(deactivateTargetId); }); afterAll(async () => { await app.close(); await prisma.$disconnect(); }); // —— 会话令牌(beforeAll 内赋值;撤会话断言用)—— let patchTargetToken: string; let resetTargetToken: string; let deactivateToken1: string; let deactivateToken2: string; function listUsers( token: string | null, query?: Record, ) { const req = request(app.getHttpServer()).get(`/${API_PREFIX}/admin/users`); if (token !== null) { req.set('Authorization', `Bearer ${token}`); } return req.query(query ?? {}); } function patchUser(token: string, userId: string, body: object) { return request(app.getHttpServer()) .patch(`/${API_PREFIX}/admin/users/${userId}`) .set('Authorization', `Bearer ${token}`) .send(body); } function resetPassword(token: string, userId: string, body?: object) { const req = request(app .getHttpServer()) .post(`/${API_PREFIX}/admin/users/${userId}/reset-password`) .set('Authorization', `Bearer ${token}`); return body === undefined ? req.send(undefined) : req.send(body); } function activate(token: string, userId: string) { return request(app.getHttpServer()) .post(`/${API_PREFIX}/admin/users/${userId}/activate`) .set('Authorization', `Bearer ${token}`) .send({}); } function deactivate(token: string, userId: string) { return request(app.getHttpServer()) .post(`/${API_PREFIX}/admin/users/${userId}/deactivate`) .set('Authorization', `Bearer ${token}`) .send({}); } /** 真实登录(限速键 = account|IP;每账号失败次数控制在本 spec 远低于 5)。 */ function login(account: string, password: string) { return request(app.getHttpServer()) .post(`/${API_PREFIX}/auth/password`) .send({ account, password }); } /** 守卫放行性探针:任意受保护端点(此处用管理列表)验证令牌是否仍有效。 */ function probeToken(token: string) { return request(app.getHttpServer()) .get(`/${API_PREFIX}/admin/users`) .set('Authorization', `Bearer ${token}`) .query({ query: 'agov-page' }); } it('① 管理列表:无参数 admin → 含全部治理状态;createdAt 倒序游标分页走全量', async () => { // 管理列表要「看见所有人」:pending/disabled/locked 与 active 并列出现 //(前缀模式只回激活资格用户,两个模式语义互补)。 const all = await listUsers(adminToken, { query: 'agov-status' }); expect(all.status).toBe(200); expect( (all.body.items as { account: string }[]).map((i) => i.account).sort(), ).toEqual( ['agov-status-a', 'agov-status-p', 'agov-status-d', 'agov-status-l'].sort(), ); // 游标分页:limit=2 三页走完 5 条,倒序 05→01。 const accounts: string[] = []; let cursor: string | undefined; let pages = 0; for (;;) { const res = await listUsers(adminToken, { query: 'agov-page', limit: 2, ...(cursor === undefined ? {} : { cursor }), }); expect(res.status).toBe(200); const items = res.body.items as { account: string }[]; accounts.push(...items.map((i) => i.account)); // CursorPageMeta 不变式:hasMore=true ⟺ nextCursor≠null。 expect(res.body.hasMore).toBe(res.body.nextCursor !== null); cursor = res.body.nextCursor; pages += 1; if (cursor === null || cursor === undefined || pages > 5) { break; } } expect(pages).toBe(3); // 2 + 2 + 1 expect(accounts).toEqual([...PAGE_ACCOUNTS].reverse()); }); it('② query 过滤:account/displayName 二进制前缀;大小写敏感;空串/超长 400', async () => { const upper = await listUsers(adminToken, { query: 'AGOV-PAGE' }); expect(upper.status).toBe(200); expect(upper.body.items).toEqual([]); // 'agov-page-0' 是 01..05 全体的前缀(05 亦命中)→ 5 条全中。 const hit = await listUsers(adminToken, { query: 'agov-page-0' }); expect(hit.status).toBe(200); expect((hit.body.items as { account: string }[]).map((i) => i.account)).toEqual( [...PAGE_ACCOUNTS].reverse(), ); // 精确到单条的前缀。 const one = await listUsers(adminToken, { query: 'agov-page-03' }); expect(one.status).toBe(200); expect((one.body.items as { account: string }[]).map((i) => i.account)).toEqual( ['agov-page-03'], ); // displayName 前缀(account 'agov-dn' 与 '岳' 无关)。 const byName = await listUsers(adminToken, { query: '岳飞' }); expect(byName.status).toBe(200); expect(byName.body.items).toEqual([ expect.objectContaining({ account: 'agov-dn', displayName: '岳飞鹏' }), ]); for (const bad of ['', 'x'.repeat(65)]) { const res = await listUsers(adminToken, { query: bad }); expect(res.status).toBe(400); expect(res.body.code).toBe(ErrorCodes.VALIDATION_FAILED); } }); it('③ status 过滤矩阵:归类优先级 pending > disabled > locked > active;非法值 400', async () => { const pick = async (status: string) => { const res = await listUsers(adminToken, { status }); expect(res.status).toBe(200); return (res.body.items as { account: string }[]).map((i) => i.account); }; // 优先级裁决:agov-prio-b(pending+disabled+locked)归 pending; // agov-prio-dl(disabled+locked)归 disabled(压过 locked)。 // agov-pend2 此时仍是 pending(⑦ 才激活)——按 createdAt 倒序排首位。 expect(await pick('pending')).toEqual([ 'agov-pend2', 'agov-prio-b', 'agov-status-p', ]); expect(await pick('disabled')).toEqual(['agov-prio-dl', 'agov-status-d']); expect(await pick('locked')).toEqual(['agov-status-l']); expect(await pick('active')).toContain('agov-status-a'); expect(await pick('active')).not.toContain('agov-status-p'); expect(await pick('active')).not.toContain('agov-status-d'); expect(await pick('active')).not.toContain('agov-status-l'); const bad = await listUsers(adminToken, { status: 'bogus' }); expect(bad.status).toBe(400); expect(bad.body.code).toBe(ErrorCodes.VALIDATION_FAILED); }); it('④ 访问策略:staff 无前缀 → 403;staff 带 accountPrefix → 200(前缀模式保留);匿名 401', async () => { const staffList = await listUsers(staffToken, {}); expect(staffList.status).toBe(403); expect(staffList.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN); const staffPrefix = await listUsers(staffToken, { accountPrefix: 'agov-page', }); expect(staffPrefix.status).toBe(200); // 前缀模式语义不变:仅激活资格用户、account 升序。 expect( (staffPrefix.body.items as { account: string }[]).map((i) => i.account), ).toEqual(PAGE_ACCOUNTS); const anon = await listUsers(null, {}); expect(anon.status).toBe(401); expect(anon.body.code).toBe(ErrorCodes.AUTH_TOKEN_EXPIRED); }); it('⑤ PATCH:role 往返 + displayName;audit user.update 记变化字段;幂等 no-op 不重复审计', async () => { const up = await patchUser(adminToken, patchTargetId, { role: 'admin' }); expect(up.status).toBe(200); expect(up.body).toEqual( expect.objectContaining({ id: patchTargetId, role: 'admin' }), ); // AdminUser 全字段形状(同 users-lookup spec ①,此处守护治理端点回显)。 expect(Object.keys(up.body).sort()).toEqual([ 'account', 'createdAt', 'credentialStatus', 'displayName', 'id', 'localLocked', 'locale', 'role', 'sourceStatus', 'synced', ]); const back = await patchUser(adminToken, patchTargetId, { role: 'staff' }); expect(back.status).toBe(200); expect(back.body.role).toBe('staff'); const renamed = await patchUser(adminToken, patchTargetId, { displayName: '新展示名', }); expect(renamed.status).toBe(200); expect(renamed.body.displayName).toBe('新展示名'); // 幂等 no-op:再提交同值 displayName → 200 但不重复落审计。 const again = await patchUser(adminToken, patchTargetId, { displayName: '新展示名', }); expect(again.status).toBe(200); expect(again.body.displayName).toBe('新展示名'); const audits = await prisma.audit_logs.findMany({ where: { action: 'user.update', resourceId: patchTargetId }, orderBy: { createdAt: 'asc' }, }); expect(audits).toHaveLength(3); // admin→staff? no:staff→admin、admin→staff、displayName expect(audits[0].actorId).toBe(adminId); expect(audits[0].metadata).toEqual({ changes: { role: { from: 'staff', to: 'admin' } }, }); expect(audits[1].metadata).toEqual({ changes: { role: { from: 'admin', to: 'staff' } }, }); expect(audits[2].metadata).toEqual({ changes: { displayName: { from: '.patch目标', to: '新展示名' } }, }); const dbRow = await prisma.users.findUnique({ where: { id: patchTargetId } }); expect(dbRow?.role).toBe('staff'); expect(dbRow?.displayName).toBe('新展示名'); }); it('⑤ PATCH localLocked:true → 目标会话全撤 + 登录 401;false → 原密码可再登录', async () => { const lock = await patchUser(adminToken, patchTargetId, { localLocked: true, }); expect(lock.status).toBe(200); expect(lock.body.localLocked).toBe(true); // 既有会话即刻失效(守卫资格拒)。 const probed = await probeToken(patchTargetToken); expect(probed.status).toBe(401); expect(probed.body.code).toBe(ErrorCodes.AUTH_TOKEN_EXPIRED); // 锁定期间正确密码也不可登录(资格不合格 → 同 401 INVALID_CREDENTIALS)。 const lockedLogin = await login('agov-patch', 'Patch-Old-123'); expect(lockedLogin.status).toBe(401); expect(lockedLogin.body.code).toBe(ErrorCodes.AUTH_INVALID_CREDENTIALS); const lockAudit = await prisma.audit_logs.findFirst({ where: { action: 'user.update', resourceId: patchTargetId }, orderBy: { createdAt: 'desc' }, }); expect(lockAudit?.metadata).toEqual({ changes: { localLocked: { from: false, to: true } }, }); // 解锁(契约:localLocked=false 仅解除本地锁定):原密码未动,可再登录。 const unlock = await patchUser(adminToken, patchTargetId, { localLocked: false, }); expect(unlock.status).toBe(200); expect(unlock.body.localLocked).toBe(false); const relogin = await login('agov-patch', 'Patch-Old-123'); expect(relogin.status).toBe(200); }); it('⑤ PATCH 入参边界:sourceStatus 只读 → 400;空体/role 非法值 → 400;幽灵用户 404', async () => { const src = await patchUser(adminToken, patchTargetId, { sourceStatus: 'disabled', } as object); expect(src.status).toBe(400); expect(src.body.code).toBe(ErrorCodes.VALIDATION_FAILED); const empty = await patchUser(adminToken, patchTargetId, {}); expect(empty.status).toBe(400); expect(empty.body.code).toBe(ErrorCodes.VALIDATION_FAILED); const badRole = await patchUser(adminToken, patchTargetId, { role: 'boss' }); expect(badRole.status).toBe(400); expect(badRole.body.code).toBe(ErrorCodes.VALIDATION_FAILED); const ghost = await patchUser(adminToken, `ghost-${Date.now()}`, { displayName: '谁', }); expect(ghost.status).toBe(404); expect(ghost.body.code).toBe(ErrorCodes.NOT_FOUND); }); it('⑤ PATCH 自我保护:admin 自改角色/自锁 → 400;自改 displayName → 200', async () => { const selfRole = await patchUser(adminToken, adminId, { role: 'staff' }); expect(selfRole.status).toBe(400); expect(selfRole.body.code).toBe(ErrorCodes.VALIDATION_FAILED); const selfLock = await patchUser(adminToken, adminId, { localLocked: true }); expect(selfLock.status).toBe(400); expect(selfLock.body.code).toBe(ErrorCodes.VALIDATION_FAILED); const selfName = await patchUser(adminToken, adminId, { displayName: '管理员改己名', }); expect(selfName.status).toBe(200); expect(selfName.body.displayName).toBe('管理员改己名'); }); it('⑥ reset-password 指定:旧密码 401/新密码 200;目标会话撤销;执行方不受影响;audit 无密码', async () => { const res = await resetPassword(adminToken, resetTargetId, { newPassword: 'Brand-New-456', }); expect(res.status).toBe(200); expect(res.body).toEqual({ newPassword: 'Brand-New-456' }); const oldLogin = await login('agov-reset', 'Reset-Old-123'); expect(oldLogin.status).toBe(401); expect(oldLogin.body.code).toBe(ErrorCodes.AUTH_INVALID_CREDENTIALS); const newLogin = await login('agov-reset', 'Brand-New-456'); expect(newLogin.status).toBe(200); expect(typeof newLogin.body.sessionToken).toBe('string'); // 目标既有会话全撤(新登录产生的会话不在其列——旧令牌作废)。 const probed = await probeToken(resetTargetToken); expect(probed.status).toBe(401); // 执行方 admin 自身会话不受影响(撤销只作用于目标用户)。 const adminStill = await listUsers(adminToken, { query: 'agov-reset' }); expect(adminStill.status).toBe(200); // 审计落账且绝不含密码明文(含请求指定值与任何可能的生成值形态)。 const audit = await prisma.audit_logs.findFirst({ where: { action: 'user.password_reset', resourceId: resetTargetId }, }); expect(audit).not.toBeNull(); expect(audit?.actorId).toBe(adminId); expect(JSON.stringify(audit?.metadata)).not.toContain('Brand-New-456'); expect(JSON.stringify(audit?.metadata)).not.toContain('password'); }); it('⑥ reset-password 生成:长度 ≥12、字母数字字符集、两次不同、可登录;长度边界 400', async () => { const res = await resetPassword(adminToken, resetTargetId); expect(res.status).toBe(200); const generated = res.body.newPassword as string; expect(generated.length).toBeGreaterThanOrEqual(12); expect(generated).toMatch(/^[A-Za-z0-9]+$/); const loginRes = await login('agov-reset', generated); expect(loginRes.status).toBe(200); // 随机性:再次生成的值不同(一次性凭据不可预测的最低限度证据)。 const res2 = await resetPassword(adminToken, resetTargetId, {}); expect(res2.status).toBe(200); expect(res2.body.newPassword).not.toBe(generated); for (const bad of ['short7!', 'x'.repeat(129)]) { const res3 = await resetPassword(adminToken, resetTargetId, { newPassword: bad, }); expect(res3.status).toBe(400); expect(res3.body.code).toBe(ErrorCodes.VALIDATION_FAILED); } }); it('⑦ activate:pending→active;重复激活幂等(不重复审计);组合流 activate→reset→登录 200', async () => { const first = await activate(adminToken, pendingTargetId); expect(first.status).toBe(200); expect(first.body).toEqual( expect.objectContaining({ id: pendingTargetId, credentialStatus: 'active', }), ); // 幂等:对 active 用户重复激活返回当前条目,不重复落审计。 const second = await activate(adminToken, pendingTargetId); expect(second.status).toBe(200); expect(second.body.credentialStatus).toBe('active'); const activateAudits = await prisma.audit_logs.findMany({ where: { action: 'user.activate', resourceId: pendingTargetId }, }); expect(activateAudits).toHaveLength(1); // §10 闭环:激活后经 reset-password 出初始凭据 → 用户可登录。 const reset = await resetPassword(adminToken, pendingTargetId, {}); expect(reset.status).toBe(200); const initial = reset.body.newPassword as string; expect(initial.length).toBeGreaterThanOrEqual(12); const loginRes = await login('agov-pend2', initial); expect(loginRes.status).toBe(200); }); it('⑧ deactivate:锁定 + 双会话撤销 + 登录 401;幂等无重复审计;PATCH 解锁后原密码可登录', async () => { const res = await deactivate(adminToken, deactivateTargetId); expect(res.status).toBe(200); expect(res.body).toEqual( expect.objectContaining({ id: deactivateTargetId, localLocked: true }), ); for (const token of [deactivateToken1, deactivateToken2]) { const probed = await probeToken(token); expect(probed.status).toBe(401); expect(probed.body.code).toBe(ErrorCodes.AUTH_TOKEN_EXPIRED); } const lockedLogin = await login('agov-deact', 'Deact-Old-123'); expect(lockedLogin.status).toBe(401); expect(lockedLogin.body.code).toBe(ErrorCodes.AUTH_INVALID_CREDENTIALS); // 幂等:重复停用 200,不重复落审计。 const again = await deactivate(adminToken, deactivateTargetId); expect(again.status).toBe(200); expect(again.body.localLocked).toBe(true); const audits = await prisma.audit_logs.findMany({ where: { action: 'user.deactivate', resourceId: deactivateTargetId }, }); expect(audits).toHaveLength(1); expect(audits[0].actorId).toBe(adminId); // 重新启用 = PATCH localLocked=false(activate 只管 credentialStatus); // 密码未动 → 原密码可再登录。 const unlock = await patchUser(adminToken, deactivateTargetId, { localLocked: false, }); expect(unlock.status).toBe(200); expect(unlock.body.localLocked).toBe(false); const relogin = await login('agov-deact', 'Deact-Old-123'); expect(relogin.status).toBe(200); }); it('⑨ 权限与目标校验:非 admin 全端点 403;幽灵用户 404', async () => { const ghost = `ghost-${Date.now()}`; const list = await listUsers(staffToken, { query: 'agov' }); expect(list.status).toBe(403); expect(list.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN); const patch = await patchUser(staffToken, patchTargetId, { displayName: '越权', }); expect(patch.status).toBe(403); expect(patch.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN); const reset = await resetPassword(staffToken, patchTargetId, { newPassword: 'Hacked-12345', }); expect(reset.status).toBe(403); expect(reset.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN); const act = await activate(staffToken, ghost); expect(act.status).toBe(403); expect(act.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN); const deact = await deactivate(staffToken, ghost); expect(deact.status).toBe(403); expect(deact.body.code).toBe(ErrorCodes.AUTH_FORBIDDEN); const resetGhost = await resetPassword(adminToken, ghost, { newPassword: 'Ghost-12345', }); expect(resetGhost.status).toBe(404); expect(resetGhost.body.code).toBe(ErrorCodes.NOT_FOUND); const actGhost = await activate(adminToken, ghost); expect(actGhost.status).toBe(404); expect(actGhost.body.code).toBe(ErrorCodes.NOT_FOUND); const deactGhost = await deactivate(adminToken, ghost); expect(deactGhost.status).toBe(404); expect(deactGhost.body.code).toBe(ErrorCodes.NOT_FOUND); }); });