"""Recipient discovery regression: synthetic databases, no native calls or sends.""" import contextlib import json import os from pathlib import Path import sqlite3 import tempfile import unittest from unittest import mock import review_assistant_contacts as contacts ACCOUNT = '168800000000001' ACTIVE = {'ready': True, 'account_id': ACCOUNT, 'account_name': 'Test', 'reason': ''} class ContactDirectoryTest(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.folder = self.root / ACCOUNT self.folder.mkdir() with contextlib.closing(sqlite3.connect(self.folder / 'message.db')) as db, db: db.execute('CREATE TABLE message_table(conversation_id TEXT,send_time REAL,content TEXT)') db.executemany('INSERT INTO message_table VALUES(?,?,?)', [ ('M:101', 1700000000, 'MESSAGE_BODY_NOT_READ'), ('S:1_2', 1700000010, 'internal'), ('R:10', 1700000020, 'group')]) self.write_user('''CREATE TABLE user_table(id TEXT,name TEXT,real_name TEXT,account TEXT); INSERT INTO user_table VALUES('168800000000001','托管账号','',''); CREATE TABLE wechat_contactV1(wxid TEXT,gene INTEGER,name TEXT); INSERT INTO wechat_contactV1 VALUES('wxid_one',101,'同名助理'); INSERT INTO wechat_contactV1 VALUES('wxid_two',999,'同名助理'); CREATE TABLE wx_friend(user_id TEXT,nick_name TEXT,wx_id TEXT); CREATE TABLE external_user_relation_v3(user_id TEXT,remarks TEXT,real_remarks TEXT);''') self.patch = mock.patch.object(contacts, 'active_context', return_value=ACTIVE) self.identity = self.patch.start() self.addCleanup(self.patch.stop) contacts._CACHE.clear() def write_user(self, sql): with contextlib.closing(sqlite3.connect(self.folder / 'user.db')) as db, db: db.executescript(sql) def listing(self, *args, **kwargs): return contacts.list_recipients(ACCOUNT, *args, cache_roots=[self.root], **kwargs) def test_directory_contacts_are_visible_but_never_fabricate_send_targets(self): result = self.listing() self.assertTrue(result['ready']) self.assertEqual((result['total'], result['selectable_total'], result['contact_total']), (3, 1, 2)) self.assertEqual({i['id'] for i in result['items'] if i['selectable']}, {'M:101'}) self.assertNotIn('M:999', {i['id'] for i in result['items']}) self.assertNotIn('MESSAGE_BODY_NOT_READ', json.dumps(result)) for item in result['items']: if not item['selectable']: self.assertIn('刷新消息库', item['unavailableReason']) self.assertFalse(contacts.validate_recipient(ACCOUNT,item['id'],cache_roots=[self.root])['ok']) def test_directory_search_and_pagination_keep_same_names_separate(self): result = self.listing('同名助理', limit=1) self.assertEqual(result['total'],2) self.assertTrue(result['has_more']) second = self.listing('同名助理',limit=1,offset=1) self.assertNotEqual(result['items'][0]['id'],second['items'][0]['id']) self.assertEqual(self.listing('wxid_two')['items'][0]['id'],'contact:wxid_two') def test_explicit_unique_friend_relation_supplies_name_and_search_aliases(self): self.write_user("INSERT INTO wx_friend VALUES('101','助理备注','wxid_one');") result = self.listing() self.assertEqual(result['total'],2) target = next(i for i in result['items'] if i['id']=='M:101') self.assertEqual(target['name'],'助理备注') self.assertTrue(target['selectable']) self.assertEqual(self.listing('wxid_one')['items'][0]['id'],'M:101') self.assertTrue(contacts.validate_recipient(ACCOUNT,'M:101',cache_roots=[self.root])['ok']) def test_ambiguous_friend_mapping_does_not_merge_contacts(self): for rows in ["('101','','wxid_one'),('202','','wxid_one')", "('101','','wxid_one'),('101','','wxid_two')"]: with self.subTest(rows=rows): self.write_user('DELETE FROM wx_friend;INSERT INTO wx_friend VALUES'+rows+';') contacts._CACHE.clear() result=self.listing() self.assertEqual(result['contact_total'],2) self.assertEqual(next(i for i in result['items'] if i['id']=='M:101')['name'],'微信联系人') def test_friend_relation_without_existing_message_does_not_enable_sending(self): self.write_user("INSERT INTO wx_friend VALUES('999','助理','wxid_two');") result=self.listing('wxid_two') self.assertFalse(result['items'][0]['selectable']) self.assertFalse(contacts.validate_recipient(ACCOUNT,'M:999',cache_roots=[self.root])['ok']) def test_remark_and_nickname_both_searchable_for_exact_existing_uid(self): self.write_user("INSERT INTO user_table VALUES('101','助理昵称','','');INSERT INTO external_user_relation_v3 VALUES('101','助理备注','');") for query in ['助理昵称','助理备注','M:101']: self.assertEqual(self.listing(query)['items'][0]['id'],'M:101') self.assertEqual(self.listing('助理备注')['items'][0]['name'],'助理备注') def test_legacy_database_without_addressbook_tables_is_supported(self): self.write_user('DROP TABLE wechat_contactV1;DROP TABLE wx_friend;DROP TABLE external_user_relation_v3;') self.assertEqual(self.listing()['total'],1) def test_refresh_invalidates_cache_after_database_change(self): self.assertEqual(self.listing()['total'],3) self.write_user("INSERT INTO wechat_contactV1 VALUES('wxid_new',123,'新助理');") self.assertEqual(self.listing('新助理')['total'],1) def test_latest_wal_revision_wins_between_account_caches(self): other=self.root/'other'/ACCOUNT other.mkdir(parents=True) from shutil import copyfile copyfile(self.folder/'message.db',other/'message.db') copyfile(self.folder/'user.db',other/'user.db') os.utime(self.folder/'user.db',ns=(100,100)) os.utime(other/'user.db',ns=(200,200)) wal=Path(str(self.folder/'user.db')+'-wal') wal.write_bytes(b'file-time-only') os.utime(wal,ns=(300,300)) self.assertEqual(contacts._databases(ACCOUNT,[self.root,self.root/'other'])['user.db'],self.folder/'user.db') def test_default_roots_include_protocol_cache_before_archived_copy(self): with mock.patch.object(contacts,'application_data_dir',return_value=self.root): self.assertEqual(contacts._roots()[0],self.root/'wxwork_reply_cache') def test_other_account_directory_is_never_read(self): self.identity.return_value={**ACTIVE,'account_id':'168800000000002'} with mock.patch.object(contacts,'_contacts') as read: self.assertFalse(self.listing()['ready']) read.assert_not_called() def test_lookup_does_not_change_database_files(self): before={p.name:p.read_bytes() for p in self.folder.glob('*.db')} self.listing(); contacts.validate_recipient(ACCOUNT,'M:101',cache_roots=[self.root]) self.assertEqual(before,{p.name:p.read_bytes() for p in self.folder.glob('*.db')}) if __name__=='__main__': unittest.main()