// 已读取AGENTS.md import * as fs from 'fs'; import * as path from 'path'; import { signUserToken, verifyUserToken } from '../src/core/security/user-token'; /** * 第四批高危项回归闸门:客户端伪造身份的资金写接口收口 / 订单支付状态机收口 / 静默吞错可观测化 * 以「源码契约断言 + 行为断言」锁定边界,防止后续重构悄然后退。 */ describe('第四批安全加固闸门 (用户会话身份 / 订单支付 / 上车申请越权 / 静默吞错)', () => { const rootDir = path.resolve(__dirname, '../../'); const serverRepoPath = path.join(rootDir, 'server/src/db/repository.ts'); const serverIndexPath = path.join(rootDir, 'server/src/index.ts'); const userTokenPath = path.join(rootDir, 'server/src/core/security/user-token.ts'); const requestPath = path.join(rootDir, 'pwuniapp/common/request.js'); const loginPath = path.join(rootDir, 'pwuniapp/pages/auth/login.vue'); const indexCode = fs.readFileSync(serverIndexPath, 'utf-8'); const repoCode = fs.readFileSync(serverRepoPath, 'utf-8'); const tokenCode = fs.readFileSync(userTokenPath, 'utf-8'); describe('1. 用户会话 token 基础设施 (HMAC 签名,防止伪造他人 userId)', () => { it('必须提供签名/验签与安全取用用户身份的能力', () => { expect(indexCode).toContain('const USER_TOKEN_SECRET'); expect(indexCode).toContain('function signUserToken('); expect(indexCode).toContain('function verifyUserToken('); expect(indexCode).toContain('function resolveSecureUserId('); expect(indexCode).toContain('function resolveRequestUserId('); }); it('token 验签必须使用恒定时间比较且校验有效期', () => { expect(tokenCode).toContain('crypto.timingSafeEqual'); expect(tokenCode).toContain('ttlMs'); expect(indexCode).toContain('USER_TOKEN_TTL_MS'); }); it('签名 token 可被正确验签并解析出用户ID', () => { const SECRET = 'unit-test-secret'; const TTL = 7 * 24 * 3600 * 1000; const token = signUserToken(8888, SECRET, TTL); expect(verifyUserToken(token, SECRET, TTL)).toBe(8888); }); it('篡改 payload/签名或使用错误密钥的 token 必须验签失败', () => { const SECRET = 'unit-test-secret'; const TTL = 7 * 24 * 3600 * 1000; const token = signUserToken(1001, SECRET, TTL); const [payload, sig] = token.split('.'); const forgedPayload = Buffer.from(JSON.stringify({ uid: 1, ts: Date.now() })).toString('base64url'); expect(verifyUserToken(`${forgedPayload}.${sig}`, SECRET, TTL)).toBeNull(); expect(verifyUserToken(`${payload}.deadbeef`, SECRET, TTL)).toBeNull(); expect(verifyUserToken(token, 'wrong-secret', TTL)).toBeNull(); expect(verifyUserToken('not-a-token', SECRET, TTL)).toBeNull(); }); it('过期 token 必须验签失败', () => { const SECRET = 'unit-test-secret'; const TTL = 1000; const issuedAt = Date.now() - 10_000; const token = signUserToken(2002, SECRET, TTL, issuedAt); expect(verifyUserToken(token, SECRET, TTL, issuedAt + 500)).toBe(2002); expect(verifyUserToken(token, SECRET, TTL, issuedAt + 10_000)).toBeNull(); }); it('生产环境缺失有效 token 时必须拒绝敏感操作 (fail closed)', () => { const start = indexCode.indexOf('function resolveSecureUserId('); expect(start).toBeGreaterThan(-1); const block = indexCode.slice(start, start + 900); expect(block).toContain('IS_PRODUCTION'); expect(block).toContain('401'); expect(block).toContain('无权以他人身份'); }); it('微信手机号登录必须签发真实签名 token,杜绝假 token', () => { expect(indexCode).toContain('token: signUserToken(user.id)'); expect(indexCode).toContain('userToken: signUserToken(user.id)'); expect(indexCode).not.toContain('token: `token_${user.id}_${Date.now()}`'); }); }); describe('2. 订单支付状态机收口', () => { it('非 LIVE 沙箱入账必须受 ALLOW_SANDBOX_PAY 生产闸门约束', () => { const start = indexCode.indexOf("app.post('/api/orders/:id/pay'"); expect(start).toBeGreaterThan(-1); const block = indexCode.slice(start, start + 3400); expect(block).toContain('if (!ALLOW_SANDBOX_PAY)'); expect(block).toContain('禁止直接变更订单支付状态'); }); it('沙箱仅允许推进到合法已支付状态,严禁客户端任意指定终态', () => { const start = indexCode.indexOf("app.post('/api/orders/:id/pay'"); const block = indexCode.slice(start, start + 3400); expect(block).toContain("const allowedSandboxStatuses = ['履约中', '待接单']"); expect(block).toContain('allowedSandboxStatuses.includes(requested)'); }); }); describe('3. 提现接口以服务端会话身份为准', () => { it('提现用户身份必须经 resolveSecureUserId 校验,禁止直接信任路径参数', () => { const start = indexCode.indexOf("app.post('/api/users/:id/withdraw'"); expect(start).toBeGreaterThan(-1); const block = indexCode.slice(start, start + 900); expect(block).toContain("resolveSecureUserId(req, pathUserId, '申请提现')"); expect(block).not.toContain('const userId = Number(req.params.id);'); }); }); describe('4. 上车申请仅动态发布者本人可处置', () => { it('处置路由必须校验申请归属并区分管理员', () => { const start = indexCode.indexOf("app.post('/api/post-applies/:id/handle'"); expect(start).toBeGreaterThan(-1); const block = indexCode.slice(start, start + 1500); expect(block).toContain('repo.getPostApplyById(applyId)'); expect(block).toContain('isVerifiedAdminRequest(req)'); expect(block).toContain('仅动态发布者本人可处置该上车申请'); expect(block).toContain('SELECT user_id FROM posts WHERE id = ?'); }); it('handlePostApply 必须仅允许待处理申请被处置一次', () => { const start = repoCode.indexOf('async handlePostApply('); expect(start).toBeGreaterThan(-1); const block = repoCode.slice(start, start + 900); expect(block).toContain("WHERE id = ? AND status = 'PENDING'"); expect(block).toContain('已处置'); }); }); describe('5. 资金/结算静默吞错可观测化', () => { it('服务端必须提供统一错误日志并用于支付与结算关键路径', () => { expect(indexCode).toContain('function logServerError('); expect(indexCode).toContain("logServerError('confirmWechatPayment.query'"); expect(indexCode).toContain("logServerError('resolveWechatPayRuntimeConfig'"); expect(repoCode).toContain('function logServerError('); expect(repoCode).toContain("logServerError('handleArbitration.settle'"); }); }); describe('6. 小程序端会话 token 存储与透传', () => { it('统一请求层必须携带 Authorization Bearer 用户会话 token', () => { const code = fs.readFileSync(requestPath, 'utf-8'); expect(code).toContain("uni.getStorageSync('userToken')"); expect(code).toContain('Authorization: `Bearer ${sessionToken}`'); }); it('微信手机号登录成功后必须落地 userToken', () => { const code = fs.readFileSync(loginPath, 'utf-8'); expect(code).toContain("res.userToken || res.token"); expect(code).toContain("uni.setStorageSync('userToken', sessionToken)"); }); }); });