// 已读取AGENTS.md import * as fs from 'fs'; import * as path from 'path'; import { ContentSecurityService } from '../src/core/security/content-security.service'; /** * 第三批高危项回归闸门:友圈动态越权删除 / 组局解散提权 / 微信内容安全真实对接 * 以「源码契约断言 + 服务行为断言」锁定安全边界,防止后续重构悄然后退。 */ 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 secServicePath = path.join(rootDir, 'server/src/core/security/content-security.service.ts'); const pwuniappApiPath = path.join(rootDir, 'pwuniapp/common/api.js'); describe('1. 友圈动态仅作者本人可删除', () => { it('deletePost 必须校验作者归属并拒绝越权删除', () => { const code = fs.readFileSync(serverRepoPath, 'utf-8'); expect(code).toContain('async deletePost('); expect(code).toContain('无权删除他人发布的动态'); expect(code).toContain('SELECT id, user_id FROM posts WHERE id = ? LIMIT 1'); expect(code).toContain('DELETE FROM posts WHERE id = ? AND user_id = ?'); }); it('删除路由必须要求显式身份,不得默认 1001', () => { const code = fs.readFileSync(serverIndexPath, 'utf-8'); const start = code.indexOf("app.delete('/api/posts/:id'"); expect(start).toBeGreaterThan(-1); const block = code.slice(start, start + 700); expect(block).toContain("req.query.userId || req.body?.userId || req.headers['x-user-id'] || 0"); expect(block).toContain('await assertUserNotBanned(uid'); expect(block).not.toContain('|| 1001)'); }); it('小程序删除动态必须携带真实登录用户ID', () => { const code = fs.readFileSync(pwuniappApiPath, 'utf-8'); const start = code.indexOf('export async function deletePost('); expect(start).toBeGreaterThan(-1); const block = code.slice(start, start + 400); expect(block).toContain('normalizeUserId(userId)'); expect(block).not.toContain('userId = 1001'); }); }); describe('2. 组局解散的管理员特权必须来自服务端签名 token', () => { it('cancel 路由严禁信任客户端 operatorRole / x-user-role 提权', () => { const code = fs.readFileSync(serverIndexPath, 'utf-8'); expect(code).toContain('function isVerifiedAdminRequest('); const start = code.indexOf("app.post('/api/activities/:id/cancel'"); expect(start).toBeGreaterThan(-1); const block = code.slice(start, start + 900); expect(block).toContain('isVerifiedAdminRequest(req)'); expect(block).not.toContain("operatorRole === 'admin'"); expect(block).not.toContain("req.headers['x-user-role']"); }); }); describe('3. 内容安全真实对接微信 msgSecCheck', () => { it('服务必须提供可注入的远端真实审核器与异步检测方法', () => { const code = fs.readFileSync(secServicePath, 'utf-8'); expect(code).toContain('setRemoteChecker'); expect(code).toContain('checkTextAsync'); expect(code).toContain('RemoteTextChecker'); }); it('index.ts 必须注册微信 msg_sec_check 真实调用并接入关键 UGC 路由', () => { const code = fs.readFileSync(serverIndexPath, 'utf-8'); expect(code).toContain('contentSecurityService.setRemoteChecker'); expect(code).toContain('https://api.weixin.qq.com/wxa/msg_sec_check'); expect(code).toContain('version: 2'); expect(code).toContain('await contentSecurityService.checkTextAsync('); }); it('checkTextAsync 在无远端审核器时降级本地词库:命中拦截、合规放行', async () => { const svc = new ContentSecurityService(); const hit = await svc.checkTextAsync('同城美女上门特殊服务'); expect(hit.pass).toBe(false); expect(hit.code).toBe(87014); expect(hit.suggest).toBe('block'); const ok = await svc.checkTextAsync('今晚一起打王者荣耀五排,辅助位缺一'); expect(ok.pass).toBe(true); expect(ok.code).toBe(0); }); it('注入的远端审核器会被调用,risky 内容应被二次拦截', async () => { const svc = new ContentSecurityService(); let called = 0; svc.setRemoteChecker(async (text) => { called += 1; if (text.includes('远端拦截')) { return { pass: false, code: 87014, label: 'WECHAT_RISKY', suggest: 'block' as const, message: '内容涉嫌违规,已被微信内容安全系统拦截' }; } return null; }); expect(svc.hasRemoteChecker()).toBe(true); const risky = await svc.checkTextAsync('这条内容需要远端拦截处理'); expect(called).toBe(1); expect(risky.pass).toBe(false); expect(risky.label).toBe('WECHAT_RISKY'); const normal = await svc.checkTextAsync('正常合规文本内容'); expect(normal.pass).toBe(true); }); it('远端审核器抛异常时必须 fail-open 降级本地,避免误杀正常内容', async () => { const svc = new ContentSecurityService(); svc.setRemoteChecker(async () => { throw new Error('wechat api down'); }); const r = await svc.checkTextAsync('今晚一起打王者荣耀五排'); expect(r.pass).toBe(true); }); }); });