412 行
20 KiB
TypeScript
412 行
20 KiB
TypeScript
// 已读取AGENTS.md
|
||
import { ContentSecurityService } from '../src/core/security/content-security.service';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安全与合规体验套件)', () => {
|
||
let secService: ContentSecurityService;
|
||
const rootDir = path.resolve(__dirname, '../../');
|
||
|
||
beforeEach(() => {
|
||
secService = new ContentSecurityService();
|
||
});
|
||
|
||
describe('1. 文本内容安全过滤规则 (security.msgSecCheck)', () => {
|
||
it('正常合规文本应安全放行', () => {
|
||
const texts = [
|
||
'今晚三里屯吃铜锅涮肉,有一起的搭子吗?人均AA',
|
||
'工体纯K量贩KTV麦霸欢唱局,准时到场',
|
||
'王者荣耀荣耀王者五排车队,辅助位缺一'
|
||
];
|
||
for (const t of texts) {
|
||
const res = secService.checkText(t);
|
||
expect(res.pass).toBe(true);
|
||
expect(res.code).toBe(0);
|
||
expect(res.suggest).toBe('pass');
|
||
}
|
||
});
|
||
|
||
it('涉黄低俗敏感词应被严格拦截 (code 87014)', () => {
|
||
const badTexts = [
|
||
'同城美女上门特殊服务,随时可约',
|
||
'招嫖包夜加微联系',
|
||
'同城一夜情约炮'
|
||
];
|
||
for (const t of badTexts) {
|
||
const res = secService.checkText(t);
|
||
expect(res.pass).toBe(false);
|
||
expect(res.code).toBe(87014);
|
||
expect(res.suggest).toBe('block');
|
||
expect(res.message).toContain('涉嫌违规');
|
||
}
|
||
});
|
||
|
||
it('黑产欺诈与引流私下交易词汇应被阻断', () => {
|
||
const scamTexts = [
|
||
'加微信私下转账免手续费直接打款',
|
||
'刷单兼职日赚千元'
|
||
];
|
||
for (const t of scamTexts) {
|
||
const res = secService.checkText(t);
|
||
expect(res.pass).toBe(false);
|
||
expect(res.code).toBe(87014);
|
||
}
|
||
});
|
||
|
||
it('maskText 能够正确对违规敏感词执行脱敏掩码', () => {
|
||
const masked = secService.maskText('严禁上门特殊服务违规行为');
|
||
expect(masked).toContain('******');
|
||
expect(masked).not.toContain('上门特殊服务');
|
||
});
|
||
});
|
||
|
||
describe('2. 多媒体图片内容安全校验 (security.mediaCheckAsync)', () => {
|
||
it('合规格式与尺寸的图片应顺利通过校验', () => {
|
||
const check = secService.checkImage('avatar_user_1.png', 'image/png', 500 * 1024);
|
||
expect(check.pass).toBe(true);
|
||
expect(check.code).toBe(0);
|
||
});
|
||
|
||
it('非图片或伪装可执行脚本应被即时拦截', () => {
|
||
const badCases = [
|
||
{ file: 'virus.exe', mime: 'application/octet-stream', size: 1024 },
|
||
{ file: 'shell.php', mime: 'text/plain', size: 1024 },
|
||
{ file: 'pic.jpg.sh', mime: 'image/jpeg', size: 1024 }
|
||
];
|
||
for (const c of badCases) {
|
||
const res = secService.checkImage(c.file, c.mime, c.size);
|
||
expect(res.pass).toBe(false);
|
||
expect(res.code).toBe(87014);
|
||
}
|
||
});
|
||
|
||
it('超大图片体积(超过10MB)应被安全拒绝', () => {
|
||
const res = secService.checkImage('huge.png', 'image/png', 12 * 1024 * 1024);
|
||
expect(res.pass).toBe(false);
|
||
expect(res.message).toContain('10MB');
|
||
});
|
||
});
|
||
|
||
describe('3. 前端微信隐私保护指引弹窗组件规范性验证 (PrivacyModal.vue)', () => {
|
||
const modalPath = path.join(rootDir, 'pwuniapp', 'components', 'PrivacyModal.vue');
|
||
|
||
it('PrivacyModal.vue 文件必须存在且格式有效', () => {
|
||
expect(fs.existsSync(modalPath)).toBe(true);
|
||
const content = fs.readFileSync(modalPath, 'utf-8');
|
||
expect(content).toContain('privacy-dialog');
|
||
expect(content).toContain('用户隐私保护指引');
|
||
});
|
||
|
||
it('必须支持微信小程序官方 agreePrivacyAuthorization 原生授权按钮', () => {
|
||
const content = fs.readFileSync(modalPath, 'utf-8');
|
||
expect(content).toContain('open-type="agreePrivacyAuthorization"');
|
||
expect(content).toContain('wx.onNeedPrivacyAuthorization');
|
||
});
|
||
|
||
it('必须支持用户服务协议与隐私指引原生调用或弹窗', () => {
|
||
const content = fs.readFileSync(modalPath, 'utf-8');
|
||
expect(content).toContain('openPrivacyContract');
|
||
});
|
||
|
||
it('必须适配冷暖双主题与无 Emoji 门禁', () => {
|
||
const content = fs.readFileSync(modalPath, 'utf-8');
|
||
expect(content).toContain('theme-warm');
|
||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||
expect(emojiRegex.test(content)).toBe(false);
|
||
});
|
||
|
||
it('首页与个人中心必须集成并暴露用户隐私指引入口', () => {
|
||
const indexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'), 'utf-8');
|
||
const userContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'), 'utf-8');
|
||
expect(indexContent).toContain('privacy-modal');
|
||
expect(userContent).toContain('privacy-modal');
|
||
});
|
||
});
|
||
|
||
describe('4. 小程序页面架构配置与文件完整性门禁验证 (pages.json)', () => {
|
||
const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json');
|
||
|
||
it('pages.json 注册的所有页面必须在源码目录物理存在,杜绝模拟器启动丢失 wxml 崩溃', () => {
|
||
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||
const config = JSON.parse(raw);
|
||
expect(Array.isArray(config.pages)).toBe(true);
|
||
expect(config.pages.length).toBe(13);
|
||
|
||
for (const pageItem of config.pages) {
|
||
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
|
||
expect({
|
||
path: pageItem.path,
|
||
exists: fs.existsSync(vueFilePath)
|
||
}).toEqual({
|
||
path: pageItem.path,
|
||
exists: true
|
||
});
|
||
}
|
||
});
|
||
|
||
it('主包核心 TabBar 页面保持精炼且四大主页全部就绪', () => {
|
||
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||
const config = JSON.parse(raw);
|
||
const tabPaths = config.tabBar.list.map((t: any) => t.pagePath);
|
||
expect(tabPaths).toEqual([
|
||
'pages/index/index',
|
||
'pages/activity/list',
|
||
'pages/order/list',
|
||
'pages/user/index'
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('5. 动态自定义引流违规词库与首页运营全局弹窗组件规范验证', () => {
|
||
it('支持动态加载自定义违规引流词并即时热拦截', () => {
|
||
secService.setCustomKeywords(['私下走微信', '线下给现金', '联系方式v']);
|
||
expect(secService.getCustomKeywords()).toEqual(['私下走微信', '线下给现金', '联系方式v']);
|
||
|
||
const blocked1 = secService.checkText('加个好友,我们私下走微信吧');
|
||
expect(blocked1.pass).toBe(false);
|
||
expect(blocked1.code).toBe(87014);
|
||
expect(blocked1.message).toContain('私下走微信');
|
||
|
||
const blocked2 = secService.checkText('活动到了现场线下给现金可以吗');
|
||
expect(blocked2.pass).toBe(false);
|
||
expect(blocked2.code).toBe(87014);
|
||
expect(blocked2.message).toContain('线下给现金');
|
||
|
||
const passText = secService.checkText('今晚一起去望京合生麒麟社玩桌游');
|
||
expect(passText.pass).toBe(true);
|
||
});
|
||
|
||
it('首页必须挂载 HomePopupModal 全局运营浮层弹窗组件', () => {
|
||
const homePopupPath = path.join(rootDir, 'pwuniapp', 'components', 'HomePopupModal.vue');
|
||
expect(fs.existsSync(homePopupPath)).toBe(true);
|
||
|
||
const indexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'index', 'index.vue'), 'utf-8');
|
||
expect(indexContent).toContain('home-popup-modal');
|
||
expect(indexContent).toContain('HomePopupModal');
|
||
});
|
||
});
|
||
|
||
describe('6. 大神技能标签库与定价区间规范配置校验 (expert_rules)', () => {
|
||
it('MySQL 数据库与种子数据必须注册 expert_rules 配置项且字段完整', () => {
|
||
const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts');
|
||
const content = fs.readFileSync(mysqlPath, 'utf-8');
|
||
expect(content).toContain('expert_rules');
|
||
expect(content).toContain('minHourlyRate');
|
||
expect(content).toContain('maxHourlyRate');
|
||
expect(content).toContain('candidateTags');
|
||
});
|
||
|
||
it('管理后台 platform-config 必须包含 Tab 8 大神资质与定价规则可视化面板', () => {
|
||
const adminConfigPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'platform-config', 'index.vue');
|
||
const content = fs.readFileSync(adminConfigPath, 'utf-8');
|
||
expect(content).toContain('expert_rules_config');
|
||
expect(content).toContain('大神资质与定价规则');
|
||
expect(content).toContain('expertRulesForm');
|
||
expect(content).toContain('expertCandidateTags');
|
||
});
|
||
|
||
it('服务端路由必须提供 PUT /api/companions/:id 接口并强校验价格上下限与敏感标签', () => {
|
||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||
const content = fs.readFileSync(serverIndexPath, 'utf-8');
|
||
expect(content).toContain("app.put('/api/companions/:id'");
|
||
expect(content).toContain('minHourlyRate');
|
||
expect(content).toContain('maxHourlyRate');
|
||
expect(content).toContain('contentSecurityService.checkText');
|
||
});
|
||
|
||
it('测试套件与新增代码严禁任何 Emoji 违规', () => {
|
||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||
const content = fs.readFileSync(serverIndexPath, 'utf-8');
|
||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||
expect(emojiRegex.test(content)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('7. 财务分销规则、组局预算规范与大神入驻申请闭环验证 (finance_rules & activity_rules)', () => {
|
||
it('MySQL 数据库与种子数据必须注册 finance_rules 与 activity_rules 配置项', () => {
|
||
const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts');
|
||
const content = fs.readFileSync(mysqlPath, 'utf-8');
|
||
expect(content).toContain('finance_rules');
|
||
expect(content).toContain('platformRatePercent');
|
||
expect(content).toContain('firstReferralPercent');
|
||
expect(content).toContain('activity_rules');
|
||
expect(content).toContain('minParticipants');
|
||
expect(content).toContain('presetPerPersonFees');
|
||
});
|
||
|
||
it('管理后台 platform-config 必须包含 Tab 9 财务规则与 Tab 10 组局预算面板', () => {
|
||
const adminConfigPath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'safety', 'platform-config', 'index.vue');
|
||
const content = fs.readFileSync(adminConfigPath, 'utf-8');
|
||
expect(content).toContain('finance_rules_config');
|
||
expect(content).toContain('财务抽成与分销规则');
|
||
expect(content).toContain('activity_rules_config');
|
||
expect(content).toContain('组局发单与预算规范');
|
||
expect(content).toContain('financeRulesForm');
|
||
expect(content).toContain('activityRulesForm');
|
||
});
|
||
|
||
it('服务端路由必须提供 POST /api/companions/apply 真实申请接口且校验姓名/身份证/安全', () => {
|
||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||
const content = fs.readFileSync(serverIndexPath, 'utf-8');
|
||
expect(content).toContain("app.post('/api/companions/apply'");
|
||
expect(content).toContain('真实姓名不能为空');
|
||
expect(content).toContain('身份证号不能为空');
|
||
});
|
||
|
||
it('小程序个人中心与发起组局页面必须动态绑定配置并实现门槛保护', () => {
|
||
const userIndexContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue'), 'utf-8');
|
||
const createActContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'), 'utf-8');
|
||
expect(userIndexContent).toContain('applyExpertCert');
|
||
expect(userIndexContent).toContain('showExpertCertModal');
|
||
expect(userIndexContent).toContain('finance_rules');
|
||
expect(createActContent).toContain('presetPerPersonFees');
|
||
expect(createActContent).toContain('activity_rules');
|
||
});
|
||
});
|
||
|
||
describe('8. 订单结算体系与前端交互人性化专项验证 (支付方式二选一、动态优惠券、组局快捷时段与商圈选点)', () => {
|
||
it('MySQL Schema 必须包含 pay_type 与 coupon_discount 字段升级补丁', () => {
|
||
const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts');
|
||
const content = fs.readFileSync(mysqlPath, 'utf-8');
|
||
expect(content).toContain('ALTER TABLE orders ADD COLUMN pay_type');
|
||
expect(content).toContain('ALTER TABLE orders ADD COLUMN coupon_discount');
|
||
});
|
||
|
||
it('Repository createOrder 必须支持余额支付原子事务校验、扣减余额及财务流水记录', () => {
|
||
const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts');
|
||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||
expect(content).toContain("data.payType === 'BALANCE'");
|
||
expect(content).toContain('SELECT balance FROM users WHERE id = ? FOR UPDATE');
|
||
expect(content).toContain('const newBalance = userBalance - amount');
|
||
expect(content).toContain('UPDATE users SET balance = ? WHERE id = ?');
|
||
expect(content).toContain('钱包余额支付订单');
|
||
expect(content).toContain("INSERT INTO finance_records (id, record_no, type, amount, direction, counterparty, related_id)");
|
||
});
|
||
|
||
it('小程序订单创建页面必须支持钱包余额与微信支付二选一并动态拉取营销优惠券', () => {
|
||
const orderCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'order', 'create.vue');
|
||
const content = fs.readFileSync(orderCreatePath, 'utf-8');
|
||
expect(content).toContain('payType');
|
||
expect(content).toContain("'BALANCE'");
|
||
expect(content).toContain("'WECHAT'");
|
||
expect(content).toContain('getMarketingCoupons');
|
||
expect(content).toContain('couponDiscount');
|
||
});
|
||
|
||
it('小程序活动发起页面必须包含快捷时段胶囊与示范商圈及地图选点交互', () => {
|
||
const actCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue');
|
||
const content = fs.readFileSync(actCreatePath, 'utf-8');
|
||
expect(content).toContain('presetTimeOptions');
|
||
expect(content).toContain('recommendPublicZones');
|
||
expect(content).toContain('handleChooseLocation');
|
||
expect(content).toContain('uni.chooseLocation');
|
||
});
|
||
|
||
it('所有新增代码与测试套件严禁包含任何 Emoji 字符', () => {
|
||
const pathsToCheck = [
|
||
path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'),
|
||
path.join(rootDir, 'server', 'src', 'db', 'repository.ts'),
|
||
path.join(rootDir, 'pwuniapp', 'pages', 'order', 'create.vue'),
|
||
path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue'),
|
||
path.join(rootDir, 'pwuniapp', 'common', 'icons.js'),
|
||
path.join(rootDir, 'pwuniapp', 'common', 'api.js')
|
||
];
|
||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||
for (const p of pathsToCheck) {
|
||
const text = fs.readFileSync(p, 'utf-8');
|
||
expect(emojiRegex.test(text)).toBe(false);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('9. 组局满员前「搭子自主退席与全额退款」与席位释放闭环验证 (P4 体验优化专项目标)', () => {
|
||
it('MySQL 数据库必须支持 finance_records 表的 user_id 字段补丁', () => {
|
||
const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts');
|
||
const content = fs.readFileSync(mysqlPath, 'utf-8');
|
||
expect(content).toContain('ALTER TABLE finance_records ADD COLUMN user_id');
|
||
});
|
||
|
||
it('Repository leaveActivity 必须支持原路退款至用户钱包并记入资金流水', () => {
|
||
const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts');
|
||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||
expect(content).toContain('async leaveActivity(id: string, userId: number)');
|
||
expect(content).toContain('DELETE FROM participants WHERE id = ?');
|
||
expect(content).toContain('UPDATE users SET balance = balance + ? WHERE id = ?');
|
||
expect(content).toContain('成员退席原路退款');
|
||
});
|
||
|
||
it('服务端路由必须提供 POST /api/activities/:id/leave 接口', () => {
|
||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||
const content = fs.readFileSync(serverIndexPath, 'utf-8');
|
||
expect(content).toContain("app.post('/api/activities/:id/leave'");
|
||
expect(content).toContain('leaveActivity');
|
||
});
|
||
|
||
it('小程序活动详情页必须支持普通搭子申请退席全额退款与本人席位标识', () => {
|
||
const actDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue');
|
||
const content = fs.readFileSync(actDetailPath, 'utf-8');
|
||
expect(content).toContain('leaveActivity');
|
||
expect(content).toContain('handleLeave');
|
||
expect(content).toContain('申请退席 (全额退款)');
|
||
expect(content).toContain('my-badge');
|
||
expect(content).toContain('我的席位');
|
||
});
|
||
|
||
it('小程序门票页必须支持未核销门票自助申请退票退款', () => {
|
||
const myTicketPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue');
|
||
const content = fs.readFileSync(myTicketPath, 'utf-8');
|
||
expect(content).toContain('leaveActivity');
|
||
expect(content).toContain('handleCancelTicket');
|
||
expect(content).toContain('申请退票退款 (释放席位)');
|
||
});
|
||
|
||
it('所有 P4 新增代码严禁包含任何 Emoji 违规字符', () => {
|
||
const p4Files = [
|
||
path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'),
|
||
path.join(rootDir, 'server', 'src', 'db', 'repository.ts'),
|
||
path.join(rootDir, 'server', 'src', 'index.ts'),
|
||
path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue'),
|
||
path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue')
|
||
];
|
||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||
for (const f of p4Files) {
|
||
const text = fs.readFileSync(f, 'utf-8');
|
||
expect(emojiRegex.test(text)).toBe(false);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('10. 个人中心未登录游客态、退出登录及切换账号门禁验证 (用户体验闭环与安全防范)', () => {
|
||
it('个人中心页面必须包含退出登录、切换账号与登录弹窗入口', () => {
|
||
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
|
||
const content = fs.readFileSync(userIndexPath, 'utf-8');
|
||
expect(content).toContain('handleLogout');
|
||
expect(content).toContain('switchDemoAccount');
|
||
expect(content).toContain('openLoginModal');
|
||
expect(content).toContain('isLoggedIn');
|
||
expect(content).toContain('account-actions-card');
|
||
expect(content).toContain('login-modal-mask');
|
||
expect(content).toContain('退出当前登录');
|
||
expect(content).toContain('切换账号');
|
||
expect(content).toContain('一键授权快捷登录');
|
||
expect(content).toContain('open-type="getPhoneNumber"');
|
||
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
|
||
});
|
||
|
||
it('个人中心核心交互必须包含未登录拦截守护 ensureLoggedIn', () => {
|
||
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
|
||
const content = fs.readFileSync(userIndexPath, 'utf-8');
|
||
expect(content).toContain('const ensureLoggedIn = () =>');
|
||
expect(content).toContain('if (!ensureLoggedIn()) return;');
|
||
});
|
||
|
||
it('所有个人中心改造代码严禁包含任何系统 Emoji', () => {
|
||
const userIndexPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
|
||
const text = fs.readFileSync(userIndexPath, 'utf-8');
|
||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||
expect(emojiRegex.test(text)).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|