feat(security): 三端闭环加固 + 微信提现打款/订阅消息落地
后端: 身份与归属鉴权收口; 资金安全(结算闸门/支付防篡改/报名费真实扣款/赠金/结算唯一入账/提现打款); 数据泄露与隐私; 登录与密钥安全(默认密钥闸门/登录限流); 可观测性; 启动自检. 微信: 商家转账到零钱提现打款闭环(含回调/查单/失败退回佣金); 订阅消息真实下发闭环与后台模板字段对齐. 后台: 财务/微信支付配置读取失败锁定; 看板/待办/走势去假数据; 打款与测试推送文案修正. 小程序: 请求层错误透传与登录过期提示; 多页失败反馈与乐观回滚; 帮助中心FAQ页面化. 测试: 新增 40+ 回归护栏与真实 MySQL 冒烟(75 套件/599 用例全绿).
这个提交包含在:
@@ -0,0 +1,31 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十六批回归闸门:组局创建参数服务端校验
|
||||
*/
|
||||
describe('第二十六批组局创建校验闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
const block = () => {
|
||||
const start = repoCode.indexOf('async createActivity(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return repoCode.slice(start, start + 1600);
|
||||
};
|
||||
|
||||
it('付费类型/人数/费用必须服务端归一化', () => {
|
||||
const b = block();
|
||||
expect(b).toContain('const payType = Number(data.payType) === 2 ? 2 : 1;');
|
||||
expect(b).toContain('Math.min(100, Math.max(1, Math.round(rawMax)))');
|
||||
expect(b).toContain('MAX_FEE_CENTS');
|
||||
expect(b).toContain('const perPersonFeeCents = payType === 1 ? fee : 0;');
|
||||
});
|
||||
|
||||
it('不得再直接信任客户端未校验费用', () => {
|
||||
const b = block();
|
||||
expect(b).not.toContain('const perPersonFeeCents = data.perPersonFeeCents || 5000;');
|
||||
expect(b).not.toContain('const maxParticipants = data.maxParticipants || 4;');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十八批回归闸门:组局详情不得泄露他人核销码
|
||||
*/
|
||||
describe('第二十八批组局详情核销码隐私闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
it('活动详情路由必须剥离非本人核销码', () => {
|
||||
const start = indexCode.indexOf("app.get('/api/activities/:id',");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
expect(block).toContain('严禁向他人泄露');
|
||||
expect(block).toContain('Number(p.userId) === callerId');
|
||||
expect(block).toContain('const { verifyCode, ...rest } = p;');
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
});
|
||||
|
||||
it('小程序请求详情携带当前用户身份', () => {
|
||||
const start = apiCode.indexOf('export async function getActivityDetail(');
|
||||
const block = apiCode.slice(start, start + 300);
|
||||
expect(block).toContain('/activities/${id}?userId=${normalizeUserId()}');
|
||||
});
|
||||
|
||||
it('数据适配器不得为缺失核销码伪造 8392', () => {
|
||||
expect(apiCode).not.toContain("verifyCode: p.verifyCode || '8392'");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十一批回归闸门:组局发起/解散、抢单、搭子上车、用户设置的身份收口
|
||||
*/
|
||||
describe('第十一批写接口身份收口闸门 (组局 / 抢单 / 上车 / 设置)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 1200) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 组局发起人身份以服务端会话为准', () => {
|
||||
it('创建组局必须经 resolveSecureUserId 绑定发起人', () => {
|
||||
const block = routeBlock("app.post('/api/activities',");
|
||||
expect(block).toContain("'发起组局'");
|
||||
expect(block).not.toContain("req.body?.creatorId || req.headers['x-user-id'] || 1001");
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 组局解散人身份以服务端会话为准', () => {
|
||||
it('解散组局:管理员走签名 token,其余经 resolveSecureUserId', () => {
|
||||
const block = routeBlock("app.post('/api/activities/:id/cancel'");
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain("'解散组局'");
|
||||
expect(block).not.toContain("const callerId = Number(hostId ||");
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 抢单身份以服务端会话为准', () => {
|
||||
it('接单必须经 resolveSecureUserId 且写库用服务端身份', () => {
|
||||
const block = routeBlock("app.post('/api/orders/:id/claim'");
|
||||
expect(block).toContain("'接单'");
|
||||
expect(block).toContain('claimOrder(orderId, { ...(req.body || {}), expertId: operatorId })');
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 搭子上车申请身份以服务端会话为准', () => {
|
||||
it('上车申请必须经 resolveSecureUserId', () => {
|
||||
const block = routeBlock("app.post('/api/posts/:id/apply'");
|
||||
expect(block).toContain("'申请加入搭子车队'");
|
||||
expect(block).not.toContain("applicantId || req.headers['x-user-id'] || 1001");
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 用户设置读写归属', () => {
|
||||
it('设置读写必须经 resolveSecureUserId', () => {
|
||||
const getBlock = routeBlock("app.get('/api/users/:id/settings'");
|
||||
const postBlock = routeBlock("app.post('/api/users/:id/settings'");
|
||||
expect(getBlock).toContain("'查看个人设置'");
|
||||
expect(postBlock).toContain("'保存个人设置'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('6. 小程序端写接口携带真实身份', () => {
|
||||
it('组局/抢单/上车/报名/退席均注入当前用户身份', () => {
|
||||
expect(apiCode).toContain('creatorId: normalizeUserId(payload.creatorId)');
|
||||
expect(apiCode).toContain('hostId: normalizeUserId(hostId)');
|
||||
expect(apiCode).toContain('userId: normalizeUserId(params.userId)');
|
||||
expect(apiCode).toContain('userId: normalizeUserId(userId),');
|
||||
expect(apiCode).toContain('userId: normalizeUserId(expertData.userId)');
|
||||
expect(apiCode).toContain('applicantId: normalizeUserId(applyData.applicantId)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十四批回归闸门:组局报名费用服务端权威 + 真实扣款
|
||||
*/
|
||||
describe('第二十四批组局报名资金闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
const joinBlock = () => {
|
||||
const start = repoCode.indexOf('async joinActivity(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return repoCode.slice(start, start + 6200);
|
||||
};
|
||||
|
||||
it('报名费以服务端活动配置为准(AA=人均分摊,房主请客=0)', () => {
|
||||
const block = joinBlock();
|
||||
expect(block).toContain('per_person_fee_cents');
|
||||
expect(block).toContain('const joinFeeCents = Number(actRows[0].pay_type) === 1');
|
||||
});
|
||||
|
||||
it('报名从钱包真实扣款', () => {
|
||||
const block = joinBlock();
|
||||
expect(block).toContain("'活动报名扣款'");
|
||||
expect(block).toContain('UPDATE users SET balance = balance - ?');
|
||||
expect(block).toContain('钱包余额不足,报名需支付');
|
||||
});
|
||||
|
||||
it('入账/参与者金额均使用服务端权威值', () => {
|
||||
const block = joinBlock();
|
||||
expect(block).not.toContain('params.payAmountCents / 100');
|
||||
expect(block).not.toContain('params.userId, params.payAmountCents, verifyCode');
|
||||
expect(block).toContain('payAmountCents: joinFeeCents');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十五批回归闸门:房主改价的金额校验与差额记账
|
||||
*/
|
||||
describe('第二十五批改价完整性闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
const block = () => {
|
||||
const start = repoCode.indexOf('async adjustPrice(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return repoCode.slice(start, start + 1800);
|
||||
};
|
||||
|
||||
it('必须校验调价金额(非负整数 + 平台上限)', () => {
|
||||
const b = block();
|
||||
expect(b).toContain('Number.isInteger(price)');
|
||||
expect(b).toContain('MAX_FEE_CENTS');
|
||||
expect(b).toContain('调价金额必须为不小于 0 的整数');
|
||||
});
|
||||
|
||||
it('必须按真实差额记账而非整份新价', () => {
|
||||
const b = block();
|
||||
expect(b).toContain('const deltaCents = price - oldFee');
|
||||
expect(b).toContain("'房主追加赏金'");
|
||||
expect(b).toContain("'房主下调出价差额'");
|
||||
expect(b).not.toContain("'房主追加赏金差额退补'");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十六批回归闸门:提现打款场景信息可在后台自助配置
|
||||
*/
|
||||
describe('第五十六批打款场景信息配置闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const adminConfig = fs.readFileSync(
|
||||
path.join(rootDir, 'admin/arco-design-pro-vite/src/views/finance/config/index.vue'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
it('后台提供转账场景ID/备注/姓名加密/回调/场景信息配置项', () => {
|
||||
expect(adminConfig).toContain('transferSceneId');
|
||||
expect(adminConfig).toContain('transferRemark');
|
||||
expect(adminConfig).toContain('transferUserNameEnabled');
|
||||
expect(adminConfig).toContain('transferNotifyUrl');
|
||||
expect(adminConfig).toContain('transferSceneReportInfosText');
|
||||
expect(adminConfig).toContain('transfer_scene_report_infos');
|
||||
});
|
||||
|
||||
it('服务端持久化并透传场景信息', () => {
|
||||
// 保存接口白名单
|
||||
expect(indexCode).toContain('transferSceneReportInfos: Array.isArray(incoming.transferSceneReportInfos)');
|
||||
// 运行时配置解析透传
|
||||
const start = indexCode.indexOf('async function resolveWechatPayRuntimeConfig(');
|
||||
const block = indexCode.slice(start, start + 2600);
|
||||
expect(block).toContain('transferSceneReportInfos');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十七批回归闸门:管理端写接口鉴权(用户删除 / 场所管理 / 订单超时扫描)
|
||||
*/
|
||||
describe('第十七批管理端写接口鉴权闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('删除用户仅管理员可执行', () => {
|
||||
expect(indexCode).toContain("app.delete('/api/users/:id', requireAdmin");
|
||||
});
|
||||
|
||||
it('场所增删改仅管理员可执行', () => {
|
||||
expect(indexCode).toContain("app.post('/api/safety/places', requireAdmin");
|
||||
expect(indexCode).toContain("app.put('/api/safety/places/:id', requireAdmin");
|
||||
expect(indexCode).toContain("app.delete('/api/safety/places/:id', requireAdmin");
|
||||
});
|
||||
|
||||
it('场所列表仍保持公开可读(小程序合规场所库)', () => {
|
||||
expect(indexCode).toContain("app.get('/api/safety/places', async");
|
||||
});
|
||||
|
||||
it('订单超时自动流转扫描仅管理员可触发', () => {
|
||||
expect(indexCode).toContain("app.post('/api/orders/process-timeouts', requireAdmin");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十八批回归闸门:生产环境不得使用仓库内置默认 admin 密钥
|
||||
*/
|
||||
describe('第十八批鉴权密钥安全闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('不得再以仓库内置默认密钥作为静默回落', () => {
|
||||
expect(indexCode).not.toContain("process.env.ADMIN_TOKEN_SECRET || 'peiwan_admin_token_secret_v1'");
|
||||
});
|
||||
|
||||
it('必须存在默认密钥常量与生产环境随机化闸门', () => {
|
||||
expect(indexCode).toContain('const DEFAULT_ADMIN_TOKEN_SECRET =');
|
||||
const start = indexCode.indexOf('const DEFAULT_ADMIN_TOKEN_SECRET =');
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
expect(block).toContain("process.env.NODE_ENV === 'production'");
|
||||
expect(block).toContain('crypto.randomBytes');
|
||||
expect(block).toContain('SECURITY');
|
||||
});
|
||||
|
||||
it('管理员 token 校验必须使用常量时间比较', () => {
|
||||
expect(indexCode).toContain('crypto.timingSafeEqual');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十六批回归闸门:买家取消订单的状态安全
|
||||
*/
|
||||
describe('第三十六批买家取消订单闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('仓储层提供 buyerCancelOrder 且拒绝已履约订单', () => {
|
||||
const start = repoCode.indexOf('async buyerCancelOrder(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 2600);
|
||||
expect(block).toContain('不可直接取消,请发起退款申诉');
|
||||
expect(block).toContain("status = \"已退款\", refund_amount");
|
||||
expect(block).toContain("'买家取消订单退款'");
|
||||
});
|
||||
|
||||
it('取消路由:管理员直改状态,买家走安全取消', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/orders/:id/cancel'");
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
expect(block).toContain('role.isAdmin');
|
||||
expect(block).toContain('repo.buyerCancelOrder(orderId)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十三批回归闸门:code2session 不泄露 session_key 且 openid 绑定需会话一致
|
||||
*/
|
||||
describe('第三十三批 code2session 加固闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
const block = () => {
|
||||
const start = indexCode.indexOf("app.post('/api/wechat/code2session'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + 2600);
|
||||
};
|
||||
|
||||
it('不得向客户端返回 session_key', () => {
|
||||
const b = block();
|
||||
expect(b).not.toContain('sessionKey: session_key');
|
||||
expect(b).not.toContain('const { openid, session_key, unionid }');
|
||||
});
|
||||
|
||||
it('openid 绑定必须经服务端会话身份', () => {
|
||||
const b = block();
|
||||
expect(b).toContain("'绑定微信账户'");
|
||||
expect(b).toContain('repo.updateUserOpenid(claimed, openid)');
|
||||
expect(b).not.toContain('repo.updateUserOpenid(Number(userId), openid)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第四十八批回归闸门:大神入驻申请不得同名抢占他人档案
|
||||
*/
|
||||
describe('第四十八批大神入驻归属闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
const block = () => {
|
||||
const start = repoCode.indexOf('async applyCompanion(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return repoCode.slice(start, start + 3000);
|
||||
};
|
||||
|
||||
it('重新提交仅按 user_id 匹配本人档案', () => {
|
||||
const b = block();
|
||||
expect(b).toContain('WHERE user_id IS NOT NULL AND user_id = ?');
|
||||
expect(b).not.toContain('OR name = ?`, [userId, name]');
|
||||
expect(b).not.toContain('SET user_id = ?, name = ?');
|
||||
});
|
||||
|
||||
it('昵称与他人档案冲突时拒绝提交', () => {
|
||||
expect(block()).toContain('该大神昵称已被占用,请更换昵称后重新提交');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十一批回归闸门:大神详情公开端剥离内部审核备注
|
||||
*/
|
||||
describe('第三十一批大神详情隐私闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('公开详情剥离 rejectReason,管理端保留完整', () => {
|
||||
const start = indexCode.indexOf("app.get('/api/companions/:id',");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 800);
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('const { rejectReason, ...publicCompanion } = companion;');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十批回归闸门:大神公开目录仅展示已通过并脱敏内部审核备注
|
||||
*/
|
||||
describe('第三十批大神目录隐私闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('公开目录仅返回已通过大神且剥离 rejectReason,管理端保留全量', () => {
|
||||
const start = indexCode.indexOf("app.get('/api/companions',");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 900);
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain("c.auditStatus === '已通过'");
|
||||
expect(block).toContain('const { rejectReason, ...rest } = c;');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第四十六批回归闸门:内容安全检测接口鉴权与 openid 可信
|
||||
*/
|
||||
describe('第四十六批内容安全接口闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('生产需登录且 openid 以会话用户为准', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/security/msg-sec-check'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
expect(block).toContain('IS_PRODUCTION');
|
||||
expect(block).toContain("'内容安全检测'");
|
||||
expect(block).toContain('await resolveUserOpenid(callerId)');
|
||||
expect(block).not.toContain('const { content, openid, scene } = req.body');
|
||||
});
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
expect(Array.isArray(config.pages)).toBe(true);
|
||||
expect(config.pages.length).toBe(25);
|
||||
expect(config.pages.length).toBe(26);
|
||||
|
||||
for (const pageItem of config.pages) {
|
||||
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十三批回归闸门:分销团队/返佣明细不得用硬编码假数据
|
||||
*/
|
||||
describe('第五十三批分销数据真实性闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('不得再返回硬编码团队成员/返佣记录', () => {
|
||||
expect(repoCode).not.toContain("'晨曦微风'");
|
||||
expect(repoCode).not.toContain("'峡谷追风少年'");
|
||||
expect(repoCode).not.toContain("'dst_101'");
|
||||
expect(repoCode).not.toContain("'星空漫步者'");
|
||||
});
|
||||
|
||||
it('无真实数据时返回空', () => {
|
||||
const start = repoCode.indexOf('async getDistributionRecords(');
|
||||
const block = repoCode.slice(start, start + 900);
|
||||
expect(block).toContain('return [];');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十批回归闸门:大神收益统计不得跨大神串算
|
||||
*/
|
||||
describe('第五十批大神收益统计范围闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('不得再用 companion_name LIKE 匹配聚合作弊式占位名', () => {
|
||||
expect(repoCode).not.toContain("'%我%'");
|
||||
expect(repoCode).not.toContain('OR companion_name LIKE ?');
|
||||
});
|
||||
|
||||
it('收益汇总与走势只按 expert_id 统计', () => {
|
||||
const stats = repoCode.slice(repoCode.indexOf('async getExpertStats('), repoCode.indexOf('async getExpertStats(') + 600);
|
||||
expect(stats).toContain('WHERE expert_id = ?');
|
||||
const trends = repoCode.slice(repoCode.indexOf('async getExpertRevenueTrends('));
|
||||
expect(trends.slice(0, 4000)).toContain('WHERE expert_id = ?');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十八批回归闸门:帮助中心 FAQ 配置一致性
|
||||
*/
|
||||
describe('第五十八批帮助中心FAQ闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const mysqlCode = fs.readFileSync(path.join(rootDir, 'server/src/db/mysql.ts'), 'utf-8');
|
||||
const userIndex = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/index.vue'), 'utf-8');
|
||||
|
||||
it('faq_list 为空时自动回填默认种子', () => {
|
||||
expect(mysqlCode).toContain('自动回填默认种子');
|
||||
expect(mysqlCode).toContain("UPDATE platform_configs SET value_json = ? WHERE config_key = 'faq_list'");
|
||||
expect(mysqlCode).toContain('!Array.isArray(parsed) || parsed.length === 0');
|
||||
});
|
||||
|
||||
it('小程序不再用写死的假 FAQ 兜底文案(改为独立页面展示)', () => {
|
||||
expect(userIndex).not.toContain('什么是4位核销码?\\n到达活动现场');
|
||||
const faqPage = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/help-faq.vue'), 'utf-8');
|
||||
expect(faqPage).toContain('暂无常见问题');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十六批回归闸门:资金写接口(充值 / 订单支付)身份与归属收口
|
||||
*/
|
||||
describe('第十六批资金接口身份闸门 (充值 / 订单支付)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 1400) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 钱包充值不得为他人刷余额', () => {
|
||||
it('充值身份以服务端会话为准', () => {
|
||||
const block = routeBlock("app.post('/api/users/:id/recharge'");
|
||||
expect(block).toContain("'充值余额'");
|
||||
expect(block).not.toContain('const userId = Number(req.params.id);\n await assertUserNotBanned(userId, \'充值余额\')');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 订单支付仅限下单买家本人', () => {
|
||||
it('支付前必须做订单归属校验', () => {
|
||||
const block = routeBlock("app.post('/api/orders/:id/pay'");
|
||||
expect(block).toContain('repo.getOrderOwnership(orderId)');
|
||||
expect(block).toContain("'确认订单支付'");
|
||||
expect(block).toContain('仅下单买家本人可确认支付该订单');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 小程序支付携带当前用户身份', () => {
|
||||
it('payOrder 必须注入 userId', () => {
|
||||
const start = apiCode.indexOf('export async function payOrder(');
|
||||
const block = apiCode.slice(start, start + 300);
|
||||
expect(block).toContain('userId: normalizeUserId(payload.userId)');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十九批回归闸门:帮助中心 FAQ 改为独立页面(非弹窗)
|
||||
*/
|
||||
describe('第五十九批帮助中心页面化闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const faqPagePath = path.join(rootDir, 'pwuniapp/pages/user/help-faq.vue');
|
||||
const pagesJson = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages.json'), 'utf-8');
|
||||
const userIndex = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/index.vue'), 'utf-8');
|
||||
|
||||
it('新增独立 FAQ 页面并注册路由', () => {
|
||||
expect(fs.existsSync(faqPagePath)).toBe(true);
|
||||
const page = fs.readFileSync(faqPagePath, 'utf-8');
|
||||
expect(page).toContain('getPlatformConfigs');
|
||||
expect(page).toContain('faq_list');
|
||||
expect(page).toContain('activeIndex');
|
||||
expect(pagesJson).toContain('"path": "pages/user/help-faq"');
|
||||
});
|
||||
|
||||
it('个人中心点击进入新页面而非弹窗', () => {
|
||||
const start = userIndex.indexOf('const openFaq');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const end = userIndex.indexOf('};', start);
|
||||
const block = userIndex.slice(start, end + 2);
|
||||
expect(block).toContain("url: '/pages/user/help-faq'");
|
||||
expect(block).not.toContain('showActionSheet');
|
||||
expect(block).not.toContain('showModal');
|
||||
});
|
||||
});
|
||||
@@ -83,14 +83,14 @@ describe('第四批安全加固闸门 (用户会话身份 / 订单支付 / 上
|
||||
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 + 1400);
|
||||
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 + 1400);
|
||||
const block = indexCode.slice(start, start + 3400);
|
||||
expect(block).toContain("const allowedSandboxStatuses = ['履约中', '待接单']");
|
||||
expect(block).toContain('allowedSandboxStatuses.includes(requested)');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十二批回归闸门:管理端登录暴力破解防护 + 常量时间口令比较
|
||||
*/
|
||||
describe('第二十二批登录安全闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('提供常量时间比较与失败计数限流', () => {
|
||||
expect(indexCode).toContain('function safeStringEqual(');
|
||||
expect(indexCode).toContain('timingSafeEqual');
|
||||
expect(indexCode).toContain('loginFailureMap');
|
||||
expect(indexCode).toContain('LOGIN_MAX_FAILURES');
|
||||
expect(indexCode).toContain('isLoginLocked');
|
||||
});
|
||||
|
||||
it('登录路由:超限锁定 + 常量时间比较', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/user/login'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 1200);
|
||||
expect(block).toContain('isLoginLocked(loginKey)');
|
||||
expect(block).toContain('登录失败次数过多');
|
||||
expect(block).toContain('recordLoginFailure(loginKey)');
|
||||
expect(block).toContain('safeStringEqual(String(password), p)');
|
||||
expect(block).not.toContain('ADMIN_PASSWORDS.includes(String(password))');
|
||||
expect(block).not.toContain('username === ADMIN_USERNAME');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第四十三批回归闸门:大神接单页失败不得伪装成功
|
||||
*/
|
||||
describe('第四十三批大神接单反馈闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const radar = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/radar.vue'), 'utf-8');
|
||||
const hall = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/hall.vue'), 'utf-8');
|
||||
|
||||
it('radar 抢单:双重失败必须提示', () => {
|
||||
const start = radar.indexOf('const handleTakeOrder =');
|
||||
const block = radar.slice(start, start + 1600);
|
||||
expect(block).toContain('抢单失败,请稍后重试');
|
||||
expect(block).toContain('return;');
|
||||
});
|
||||
|
||||
it('hall 接单:失败必须提示', () => {
|
||||
const start = hall.indexOf('const handleGrabOrder =');
|
||||
const block = hall.slice(start, start + 1400);
|
||||
expect(block).toContain('接单失败,请稍后重试');
|
||||
expect(block).toContain('return;');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第四十二批回归闸门:邀请码不得静默丢失 + 举报人身份以当前用户为准
|
||||
*/
|
||||
describe('第四十二批邀请与举报身份闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const userIndex = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/index.vue'), 'utf-8');
|
||||
const expertDetail = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/detail.vue'), 'utf-8');
|
||||
const orderList = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/order/list.vue'), 'utf-8');
|
||||
|
||||
it('邀请码绑定失败保留待绑定码以便重试', () => {
|
||||
expect(userIndex).toContain('将于下次登录重试');
|
||||
const start = userIndex.indexOf('PENDING_INVITER_CODE');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
});
|
||||
|
||||
it('举报不再硬编码 reporterId=1001(由当前用户身份承担)', () => {
|
||||
expect(expertDetail).not.toContain('reporterId: 1001');
|
||||
expect(orderList).not.toContain('reporterId: 1001');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十八批回归闸门:小程序写操作失败不得再伪装成功
|
||||
*/
|
||||
describe('第三十八批小程序失败反馈闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const workspace = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/expert/workspace.vue'), 'utf-8');
|
||||
const orderList = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/order/list.vue'), 'utf-8');
|
||||
|
||||
it('大神工作台:抢单/拒单/提交完成/接单状态切换失败必须提示', () => {
|
||||
expect(workspace).toContain('抢单失败,请稍后重试');
|
||||
expect(workspace).toContain('拒单失败,请稍后重试');
|
||||
expect(workspace).toContain('提交失败,请稍后重试');
|
||||
expect(workspace).toContain('接单状态切换失败,请稍后重试');
|
||||
});
|
||||
|
||||
it('抢单失败不再无条件提示成功并移出订单池', () => {
|
||||
const start = workspace.indexOf('const grabOrder = async');
|
||||
const block = workspace.slice(start, start + 700);
|
||||
expect(block).toContain('return;');
|
||||
expect(block.indexOf('抢单失败')).toBeLessThan(block.indexOf("'抢单成功已接单'"));
|
||||
});
|
||||
|
||||
it('订单列表:取消失败必须提示且不本地改成已取消', () => {
|
||||
const start = orderList.indexOf('const cancelOrder =');
|
||||
const block = orderList.slice(start, start + 900);
|
||||
expect(block).toContain('取消失败,请稍后重试');
|
||||
expect(block).toContain('loadLiveOrders()');
|
||||
});
|
||||
|
||||
it('社区页:删除/上车申请失败必须提示', () => {
|
||||
const ci = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/index.vue'), 'utf-8');
|
||||
const cd = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/detail.vue'), 'utf-8');
|
||||
for (const code of [ci, cd]) {
|
||||
expect(code).toContain('删除失败,请重试');
|
||||
expect(code).toContain('申请失败,请重试');
|
||||
}
|
||||
});
|
||||
|
||||
it('设置保存:API 层不得吞错,页面失败需回滚并提示', () => {
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
const settings = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/settings.vue'), 'utf-8');
|
||||
const start = apiCode.indexOf('export async function updateUserSettings(');
|
||||
const block = apiCode.slice(start, start + 700);
|
||||
expect(block).toContain('throw err;');
|
||||
expect(block).not.toContain('return {\n userId: uid,');
|
||||
expect(settings).toContain('设置同步失败,请稍后重试');
|
||||
expect(settings).toContain('privacyHidePhone.value = prev');
|
||||
});
|
||||
|
||||
it('点赞:接口层不得吞错,页面需回滚乐观状态', () => {
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
const likeStart = apiCode.indexOf('export async function likePost(');
|
||||
const likeBlock = apiCode.slice(likeStart, likeStart + 500);
|
||||
expect(likeBlock).toContain('throw err;');
|
||||
expect(likeBlock).not.toContain('return null;');
|
||||
|
||||
const ci = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/index.vue'), 'utf-8');
|
||||
const cd = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/community/detail.vue'), 'utf-8');
|
||||
expect(ci).toContain('回滚乐观更新');
|
||||
expect(cd).toContain('回滚乐观更新');
|
||||
});
|
||||
|
||||
it('订单动作:取消/验收接口层不得吞错,验收失败需提示', () => {
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
for (const fn of ['export async function cancelOrder(', 'export async function completeOrder(']) {
|
||||
const block = apiCode.slice(apiCode.indexOf(fn), apiCode.indexOf(fn) + 500);
|
||||
expect(block).toContain('throw err;');
|
||||
expect(block).not.toContain('return null;');
|
||||
}
|
||||
const orderList2 = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/order/list.vue'), 'utf-8');
|
||||
const start = orderList2.indexOf('const confirmComplete =');
|
||||
const block = orderList2.slice(start, start + 900);
|
||||
expect(block).toContain('确认验收失败,请稍后重试');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十一批回归闸门:订单金额与优惠券服务端权威化
|
||||
*/
|
||||
describe('第二十一批订单金额完整性闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
const createOrderBlock = () => {
|
||||
const start = repoCode.indexOf('async createOrder(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return repoCode.slice(start, start + 4200);
|
||||
};
|
||||
|
||||
it('服务单价必须按服务端大神档案校验', () => {
|
||||
const block = createOrderBlock();
|
||||
expect(block).toContain('resolveCompanionRow(expertId)');
|
||||
expect(block).toContain('compRate');
|
||||
expect(block).toContain('hourlyRate = compRate');
|
||||
});
|
||||
|
||||
it('订单金额由服务端计算,不再直接信任客户端 amount', () => {
|
||||
const block = createOrderBlock();
|
||||
expect(block).toContain('const amount = Number(Math.max(baseAmount - couponDiscount, 0.01).toFixed(2))');
|
||||
expect(block).not.toContain('const amount = Number(data.amount) || hourlyRate * durationHours;');
|
||||
});
|
||||
|
||||
it('优惠券折扣必须经服务端有效券校验', () => {
|
||||
expect(repoCode).toContain('async resolveCouponDiscount(');
|
||||
const block = createOrderBlock();
|
||||
expect(block).toContain('resolveCouponDiscount(rawDiscount, track, baseAmount)');
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe('第五批安全加固闸门 (订单越权结算 / 订单身份收口 /
|
||||
const start = repoCode.indexOf('async completeOrderWithSettlement(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 1600);
|
||||
expect(block).toContain("const settleBlocked = ['待支付', '待接单', '已取消', '已退款', '已全额退款']");
|
||||
expect(block).toContain("const settleBlocked = ['待支付', '待付款', '待接单', '已取消', '已退款', '已全额退款']");
|
||||
expect(block).toContain('settleBlocked.includes(String(order.status))');
|
||||
expect(block).toContain('不可结算');
|
||||
expect(block).toContain('e.status = 409');
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十七批回归闸门:拒单/申请完成的状态闸门
|
||||
*/
|
||||
describe('第三十七批订单流转状态闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('仅「待接单」可拒单', () => {
|
||||
const start = repoCode.indexOf('async rejectOrder(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 900);
|
||||
expect(block).toContain("!== '待接单'");
|
||||
expect(block).toContain('仅待接单可拒单');
|
||||
});
|
||||
|
||||
it('申请完成仅限已履约订单', () => {
|
||||
const start = repoCode.indexOf('async applyCompleteOrder(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 1400);
|
||||
expect(block).toContain("const allowed = ['履约中', '服务中', '已到场核销', '待确认'];");
|
||||
expect(block).toContain('不可申请完成');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十九批回归闸门:抢单池只返回可认领的有效订单
|
||||
*/
|
||||
describe('第二十九批抢单池过滤闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('抢单池必须排除终态/已核销订单并过滤已认领订单', () => {
|
||||
const start = repoCode.indexOf('async getOrdersPool(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 1400);
|
||||
expect(block).toContain("WHERE status NOT IN ('已完成', '已取消'");
|
||||
expect(block).toContain('claimed_by IS NULL');
|
||||
expect(block).toContain('LIMIT 20');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十九批回归闸门:预支付金额不可篡改(低价支付高价订单)
|
||||
*/
|
||||
describe('第十九批支付金额完整性闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('订单归属解析必须携带订单金额', () => {
|
||||
const start = repoCode.indexOf('async getOrderOwnership(');
|
||||
const block = repoCode.slice(start, start + 900);
|
||||
expect(block).toContain('o.amount');
|
||||
expect(block).toContain('amount: Number(r.amount)');
|
||||
});
|
||||
|
||||
it('入账必须校验实收金额与支付意图一致', () => {
|
||||
const start = repoCode.indexOf('async applyPaymentSuccess(');
|
||||
const block = repoCode.slice(start, start + 1800);
|
||||
expect(block).toContain('amountMismatch');
|
||||
expect(block).toContain("reason: 'amount_mismatch'");
|
||||
});
|
||||
|
||||
it('预支付:仅买家可发起,且订单金额以服务端订单为准', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/pay/wechat/prepay'");
|
||||
const block = indexCode.slice(start, start + 2200);
|
||||
expect(block).toContain("'发起微信支付'");
|
||||
expect(block).toContain('repo.getOrderOwnership(String(orderId))');
|
||||
expect(block).toContain('payAmount = Number(own.amount) || 0');
|
||||
expect(block).not.toContain('const uid = Number(userId) || 0;');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十五批回归闸门:手机号登录身份匹配与联系方式唯一性
|
||||
*/
|
||||
describe('第三十五批手机号登录身份闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('登录优先按微信 openid 精确匹配', () => {
|
||||
const start = repoCode.indexOf('async findOrCreateUserByPhone(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 1600);
|
||||
expect(block).toContain('WHERE wechat_openid = ? LIMIT 1');
|
||||
// openid 查询应出现在手机号查询之前
|
||||
expect(block.indexOf('wechat_openid = ?')).toBeLessThan(block.indexOf('WHERE phone = ?'));
|
||||
});
|
||||
|
||||
it('修改联系方式必须做唯一性校验', () => {
|
||||
const start = repoCode.indexOf('async updateUserProfile(');
|
||||
const block = repoCode.slice(start, start + 1500);
|
||||
expect(block).toContain('该联系方式已被其他账号使用');
|
||||
expect(block).toContain('WHERE phone = ? AND id <> ?');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十四批回归闸门:举报/评价/上车申请/收藏/大神收益等读接口越权与泄露收口
|
||||
*/
|
||||
describe('第十四批读接口越权收口闸门 (举报 / 评价 / 上车申请 / 收藏 / 收益)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 1200) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 举报工单列表', () => {
|
||||
it('管理端全量 / 普通用户仅本人提交', () => {
|
||||
const block = routeBlock("app.get('/api/reports',");
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain("'查看我的举报'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 全量评价列表仅管理员', () => {
|
||||
it('GET /api/reviews 必须挂 requireAdmin', () => {
|
||||
expect(indexCode).toContain("app.get('/api/reviews', requireAdmin");
|
||||
expect(indexCode).toContain("app.get('/api/post-applies', requireAdmin");
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 动态上车申请名单归属', () => {
|
||||
it('仅发布者本人或管理员可查看某动态的上车申请', () => {
|
||||
const block = routeBlock("app.get('/api/posts/:id/applies'");
|
||||
expect(block).toContain("'查看上车申请'");
|
||||
expect(block).toContain('仅动态发布者本人可查看该动态的上车申请');
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 收藏列表/状态归属', () => {
|
||||
it('必须经 resolveSecureUserId', () => {
|
||||
expect(routeBlock("app.get('/api/favorites',")).toContain("'查看收藏列表'");
|
||||
expect(routeBlock("app.get('/api/favorites/check'")).toContain("'查询收藏状态'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 大神收益数据归属', () => {
|
||||
it('收益汇总与走势必须经 assertCompanionActor', () => {
|
||||
const stats = routeBlock("app.get('/api/expert/stats/:id',");
|
||||
const trends = routeBlock("app.get('/api/expert/stats/:id/trends'");
|
||||
expect(stats).toContain('assertCompanionActor(req, own');
|
||||
expect(trends).toContain('assertCompanionActor(req, own');
|
||||
});
|
||||
});
|
||||
|
||||
describe('6. 小程序端携带当前用户身份', () => {
|
||||
it('getPostApplies 必须带当前用户 id', () => {
|
||||
const start = apiCode.indexOf('export async function getPostApplies(');
|
||||
const block = apiCode.slice(start, start + 320);
|
||||
expect(block).toContain('/posts/${postId}/applies?userId=${normalizeUserId()}');
|
||||
});
|
||||
});
|
||||
});
|
||||
57
server/tests/read-scope.spec.ts
普通文件
57
server/tests/read-scope.spec.ts
普通文件
@@ -0,0 +1,57 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十二批回归闸门:读接口数据越权/泄露收口(订单全量、我的组局、分销团队)
|
||||
*/
|
||||
describe('第十二批读接口数据隔离闸门 (订单 / 我的组局 / 分销)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 1000) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 订单列表不得向任意客户端下发全量订单', () => {
|
||||
it('仓储层提供按用户隔离的 getOrdersForUser', () => {
|
||||
expect(repoCode).toContain('async getOrdersForUser(');
|
||||
const start = repoCode.indexOf('async getOrdersForUser(');
|
||||
const block = repoCode.slice(start, start + 1400);
|
||||
expect(block).toContain('buyer_id = ?');
|
||||
expect(block).toContain('claimed_by = ?');
|
||||
expect(block).toContain('companion_name IN (SELECT name FROM companions');
|
||||
});
|
||||
|
||||
it('订单列表:管理端全量,普通用户仅本人相关', () => {
|
||||
const block = routeBlock("app.get('/api/orders',");
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('repo.getOrdersForUser(uid)');
|
||||
expect(block).toContain("'查看订单列表'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 我的组局 / 分销数据归属', () => {
|
||||
it('my-activities 必须经 resolveSecureUserId', () => {
|
||||
const block = routeBlock("app.get('/api/users/:id/my-activities'");
|
||||
expect(block).toContain("'查看我的组局'");
|
||||
});
|
||||
|
||||
it('分销团队与返佣明细必须经 resolveSecureUserId', () => {
|
||||
expect(routeBlock("app.get('/api/distribution/team'")).toContain("'查看分销团队'");
|
||||
expect(routeBlock("app.get('/api/distribution/records'")).toContain("'查看返佣明细'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 小程序端订单列表携带当前用户身份', () => {
|
||||
it('getOrders 必须带上当前用户 id', () => {
|
||||
const start = apiCode.indexOf('export async function getOrders(');
|
||||
const block = apiCode.slice(start, start + 300);
|
||||
expect(block).toContain('/orders?userId=${normalizeUserId()}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十批回归闸门:充值赠金必须服务端按阶梯权威计算
|
||||
*/
|
||||
describe('第二十批充值赠金权威性闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('仓储层提供服务端赠金计算', () => {
|
||||
expect(repoCode).toContain('async computeRechargeBonusCents(');
|
||||
const start = repoCode.indexOf('async computeRechargeBonusCents(');
|
||||
const block = repoCode.slice(start, start + 700);
|
||||
expect(block).toContain('FROM recharge_tiers');
|
||||
expect(block).toContain('status = 1');
|
||||
expect(block).toContain('ORDER BY amount DESC');
|
||||
});
|
||||
|
||||
it('预支付不再信任客户端赠金', () => {
|
||||
expect(indexCode).not.toContain('bonusCents: Math.round(Number(bonusAmount || 0) * 100)');
|
||||
const start = indexCode.indexOf("app.post('/api/pay/wechat/prepay'");
|
||||
const block = indexCode.slice(start, start + 5200);
|
||||
expect(block).toContain('computeRechargeBonusCents(amountCents)');
|
||||
});
|
||||
|
||||
it('沙箱直接充值也不再信任客户端赠金', () => {
|
||||
expect(indexCode).not.toContain('rechargeUserBalance(userId, Number(amount), Number(bonusAmount || 0))');
|
||||
const start = indexCode.indexOf("app.post('/api/users/:id/recharge'");
|
||||
const block = indexCode.slice(start, start + 2400);
|
||||
expect(block).toContain('computeRechargeBonusCents(Math.round(amountYuan * 100))');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十七批回归闸门:请求层错误透传 + 核销码反查冲突优先级
|
||||
*/
|
||||
describe('第二十七批请求层与核销码反查闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const requestJs = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/request.js'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
describe('1. 小程序请求层', () => {
|
||||
it('错误必须附带业务 code / statusCode', () => {
|
||||
expect(requestJs).toContain('bizErr.code = res.data.code');
|
||||
expect(requestJs).toContain('bizErr.statusCode = res.statusCode');
|
||||
expect(requestJs).toContain('httpErr.statusCode = res.statusCode');
|
||||
});
|
||||
it('登录过期必须清理会话并提示', () => {
|
||||
expect(requestJs).toContain('function handleAuthExpired(');
|
||||
expect(requestJs).toContain('40100');
|
||||
expect(requestJs).toContain('登录已过期');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 核销码反查冲突优先级', () => {
|
||||
it('活动核销码优先未核销、最新记录', () => {
|
||||
const start = repoCode.indexOf('async queryCheckinCodeInfo(');
|
||||
const block = repoCode.slice(start, start + 1000);
|
||||
expect(block).toContain('ORDER BY p.checkin_status ASC, p.created_at DESC');
|
||||
});
|
||||
it('订单核销码排除已关闭状态并优先待核销', () => {
|
||||
const start = repoCode.indexOf('async queryCheckinCodeInfo(');
|
||||
const block = repoCode.slice(start, start + 2600);
|
||||
expect(block).toContain("AND status NOT IN ('已取消', '已退款', '已全额退款')");
|
||||
expect(block).toContain("ORDER BY FIELD(status, '履约中', '服务中', '待确认'");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十批回归闸门:订单评价/动态发布与删除的身份收口 + 关键路径可观测性
|
||||
*/
|
||||
describe('第十批身份收口与可观测性闸门 (评价 / 动态发布 / 动态删除)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 900) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 订单评价身份以服务端会话为准', () => {
|
||||
it('评价路由必须经 resolveSecureUserId 绑定买家身份', () => {
|
||||
const block = routeBlock("app.post('/api/orders/:id/review'");
|
||||
expect(block).toContain('resolveSecureUserId(');
|
||||
expect(block).toContain("'提交订单评价'");
|
||||
expect(block).toContain('userId: reviewerId');
|
||||
expect(block).not.toContain('userId: userId ? Number(userId) : undefined');
|
||||
});
|
||||
|
||||
it('仓储层仍保留服务端归属校验(仅下单买家)', () => {
|
||||
expect(repoCode).toContain('仅下单买家本人可以评价该订单');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 动态发布身份以服务端会话为准', () => {
|
||||
it('发布路由必须经 resolveSecureUserId 且写库用服务端 uid', () => {
|
||||
const block = routeBlock("app.post('/api/posts',");
|
||||
expect(block).toContain("'发布动态'");
|
||||
expect(block).toContain('repo.createPost({ ...(req.body || {}), userId: uid })');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 动态删除身份以服务端会话为准', () => {
|
||||
it('删除路由必须经 resolveSecureUserId', () => {
|
||||
const block = routeBlock("app.delete('/api/posts/:id'");
|
||||
expect(block).toContain("'删除动态'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 关键路径不得静默吞错', () => {
|
||||
it('openid 持久化/预支付解析/用户设置 IO 必须记录错误', () => {
|
||||
expect(indexCode).toContain("logServerError('login.updateUserOpenid'");
|
||||
expect(indexCode).toContain("logServerError('prepay.resolveOpenid'");
|
||||
expect(indexCode).toContain("logServerError('userSettings.write'");
|
||||
expect(repoCode).toContain("logServerError('getDashboardTodos.reports'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 小程序端写接口携带真实身份', () => {
|
||||
it('发布动态/提交评价必须注入当前用户身份', () => {
|
||||
expect(apiCode).toContain('userId: normalizeUserId(payload && payload.userId)');
|
||||
expect(apiCode).toContain('userId: normalizeUserId(reviewData.userId)');
|
||||
expect(apiCode).toContain('const uid = normalizeUserId(userId);');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('第三批安全加固闸门 (动态越权删除 / 组局解散提权 /
|
||||
const start = code.indexOf('export async function deletePost(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = code.slice(start, start + 400);
|
||||
expect(block).toContain("uni.getStorageSync('userId') || 1001");
|
||||
expect(block).toContain('normalizeUserId(userId)');
|
||||
expect(block).not.toContain('userId = 1001');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第三十二批回归闸门:用户设置文件不得位于对外静态目录
|
||||
*/
|
||||
describe('第三十二批用户设置文件私有化闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('设置文件路径必须位于私有 data 目录而非 /uploads', () => {
|
||||
expect(indexCode).not.toContain("path.resolve(uploadDir, 'user_settings.json')");
|
||||
expect(indexCode).toContain('const userSettingsDir =');
|
||||
expect(indexCode).toContain("path.resolve(userSettingsDir, 'user_settings.json')");
|
||||
});
|
||||
|
||||
it('/uploads 仍仅用于静态资源', () => {
|
||||
expect(indexCode).toContain("app.use('/uploads', express.static(uploadDir))");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第四十九批回归闸门:结算/仲裁给大神入账必须唯一命中,禁止昵称模糊重复入账
|
||||
*/
|
||||
describe('第四十九批结算入账唯一性闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('提供唯一解析结算收款用户的私有方法', () => {
|
||||
expect(repoCode).toContain('async resolveExpertCreditUserId(');
|
||||
const start = repoCode.indexOf('async resolveExpertCreditUserId(');
|
||||
const block = repoCode.slice(start, start + 800);
|
||||
expect(block).toContain('WHERE nickname = ? LIMIT 1');
|
||||
expect(block).toContain('comp?.user_id');
|
||||
});
|
||||
|
||||
it('不得再使用 id OR nickname 的模糊入账', () => {
|
||||
expect(repoCode).not.toContain('WHERE id = ? OR nickname = ?');
|
||||
});
|
||||
|
||||
it('完单结算与仲裁部分结算均使用唯一收款用户', () => {
|
||||
const settle = repoCode.slice(repoCode.indexOf('async completeOrderWithSettlement('));
|
||||
expect(settle.slice(0, 3000)).toContain('resolveExpertCreditUserId(conn, order, comp)');
|
||||
const arb = repoCode.slice(repoCode.indexOf('async handleArbitration('));
|
||||
expect(arb.slice(0, 7000)).toContain('resolveExpertCreditUserId(conn, order, comp)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第二十三批回归闸门:订单初始状态与结算状态闸门
|
||||
*/
|
||||
describe('第二十三批结算状态闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('结算闸门必须拦截未付款(待付款)订单', () => {
|
||||
const start = repoCode.indexOf('async completeOrderWithSettlement(');
|
||||
const block = repoCode.slice(start, start + 900);
|
||||
expect(block).toContain("'待付款'");
|
||||
expect(block).toContain("'待支付'");
|
||||
});
|
||||
|
||||
it('订单初始状态必须由服务端按支付方式决定', () => {
|
||||
const start = repoCode.indexOf('async createOrder(');
|
||||
const block = repoCode.slice(start, start + 4200);
|
||||
expect(block).toContain("const status = payType === 'BALANCE' ? '履约中' : '待付款';");
|
||||
expect(block).not.toContain('const status = data.status ||');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十四批回归闸门:启动配置自检(部署验收)
|
||||
*/
|
||||
describe('第五十四批启动自检闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('提供启动自检并输出就绪状态(不泄露密钥)', () => {
|
||||
expect(indexCode).toContain('async function logStartupConfigSelfCheck(');
|
||||
const start = indexCode.indexOf('async function logStartupConfigSelfCheck(');
|
||||
const block = indexCode.slice(start, start + 1800);
|
||||
expect(block).toContain('运行模式=');
|
||||
expect(block).toContain('实盘就绪=');
|
||||
expect(block).toContain('未设置 ADMIN_TOKEN_SECRET');
|
||||
// 不得打印密钥值,仅打印是否设置
|
||||
expect(block).not.toContain('${ADMIN_TOKEN_SECRET}');
|
||||
expect(block).not.toContain('${USER_TOKEN_SECRET}');
|
||||
});
|
||||
|
||||
it('bootstrap 中调用自检', () => {
|
||||
expect(indexCode).toContain('await logStartupConfigSelfCheck();');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十七批回归闸门:微信订阅消息(服务通知)真实下发闭环
|
||||
*/
|
||||
describe('第五十七批微信订阅消息闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const svc = fs.readFileSync(path.join(rootDir, 'server/src/core/notify/wechat-subscribe.service.ts'), 'utf-8');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
const settings = fs.readFileSync(path.join(rootDir, 'pwuniapp/pages/user/settings.vue'), 'utf-8');
|
||||
|
||||
it('提供微信订阅消息下发客户端', () => {
|
||||
expect(svc).toContain('export function sendSubscribeMessageHttp(');
|
||||
expect(svc).toContain("cgi-bin/message/subscribe/send");
|
||||
expect(svc).toContain('export function buildTemplateData(');
|
||||
});
|
||||
|
||||
it('服务端按配置真实下发并接入触发点(字段与所选公共模板对齐)', () => {
|
||||
expect(indexCode).toContain('async function notifyBySubscribe(');
|
||||
expect(indexCode).toContain("app.get('/api/message/subscribe-templates'");
|
||||
expect(indexCode).toContain('sendSubscribeMessageHttp(accessToken');
|
||||
for (const key of ['orderStatus', 'activityJoin', 'withdrawSuccess']) {
|
||||
expect(indexCode).toContain(`notifyBySubscribe('${key}'`);
|
||||
}
|
||||
// 订单状态提醒模板字段
|
||||
expect(indexCode).toContain('character_string1:');
|
||||
expect(indexCode).toContain('phrase2:');
|
||||
expect(indexCode).toContain('thing3:');
|
||||
expect(indexCode).toContain('thing4:');
|
||||
// 提现审核通知模板字段
|
||||
expect(indexCode).toContain('amount2:');
|
||||
expect(indexCode).toContain('phrase5:');
|
||||
expect(indexCode).toContain('time7:');
|
||||
expect(indexCode).toContain('amount8:');
|
||||
// 活动报名成功通知模板字段
|
||||
expect(indexCode).toContain('thing1:');
|
||||
expect(indexCode).toContain('time2:');
|
||||
expect(indexCode).toContain('time3:');
|
||||
// 开始时间兜底:非标准日期回退当前时间,避免 time 字段 47003
|
||||
expect(indexCode).toContain('function toCnTimeOrNow(');
|
||||
expect(indexCode).toContain('time3: toCnTimeOrNow(act?.startTime)');
|
||||
});
|
||||
|
||||
it('管理端测试推送在就绪时真实下发', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/admin/message/test-send'");
|
||||
const block = indexCode.slice(start, start + 3400);
|
||||
expect(block).toContain("mode: 'REAL'");
|
||||
expect(block).toContain("mode: 'SIMULATED'");
|
||||
});
|
||||
|
||||
it('小程序用真实模板ID 请求订阅授权', () => {
|
||||
expect(apiCode).toContain('export async function getSubscribeTemplates(');
|
||||
expect(apiCode).toContain('/message/subscribe-templates');
|
||||
expect(settings).toContain('requestNoticeSubscribe');
|
||||
expect(settings).not.toContain("'ORDER_STATUS_NOTICE_TMPL'");
|
||||
});
|
||||
});
|
||||
20
server/tests/upload-auth.spec.ts
普通文件
20
server/tests/upload-auth.spec.ts
普通文件
@@ -0,0 +1,20 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第四十五批回归闸门:上传接口生产环境需登录
|
||||
*/
|
||||
describe('第四十五批上传鉴权闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('生产环境上传必须要求登录', () => {
|
||||
const start = indexCode.indexOf('const handleFileUpload =');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 500);
|
||||
expect(block).toContain('IS_PRODUCTION');
|
||||
expect(block).toContain("resolveSecureUserId(req, 0, '上传文件')");
|
||||
expect(block).toContain('请先登录后再上传');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十一批回归闸门:管理员用户360画像按 id 精确关联
|
||||
*/
|
||||
describe('第五十一批用户360画像关联闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('订单按 buyer/expert/claimed/companion(user_id) 关联', () => {
|
||||
const start = repoCode.indexOf('async getUserDetail(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 3000);
|
||||
expect(block).toContain('OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)');
|
||||
expect(block).not.toContain('WHERE client_name = ? OR companion_name = ?');
|
||||
});
|
||||
|
||||
it('资金流水按 user_id 精确归属', () => {
|
||||
const start = repoCode.indexOf('async getUserDetail(');
|
||||
const block = repoCode.slice(start, start + 3200);
|
||||
expect(block).toContain('WHERE user_id = ?');
|
||||
expect(block).not.toContain('counterparty LIKE ? OR counterparty LIKE ?');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十三批回归闸门:用户/风控读接口的敏感字段与鉴权收口
|
||||
*/
|
||||
describe('第十三批用户读接口隐私闸门 (全量用户 / 360画像 / 风控日志 / 个人资产)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 1600) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 管理端专属读接口必须鉴权', () => {
|
||||
it('全量用户列表仅管理员可见', () => {
|
||||
expect(indexCode).toContain("app.get('/api/users', requireAdmin");
|
||||
});
|
||||
it('用户360画像仅管理员可见', () => {
|
||||
expect(indexCode).toContain("app.get('/api/users/:id/detail', requireAdmin");
|
||||
});
|
||||
it('风控日志仅管理员可见', () => {
|
||||
expect(indexCode).toContain("app.get('/api/safety/audit-logs', requireAdmin");
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 用户基本信息:敏感字段仅本人/管理员可见', () => {
|
||||
it('GET /api/users/:id 必须做本人/管理员判断并剥离敏感字段', () => {
|
||||
const block = routeBlock("app.get('/api/users/:id',", 8000);
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('publicUser');
|
||||
expect(block).not.toContain('phone: user.phone');
|
||||
expect(block).not.toContain('balance: user.balance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 个人资产/流水仅本人可读', () => {
|
||||
it('提现记录与财务流水必须经 resolveSecureUserId', () => {
|
||||
expect(routeBlock("app.get('/api/users/:id/withdrawals'")).toContain("'查看提现记录'");
|
||||
expect(routeBlock("app.get('/api/users/:id/finance-records'")).toContain("'查看财务流水'");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第十五批回归闸门:核销码反查防枚举 + 抢单池客户名脱敏
|
||||
*/
|
||||
describe('第十五批核销码与抢单池加固闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
const apiCode = fs.readFileSync(path.join(rootDir, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
|
||||
const routeBlock = (marker: string, len = 1400) => {
|
||||
const start = indexCode.indexOf(marker);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
return indexCode.slice(start, start + len);
|
||||
};
|
||||
|
||||
describe('1. 核销码反查不得被匿名枚举', () => {
|
||||
it('必须要求登录并按门票归属方校验', () => {
|
||||
const block = routeBlock("app.get('/api/verify/query/:code'");
|
||||
expect(block).toContain("'查询核销码'");
|
||||
expect(block).toContain('IS_PRODUCTION && !isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('无权查看该核销码信息');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 抢单池客户名脱敏', () => {
|
||||
it('仓储层必须对 client_name 脱敏', () => {
|
||||
const start = repoCode.indexOf('async getOrdersPool(');
|
||||
const block = repoCode.slice(start, start + 1400);
|
||||
expect(block).toContain('maskClientName');
|
||||
expect(block).not.toContain('userName: r.clientName ||');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 小程序反查携带当前用户身份', () => {
|
||||
it('queryCheckinCode 必须带 userId', () => {
|
||||
const start = apiCode.indexOf('export async function queryCheckinCode(');
|
||||
const block = apiCode.slice(start, start + 300);
|
||||
expect(block).toContain('/verify/query/${code}?userId=${normalizeUserId()}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十二批回归闸门:个人资金流水按 user_id 精确归属
|
||||
*/
|
||||
describe('第五十二批资金流水归属闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
|
||||
it('个人流水查询仅按 user_id 过滤,不再用 counterparty LIKE', () => {
|
||||
const start = repoCode.indexOf('async getUserFinanceRecords(');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = repoCode.slice(start, start + 700);
|
||||
expect(block).toContain('WHERE user_id = ?');
|
||||
expect(block).not.toContain('counterparty LIKE ?');
|
||||
expect(block).not.toContain('OR related_id = ?');
|
||||
});
|
||||
|
||||
it('关键资金流水写入时带 user_id', () => {
|
||||
expect(repoCode).toContain("'钱包余额支付订单', ?, 'OUT'");
|
||||
expect(repoCode).toContain("'佣金提现', ?, 'OUT'");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第五十五批回归闸门:提现「真实微信打款」闭环
|
||||
*/
|
||||
describe('第五十五批提现打款闭环闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const payV3 = fs.readFileSync(path.join(rootDir, 'server/src/core/pay/wechat-pay-v3.ts'), 'utf-8');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
const mysqlCode = fs.readFileSync(path.join(rootDir, 'server/src/db/mysql.ts'), 'utf-8');
|
||||
|
||||
it('支付客户端具备商家转账到零钱能力', () => {
|
||||
expect(payV3).toContain('export async function transferToWallet(');
|
||||
expect(payV3).toContain("'/v3/fund-app/mch-transfer/transfer-bills'");
|
||||
expect(payV3).toContain('export async function queryTransferByOutBillNo(');
|
||||
expect(payV3).toContain('export function encryptSensitiveInfo(');
|
||||
});
|
||||
|
||||
it('提现审核通过按模式真实打款 / 生产未就绪拒绝', () => {
|
||||
const start = indexCode.indexOf("app.post('/api/finance/withdrawals/:id/review'");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const block = indexCode.slice(start, start + 4600);
|
||||
expect(block).toContain('cfg.isReadyForTransfer');
|
||||
expect(block).toContain('transferToWallet(cfg');
|
||||
expect(block).toContain('getWithdrawalForTransfer(wdId)');
|
||||
expect(block).toContain('微信商家转账未就绪');
|
||||
expect(block).not.toContain('款项已通过微信商户零钱打款'); // 移除误导文案
|
||||
});
|
||||
|
||||
it('提供转账结果回调与最终态落库', () => {
|
||||
expect(indexCode).toContain("app.post('/api/pay/wechat/transfer-notify'");
|
||||
expect(repoCode).toContain('async finalizeWithdrawalTransfer(');
|
||||
expect(repoCode).toContain('async getWithdrawalForTransfer(');
|
||||
expect(repoCode).toContain('async markWithdrawalTransfer(');
|
||||
});
|
||||
|
||||
it('提现表具备转账/收款人字段', () => {
|
||||
expect(mysqlCode).toContain('transfer_bill_no');
|
||||
expect(mysqlCode).toContain('transfer_status');
|
||||
expect(mysqlCode).toContain('payee_name');
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户