import json import plistlib import subprocess import tempfile import unittest from pathlib import Path from unittest.mock import patch from wechat_tool.mac_connect import MacConnection from wechat_tool.mac_keys import permission_diagnostic from wechat_tool.mac_recovery import DebugRecovery class PermissionTests(unittest.TestCase): def test_signed_target_is_distinct_from_administrator_permission(self): proc = {'pid':123,'path':'/Applications/WeChat.app/Contents/MacOS/WeChat'} with patch('wechat_tool.mac_keys.wechat_processes', return_value=[proc]), patch('wechat_tool.mac_keys.signing_flags', return_value={'hardened_runtime':True,'get_task_allow':False}): result=permission_diagnostic(5,123) self.assertEqual(result['code'],'target_restricted') self.assertEqual(result['task_for_pid_code'],5) self.assertIn('反复输入密码',result['summary']) with patch('wechat_tool.mac_keys.wechat_processes',return_value=[]): self.assertEqual(permission_diagnostic()['code'],'no_process') with patch('wechat_tool.mac_keys.wechat_processes',return_value=[proc,dict(proc,pid=456)]): self.assertEqual(permission_diagnostic()['code'],'multiple_processes') def test_administrator_helper_preserves_structured_failure(self): diagnostic={'code':'target_restricted','summary':'目标签名拒绝调试','task_for_pid_code':5} output=json.dumps({'error':'目标签名拒绝调试','diagnostic':diagnostic}) with tempfile.TemporaryDirectory() as directory: service=MacConnection(directory);service.job={'status':'running'} with patch('wechat_tool.mac_connect.subprocess.run',return_value=subprocess.CompletedProcess([],0,output,'')): service.work('id','capture',{'administrator':True},None,[]) self.assertEqual(service.job['status'],'failed') self.assertEqual(service.job['error_code'],'target_restricted') self.assertEqual(service.status()['diagnostic'],diagnostic) class RecoveryTests(unittest.TestCase): def setUp(self): self.temp=tempfile.TemporaryDirectory() self.base=Path(self.temp.name) self.app=self.base/'Applications/WeChat.app' for directory in ('Contents/MacOS','Contents/_CodeSignature'): (self.app/directory).mkdir(parents=True) (self.app/'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier':'com.tencent.xinWeChat','CFBundleExecutable':'WeChat','CFBundleShortVersionString':'4.1.0'})) (self.app/'Contents/MacOS/WeChat').write_bytes(b'fake-app-binary') (self.app/'Contents/_CodeSignature/CodeResources').write_bytes(b'fake-resource-seal') self.account=self.base/'xwechat_files/account/db_storage' self.account.mkdir(parents=True) (self.account/'test.db').write_bytes(b'account-original') all_users=self.account.parent.parent/'all_users' all_users.mkdir() (all_users/'login-config').write_bytes(b'login-original') self.recovery=DebugRecovery(self.base/'workspace') self.command_patch=patch('wechat_tool.mac_recovery.run',return_value=subprocess.CompletedProcess([],0,b'',b'')) self.runner=self.command_patch.start() def tearDown(self): self.command_patch.stop() self.temp.cleanup() def plan(self): return self.recovery.plan(self.account,app=self.app) def test_preview_is_readonly_and_consent_is_required(self): result=self.plan() self.assertFalse((self.base/'workspace').exists()) self.assertTrue(result['effects']) with self.assertRaisesRegex(ValueError,'明确勾选'): self.recovery.consume(result['plan_id'],False,self.account) self.assertTrue(all('--force' not in call.args[0] for call in self.runner.call_args_list)) self.recovery.consume(result['plan_id'],True,self.account) with self.assertRaisesRegex(ValueError,'已使用'): self.recovery.consume(result['plan_id'],True,self.account) def test_preparation_backs_up_before_signing_copy_and_preserves_original(self): result=self.plan() plan=self.recovery.consume(result['plan_id'],True,self.account) original={str(p.relative_to(self.app)):p.read_bytes() for p in self.app.rglob('*') if p.is_file()} def grants(app): return {'com.apple.security.get-task-allow':app!=self.app} def command(args,timeout=90): if '--force' in args: self.assertEqual(Path(args[-1]),plan['root']/'WeChat.app') self.assertEqual((plan['root']/'backup/0-account/db_storage/test.db').read_bytes(),b'account-original') self.assertEqual((plan['root']/'backup/1-all_users/login-config').read_bytes(),b'login-original') return subprocess.CompletedProcess(args,0,b'',b'') self.runner.side_effect=command with patch('wechat_tool.mac_recovery.wechat_pid',return_value=None),patch('wechat_tool.mac_recovery.entitlements',side_effect=grants): prepared=self.recovery.prepare(plan,lambda _:None) self.assertTrue(prepared['original_unchanged']) self.assertEqual(original,{str(p.relative_to(self.app)):p.read_bytes() for p in self.app.rglob('*') if p.is_file()}) mutations=[c.args[0] for c in self.runner.call_args_list if '--force' in c.args[0]] self.assertEqual(len(mutations),1) self.assertNotIn(str(self.app),mutations[0]) with patch('wechat_tool.mac_recovery.wechat_pid',return_value=None): self.recovery.launch() self.assertEqual(self.runner.call_args.args[0],['/usr/bin/open','-n',prepared['copy']]) def test_running_wechat_or_changed_original_prevents_preparation(self): result=self.plan();plan=self.recovery.consume(result['plan_id'],True,self.account) with patch('wechat_tool.mac_recovery.wechat_pid',return_value=123): with self.assertRaisesRegex(ValueError,'退出所有微信'): self.recovery.prepare(plan,lambda _:None) (self.app/'Contents/MacOS/WeChat').write_bytes(b'changed') with patch('wechat_tool.mac_recovery.wechat_pid',return_value=None): with self.assertRaisesRegex(ValueError,'发生变化'): self.recovery.prepare(plan,lambda _:None) self.assertIsNone(self.recovery.prepared) def test_unknown_or_tampered_copy_cannot_be_launched(self): with self.assertRaisesRegex(ValueError,'先预览'): self.recovery.launch() self.recovery.prepared={'app':self.app,'fingerprint':'wrong'} with patch('wechat_tool.mac_recovery.wechat_pid',return_value=None): with self.assertRaisesRegex(ValueError,'已变化'): self.recovery.launch() def test_account_switch_and_expired_plan_are_rejected(self): result=self.plan() with self.assertRaisesRegex(ValueError,'账号发生变化'): self.recovery.consume(result['plan_id'],True,self.account/'other') result=self.plan() self.recovery.plans[result['plan_id']]['created']-=1801 with self.assertRaisesRegex(ValueError,'过期'): self.recovery.consume(result['plan_id'],True,self.account)