170 lines
8.2 KiB
JavaScript
170 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* WP-WX-01 离线回归:forwardMessage 文本转发真 Frida 分支逻辑
|
||
*
|
||
* 真机铁律:本测试**仅作离线回归**,不替代真机 Frida attach E2E。
|
||
* 它从 wechat_hook_v2.js 抽取真实 forwardMessage 源码(不复制、不漂移),
|
||
* 注入 mock 依赖,验证三条分支:
|
||
* 1) 文本类(type=1) → _sendMessageInternal 真发送成功 → 返回 text_resend,不走 intent
|
||
* 2) 非文本 / DB miss → 降级 _intentAction,标 non_text_or_db_miss(不伪造成功)
|
||
* 3) 真发送未验证(success:false) → 不假成功,降级 intent
|
||
* 4) 缺参 → 直接 error
|
||
*
|
||
* 运行:node sdk/tests/test_forward_message_logic.mjs
|
||
*/
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import path from 'node:path';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const JS_PATH = path.join(__dirname, '..', 'agent', 'hook', 'wechat_hook_v2.js');
|
||
const src = readFileSync(JS_PATH, 'utf8');
|
||
|
||
// 抽取 forwardMessage 函数体(花括号配平),避免复制造成漂移
|
||
function extractFunc(name) {
|
||
const re = new RegExp(name + '\\s*:\\s*function\\s*\\([^)]*\\)\\s*\\{');
|
||
const m = re.exec(src);
|
||
if (!m) throw new Error('未找到函数 ' + name);
|
||
const bodyStart = src.indexOf('{', m.index + m[0].length - 1);
|
||
let depth = 0;
|
||
for (let i = bodyStart; i < src.length; i++) {
|
||
if (src[i] === '{') depth++;
|
||
else if (src[i] === '}') {
|
||
depth--;
|
||
if (depth === 0) {
|
||
// 返回 "function(params){ ... }" 形式
|
||
const argsStart = src.indexOf('(', m.index);
|
||
const argsEnd = src.indexOf(')', argsStart);
|
||
const args = src.slice(argsStart + 1, argsEnd);
|
||
return new Function(args, src.slice(bodyStart + 1, i));
|
||
}
|
||
}
|
||
}
|
||
throw new Error('花括号未配平');
|
||
}
|
||
|
||
let assertions = 0;
|
||
function assert(cond, msg) {
|
||
assertions++;
|
||
if (!cond) { console.error(' ✗ FAIL: ' + msg); process.exitCode = 1; }
|
||
else console.log(' ✓ ' + msg);
|
||
}
|
||
|
||
// ---- mock harness ----
|
||
let calls;
|
||
function resetMocks(dbRows, sendResult, revokeResult) {
|
||
calls = { execSQL: [], sendInternal: [], intent: [], event: [], revoke: [] };
|
||
globalThis.Java = { performNow: (fn) => fn() };
|
||
globalThis._execSQL = (sql) => { calls.execSQL.push(sql); return dbRows; };
|
||
globalThis._sendMessageInternal = (toId, content, type) => {
|
||
calls.sendInternal.push({ toId, content, type });
|
||
return sendResult;
|
||
};
|
||
globalThis._intentAction = (action, payload, evt, extra) => {
|
||
calls.intent.push({ action, payload, evt, extra });
|
||
return { success: false, action, _intent: true, extra };
|
||
};
|
||
// revokeMessage 已改走真 Frida RevokeMsgEvent,mock 其返回以验证诚实分支
|
||
globalThis._revokeMessageByRevokeMsgEvent = (msgSvrId, row) => {
|
||
calls.revoke.push({ msgSvrId, row });
|
||
return revokeResult || { success: false, error: 'revoke_frida_failed', msg_svr_id: msgSvrId, eligible: true };
|
||
};
|
||
globalThis.emitEvent = (evt, data) => { calls.event.push({ evt, data }); };
|
||
globalThis.String = String;
|
||
}
|
||
|
||
const forwardMessage = extractFunc('forwardMessage');
|
||
|
||
console.log('[WP-WX-01] forwardMessage 离线分支回归');
|
||
|
||
// 1) 文本类真发送成功
|
||
resetMocks([{ type: 1, content: '你好转发' }], { success: true, message_id: 'm123', verified: true });
|
||
let r = forwardMessage({ msg_svr_id: 's1', to_id: 'wxid_a' });
|
||
assert(r.success === true, '文本类转发成功');
|
||
assert(r.forward_kind === 'text_resend', '标记 text_resend');
|
||
assert(calls.sendInternal.length === 1 && calls.sendInternal[0].content === '你好转发', '走 _sendMessageInternal 真发送');
|
||
assert(calls.intent.length === 0, '成功路径不降级 intent');
|
||
assert(calls.event.length === 1 && calls.event[0].evt === 'message_forwarded', '发 message_forwarded 事件');
|
||
|
||
// 2) 非文本(type=3 图片)→ intent 降级,不假成功
|
||
resetMocks([{ type: 3, content: '[图片]' }], { success: true });
|
||
r = forwardMessage({ msg_svr_id: 's2', to_id: 'wxid_b' });
|
||
assert(calls.sendInternal.length === 0, '非文本不调真发送');
|
||
assert(calls.intent.length === 1 && calls.intent[0].extra.kind === 'non_text_or_db_miss', '非文本降级 intent (non_text_or_db_miss)');
|
||
assert(r._intent === true && r.success === false, '降级 intent 不伪造成功');
|
||
|
||
// 3) DB miss(无行)→ intent 降级
|
||
resetMocks([], { success: true });
|
||
r = forwardMessage({ msg_svr_id: 's3', to_id: 'wxid_c' });
|
||
assert(calls.intent.length === 1, 'DB miss 降级 intent');
|
||
|
||
// 4) 文本但真发送未验证(success:false) → 不假成功,降级 intent
|
||
resetMocks([{ type: 1, content: 'x' }], { success: false, error: 'unverified' });
|
||
r = forwardMessage({ msg_svr_id: 's4', to_id: 'wxid_d' });
|
||
assert(calls.sendInternal.length === 1, '尝试真发送');
|
||
assert(calls.intent.length === 1, '真发送未验证 → 降级 intent,不假成功');
|
||
|
||
// 5) 缺参
|
||
r = forwardMessage({ msg_svr_id: '', to_id: '' });
|
||
assert(r.success === false && /缺少/.test(r.error), '缺参直接 error');
|
||
|
||
// ---- forwardMultiple 批量转发 ----
|
||
console.log('[WP-WX-01] forwardMultiple 离线分支回归');
|
||
const forwardMultiple = extractFunc('forwardMultiple');
|
||
|
||
// 6) 全文本 → 全部真发送,forwarded=2
|
||
resetMocks([{ type: 1, content: 'a' }], { success: true, message_id: 'm' });
|
||
r = forwardMultiple({ msg_svr_ids: ['s1', 's2'], to_id: 'wxid_x' });
|
||
assert(r.success === true && r.forwarded === 2 && r.fallback === 0, '全文本批量转发 forwarded=2');
|
||
assert(calls.sendInternal.length === 2, '逐条调真发送 2 次');
|
||
assert(calls.intent.length === 0, '有成功条不整体降级');
|
||
|
||
// 7) 全非文本 → forwarded=0 整体降级 intent,不假成功
|
||
resetMocks([{ type: 3, content: '[图片]' }], { success: true });
|
||
r = forwardMultiple({ msg_svr_ids: ['s1', 's2'], to_id: 'wxid_y' });
|
||
assert(r._intent === true && r.success === false, '全非文本整体降级 intent 不假成功');
|
||
assert(calls.intent[0].extra.forwarded === 0, 'intent 标 forwarded=0');
|
||
|
||
// 8) 缺参
|
||
r = forwardMultiple({ msg_svr_ids: [], to_id: 'wxid_z' });
|
||
assert(r.success === false && /缺少/.test(r.error), 'forwardMultiple 缺参 error');
|
||
|
||
// ---- revokeMessage 撤回资格预检 ----
|
||
console.log('[WP-WX] revokeMessage 资格预检回归');
|
||
const revokeMessage = extractFunc('revokeMessage');
|
||
|
||
// 9) 消息不存在 → 诚实 msg_not_found,不发 intent
|
||
resetMocks([], null);
|
||
r = revokeMessage({ msg_svr_id: 's1' });
|
||
assert(r.success === false && r.error_code === 'msg_not_found', '消息不存在→msg_not_found');
|
||
assert(calls.intent.length === 0, '不存在不发 intent 假成功');
|
||
|
||
// 10) 非本人发送 → not_own_message
|
||
resetMocks([{ isSend: 0, createTime: Date.now() }], null);
|
||
r = revokeMessage({ msg_svr_id: 's2' });
|
||
assert(r.success === false && r.error_code === 'not_own_message', '非本人→not_own_message');
|
||
|
||
// 11) 超 2 分钟窗口 → revoke_window_expired
|
||
resetMocks([{ isSend: 1, createTime: Date.now() - 200000 }], null);
|
||
r = revokeMessage({ msg_svr_id: 's3' });
|
||
assert(r.success === false && r.error_code === 'revoke_window_expired', '超窗→revoke_window_expired');
|
||
|
||
// 12) 资格通过(本人+窗口内) → 走真 Frida RevokeMsgEvent,DB 已确认→成功发 message_revoked
|
||
resetMocks([{ isSend: 1, createTime: Date.now() - 5000 }], null, { success: true, verified: true, msg_svr_id: 's4' });
|
||
r = revokeMessage({ msg_svr_id: 's4' });
|
||
assert(calls.revoke.length === 1, '资格通过→走真 Frida RevokeMsgEvent');
|
||
assert(r.success === true && r.verified === true, '真撤回 DB 确认→成功');
|
||
assert(calls.event.length === 1 && calls.event[0].evt === 'message_revoked', '发 message_revoked 事件');
|
||
|
||
// 12b) 资格通过但真 RPC 未 DB 确认 → 诚实失败,不假成功、不发事件
|
||
resetMocks([{ isSend: 1, createTime: Date.now() - 5000 }], null, { success: false, error: 'revoke_frida_failed', eligible: true });
|
||
r = revokeMessage({ msg_svr_id: 's4b' });
|
||
assert(calls.revoke.length === 1, '资格通过→尝试真 Frida 撤回');
|
||
assert(r.success === false && calls.event.length === 0, '真撤回未确认→诚实失败不发事件');
|
||
|
||
// 13) 缺参
|
||
r = revokeMessage({ msg_svr_id: '' });
|
||
assert(r.success === false && /缺少/.test(r.error), 'revokeMessage 缺参 error');
|
||
|
||
console.log(`\n[done] 断言 ${assertions} 条,exit=${process.exitCode || 0}`);
|