// 语义化发包:route 正确、只带意图、参数形状校验 const { load, throws } = require('./_load'); const t = require('./_assert')(); load('client/js/01_SubGame/codes/state/GameState.js'); // 桩掉 RpcHelper,捕获发出的包 const sent = []; global.RpcHelper = { sendRpc: function (app, route, rpc, data) { sent.push({ app, route, rpc, data }); } }; load('client/js/01_SubGame/codes/net/Rpc.js'); EQW_GameState.room.mySeat = 2; // ---- route 必须是 erqiwang,不是 room ---- sent.length = 0; EQW_Rpc.jiaofen(60); t.eq('发了一个包', sent.length, 1); t.eq('app', sent[0].app, 'youle'); t.eq('route 是 erqiwang', sent[0].route, 'erqiwang'); t.eq('rpc', sent[0].rpc, 'jiaofen'); t.eq('业务字段', sent[0].data, { seat: 2, call: 60 }); // ---- seat 自动从 GameState 带上 ---- EQW_GameState.room.mySeat = 0; sent.length = 0; EQW_Rpc.zhunbei(); t.eq('seat 自动带上', sent[0].data.seat, 0); EQW_GameState.room.mySeat = 2; // ---- 各方法的字段集合 ---- const call = (fn) => { sent.length = 0; fn(); return sent[0]; }; t.eq('不叫', call(() => EQW_Rpc.jiaofen(0)).data, { seat: 2, call: 0 }); t.eq('投降', call(() => EQW_Rpc.touxiang()).data, { seat: 2 }); t.eq('选主', call(() => EQW_Rpc.xuanzhu(3)).data, { seat: 2, flower: 3 }); t.eq('埋牌', call(() => EQW_Rpc.maipai([1,2,3,4,5,6,7,8])).data, { seat: 2, cards: [1,2,3,4,5,6,7,8] }); t.eq('出牌', call(() => EQW_Rpc.chupai([10,11])).data, { seat: 2, cards: [10,11] }); t.eq('明牌', call(() => EQW_Rpc.mingpai()).data, { seat: 2 }); t.eq('提示', call(() => EQW_Rpc.tishi(1)).data, { seat: 2, tip: 1 }); t.eq('准备', call(() => EQW_Rpc.zhunbei()).data, { seat: 2 }); // ---- 【核心】只带意图:所有发包的字段集合不含任何结论字段 ---- const FORBIDDEN = ['score', 'isWin', 'phase', 'nextSeat', 'nextseat', 'handCards', 'grade', 'multiple', 'result', 'banker', 'curmultiple', 'upgrade']; sent.length = 0; EQW_Rpc.jiaofen(60); EQW_Rpc.touxiang(); EQW_Rpc.xuanzhu(1); EQW_Rpc.maipai([1,2,3,4,5,6,7,8]); EQW_Rpc.chupai([9]); EQW_Rpc.mingpai(); EQW_Rpc.tishi(2); EQW_Rpc.zhunbei(); const leaked = []; sent.forEach(p => Object.keys(p.data).forEach(k => { if (FORBIDDEN.indexOf(k) >= 0) { leaked.push(p.rpc + '.' + k); } })); t.eq('发包不含任何结论字段', leaked, []); // ---- 参数形状校验:非法即抛错,不发残缺包 ---- sent.length = 0; t.eq('牌 id 非数组抛错', throws(() => EQW_Rpc.chupai('5')), true); t.eq('空数组抛错', throws(() => EQW_Rpc.chupai([])), true); t.eq('字符串牌 id 抛错', throws(() => EQW_Rpc.chupai(['5'])), true); t.eq('小数牌 id 抛错', throws(() => EQW_Rpc.chupai([1.5])), true); t.eq('越界牌 id 抛错', throws(() => EQW_Rpc.chupai([108])), true); t.eq('负数牌 id 抛错', throws(() => EQW_Rpc.chupai([-1])), true); t.eq('重复牌 id 抛错', throws(() => EQW_Rpc.chupai([5, 5])), true); t.eq('埋牌非 8 张抛错', throws(() => EQW_Rpc.maipai([1,2,3])), true); t.eq('叫分越界抛错', throws(() => EQW_Rpc.jiaofen(71)), true); t.eq('叫分非 5 的倍数抛错', throws(() => EQW_Rpc.jiaofen(7)), true); t.eq('花色越界抛错', throws(() => EQW_Rpc.xuanzhu(5)), true); t.eq('提示类型越界抛错', throws(() => EQW_Rpc.tishi(4)), true); t.eq('校验失败时一个包都没发', sent.length, 0); // ---- 合法边界值不抛 ---- t.eq('牌 id 0 合法', throws(() => EQW_Rpc.chupai([0])), false); t.eq('牌 id 107 合法', throws(() => EQW_Rpc.chupai([107])), false); t.eq('叫分 5 合法', throws(() => EQW_Rpc.jiaofen(5)), false); t.eq('叫分 70 合法', throws(() => EQW_Rpc.jiaofen(70)), false); process.exit(t.done('rpc_send') ? 0 : 1);