Prepare documented WhatsApp injector with portable Python bundle
This commit is contained in:
commit
d2b621bce7
49 files changed
+5737
No files matched your search
@@ -0,0 +1,62 @@
|
||||
// Fixture tests for pagination, decryption errors, and plaintext redaction.
|
||||
const assert = require('node:assert/strict');
|
||||
const vm = require('node:vm');
|
||||
const fs = require('node:fs');
|
||||
const probe = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const rows = ['a','b','c','d','e'].map(id => ({id, body:'text-'+id, type:'chat', t:123, invisible:id==='b'}));
|
||||
const events = [];
|
||||
function req(result) {
|
||||
const r = {};
|
||||
queueMicrotask(() => {r.result=result; r.onsuccess();});
|
||||
return r;
|
||||
}
|
||||
const store = {
|
||||
count:()=>req(rows.length),
|
||||
openKeyCursor(range,direction) {
|
||||
const selected=rows.filter(r=>!range || ((range.lower===undefined || r.id>range.lower) && r.id<=range.upper));
|
||||
if(direction==='prev')selected.reverse();
|
||||
const request={};let index=0;
|
||||
function next(){queueMicrotask(()=>{
|
||||
request.result=index<selected.length?{primaryKey:selected[index++].id,continue:next}:null;
|
||||
request.onsuccess();
|
||||
});}
|
||||
next();return request;
|
||||
}
|
||||
};
|
||||
const db={objectStoreNames:{contains:n=>n==='message'},transaction:(name,mode)=>{
|
||||
assert.equal(name,'message');assert.equal(mode,'readonly');return {objectStore:()=>store};
|
||||
},close:()=>events.push('close')};
|
||||
const modules={
|
||||
WAWebSchemaMessage:{getMessageTable:()=>({get:async key=>{
|
||||
events.push('decrypt:'+key);
|
||||
if(key==='c')throw Error('simulated unavailable decryption key');
|
||||
return {...rows.find(r=>r.id===key)};
|
||||
}})},
|
||||
WAWebDBMessageSerialization:{messageFromDbRow:row=>{
|
||||
events.push('decode:'+row.id);
|
||||
return {...row,id:{id:row.id,remote:'test-chat',fromMe:false}};
|
||||
}}
|
||||
};
|
||||
async function run(options){return vm.runInNewContext(probe.replace('__OPTIONS__',JSON.stringify(options)),{
|
||||
location:{origin:'https://web.whatsapp.com'}, require:n=>modules[n],
|
||||
indexedDB:{databases:async()=>[{name:'model-storage'}],open:()=>req(db)},
|
||||
IDBKeyRange:{upperBound:upper=>({upper}),bound:(lower,upper,openLower,openUpper)=>{
|
||||
assert.equal(openLower,true);assert.equal(openUpper,false);return {lower,upper};
|
||||
}}
|
||||
});}
|
||||
(async()=>{
|
||||
const bounds=await run({mode:'bounds'});assert.equal(bounds.count,5);assert.equal(bounds.upper,'e');
|
||||
let after=null,all=[];
|
||||
while(after!=='e'){
|
||||
const page=await run({mode:'batch',upper:'e',after,batch:2,redact:false});
|
||||
assert.ok(page.keys.length<=2);all.push(...page.messages);after=page.last;
|
||||
}
|
||||
assert.deepEqual(all.map(m=>m.id),['a','b','c','d','e']); // UI-invisible b remains included.
|
||||
assert.equal(all[2].error,'row_decryption_or_decoding_failed');
|
||||
assert.equal(all[1].body,'text-b');
|
||||
const redacted=await run({mode:'batch',upper:'b',after:null,batch:2,redact:true});
|
||||
assert.ok(redacted.messages.every(m=>!('body' in m)&&!('caption' in m)));
|
||||
assert.equal(redacted.messages[0].body_length,6);
|
||||
assert.ok(events.indexOf('decrypt:a')<events.indexOf('decode:a'));
|
||||
console.log('PASS: bounded pagination, upper bound, invisible rows, row errors, decryption order, redaction');
|
||||
})().catch(error=>{console.error(error);process.exitCode=1;});
|
||||
@@ -0,0 +1,41 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const vm = require('node:vm');
|
||||
const fs = require('node:fs');
|
||||
const probe = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const rows = [90, 20, 60, 10, 70, 30, 40, 0].map((t, i) => ({id: String(i), t}));
|
||||
const reads = [];
|
||||
let closed = false;
|
||||
const store = {openCursor() {
|
||||
const request = {}; let index = 0;
|
||||
function advance() {queueMicrotask(() => {
|
||||
const row = rows[index++];
|
||||
request.result = row ? {primaryKey: row.id, value: row, continue: advance} : null;
|
||||
request.onsuccess();
|
||||
});}
|
||||
advance(); return request;
|
||||
}};
|
||||
const db = {transaction(name, mode) {
|
||||
assert.equal(name, 'message'); assert.equal(mode, 'readonly');
|
||||
return {objectStore: () => store};
|
||||
}, close: () => {closed = true;}};
|
||||
const modules = {
|
||||
WAWebSchemaMessage: {getMessageTable: () => ({get: async key => {
|
||||
reads.push(key); return rows.find(r => r.id === key);
|
||||
}})},
|
||||
WAWebDBMessageSerialization: {messageFromDbRow: row => ({...row, type: 'chat', body: 'fixture-' + row.id})},
|
||||
};
|
||||
(async () => {
|
||||
const result = await vm.runInNewContext(probe, {
|
||||
location: {origin: 'https://web.whatsapp.com'}, require: n => modules[n],
|
||||
indexedDB: {databases: async () => [{name: 'model-storage'}], open: () => {
|
||||
const request = {}; queueMicrotask(() => {request.result = db; request.onsuccess();}); return request;
|
||||
}},
|
||||
});
|
||||
assert.deepEqual(Array.from(result.messages, m => m.timestamp), [10, 20, 30, 40, 60]);
|
||||
assert.equal(result.scanned, 8);
|
||||
assert.equal(reads.length, 5);
|
||||
assert.ok(result.messages.every(m => m.body.startsWith('fixture-')));
|
||||
assert.equal(result.cdp, false);
|
||||
assert.equal(closed, true);
|
||||
console.log('PASS: oldest selection, bounded app decryption, decoded text, readonly transaction, cleanup');
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
@@ -0,0 +1,36 @@
|
||||
const assert=require('node:assert/strict');
|
||||
const vm=require('node:vm');
|
||||
const fs=require('node:fs');
|
||||
const probes=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
|
||||
const me={toString:()=> 'self@lid'};
|
||||
const other={toString:()=> 'other@lid'};
|
||||
let sends=0;
|
||||
const data={id:{remote:me,toString:()=> 'self-message'},body:'fixture'};
|
||||
const chat={id:me,msgs:{get:()=>({ack:3})}};
|
||||
const modules={
|
||||
WAWebUserPrefsMeUser:{getMaybeMeLidUser:()=>me,isMeAccount:v=>v===me},
|
||||
WAWebFindChatAction:{findOrCreateLatestChat:async()=>({chat})},
|
||||
WAWebSendTextMsgChatAction:{createTextMsgData:async()=>data,addAndSendTextMsg:async()=>{sends++;}},
|
||||
WAWebSchemaMessage:{getMessageTable:()=>({get:async()=>({body:'fixture',t:123})})},
|
||||
WAWebDBMessageSerialization:{messageFromDbRow:v=>v},
|
||||
WAWebSchemaContact_DO_NOT_USE_DIRECTLY:{getContactTable:()=>({all:async()=>[
|
||||
{id:'a@lid',name:'Grüße 😺',phoneNumber:'123',unexpectedSecret:{value:'never export'}},
|
||||
{id:'b@lid',name:null,isBusiness:true},
|
||||
]})},
|
||||
};
|
||||
const run=code=>vm.runInNewContext(code,{require:name=>modules[name]});
|
||||
(async()=>{
|
||||
const good=await run(probes.send);
|
||||
assert.equal(sends,1);assert.equal(good.persisted,true);assert.equal(good.plaintext_matches,true);
|
||||
chat.id=other;
|
||||
await assert.rejects(run(probes.send),/Refusing to send outside own account/);
|
||||
assert.equal(sends,1);
|
||||
chat.id=me;data.id.remote=other;
|
||||
await assert.rejects(run(probes.send),/not addressed to self/);
|
||||
assert.equal(sends,1);
|
||||
const contacts=await run(probes.contacts);
|
||||
assert.equal(contacts.count,2);assert.equal(contacts.contacts[0].name,'Grüße 😺');
|
||||
assert.equal('unexpectedSecret' in contacts.contacts[0],false);
|
||||
assert.equal(contacts.cdp,false);assert.equal(contacts.ui_automation,false);
|
||||
console.log('PASS: send once, own-chat guard, own-recipient guard, plaintext readback, contact fields');
|
||||
})().catch(error=>{console.error(error);process.exitCode=1;});
|
||||
@@ -0,0 +1,291 @@
|
||||
import argparse
|
||||
import base64
|
||||
import contextlib
|
||||
import hashlib
|
||||
import importlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from whatsapp_suite import exports, history, live, qr_memory, ram_search
|
||||
from whatsapp_suite.common import BASE, asset_bytes, resource_bytes, cli_command
|
||||
from whatsapp_suite.analyzer import endpoint
|
||||
|
||||
|
||||
class SavedExportTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.folder = Path(self.temp.name)
|
||||
self.rows = [
|
||||
{'id': '1', 'chat_id': 'a', 'body': 'Cafe\u0301 STRASSE', 'type': 'chat', 'nested': {'secret': 'private'}},
|
||||
{'id': '2', 'chatId': 'b', 'caption': 'CAFÉ Straße', 'type': 'image'},
|
||||
{'id': '3', 'chat_id': 'a', 'indexed_text': 'other', 'type': 'chat'},
|
||||
]
|
||||
self.path = self.folder / 'messages.json'
|
||||
self.path.write_text(json.dumps(self.rows), encoding='utf-8')
|
||||
|
||||
def invoke(self, *args):
|
||||
out = io.StringIO()
|
||||
with contextlib.redirect_stdout(out):
|
||||
self.assertEqual(exports.main(list(args)), 0)
|
||||
return json.loads(out.getvalue())
|
||||
|
||||
def test_json_report_wrappers(self):
|
||||
for key in ('messages', 'models', 'results'):
|
||||
self.path.write_text(json.dumps({key: self.rows}), encoding='utf-8')
|
||||
self.assertEqual(list(exports.messages(self.path)), self.rows)
|
||||
|
||||
def test_jsonl_bom_and_blank_lines(self):
|
||||
path = self.folder / 'messages.jsonl'
|
||||
path.write_text('\n' + '\n\n'.join(json.dumps(r) for r in self.rows), encoding='utf-8-sig')
|
||||
self.assertEqual(list(exports.messages(path)), self.rows)
|
||||
|
||||
def test_malformed_jsonl_reports_line(self):
|
||||
path = self.folder / 'broken.jsonl'
|
||||
path.write_text('{}\ninvalid', encoding='utf-8')
|
||||
with self.assertRaisesRegex(ValueError, 'line 2'):
|
||||
list(exports.messages(path))
|
||||
|
||||
def test_nonobject_records_rejected(self):
|
||||
for data in ([None], {'messages': [42]}, 'wrong', {}):
|
||||
self.path.write_text(json.dumps(data), encoding='utf-8')
|
||||
with self.assertRaises(ValueError):
|
||||
list(exports.messages(self.path))
|
||||
|
||||
def test_unicode_search_and_total_beyond_limit(self):
|
||||
result = self.invoke('search', str(self.path), 'café straße', '--limit', '1')
|
||||
self.assertEqual((result['matches'], result['returned']), (2, 1))
|
||||
|
||||
def test_chat_filter_and_nested_redaction(self):
|
||||
result = self.invoke('search', str(self.path), 'café', '--chat-id', 'a', '--redact-text')
|
||||
self.assertEqual(result['matches'], 1)
|
||||
row = result['results'][0]
|
||||
self.assertEqual(row['id'], '1')
|
||||
self.assertNotIn('body', row)
|
||||
self.assertNotIn('nested', row)
|
||||
self.assertGreater(row['text_length'], 0)
|
||||
|
||||
def test_stats_understands_both_chat_id_fields(self):
|
||||
result = self.invoke('stats', str(self.path))
|
||||
self.assertEqual((result['messages'], result['messages_with_text'], result['chats']), (3, 3, 2))
|
||||
self.assertEqual(result['types'], {'chat': 2, 'image': 1})
|
||||
|
||||
def test_output_cannot_overwrite_input(self):
|
||||
before = self.path.read_bytes()
|
||||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
|
||||
exports.main(['stats', str(self.path), '--output', str(self.path)])
|
||||
self.assertEqual(self.path.read_bytes(), before)
|
||||
|
||||
def test_empty_query_and_invalid_limit_rejected(self):
|
||||
for tail in ([' '], ['café', '--limit', '0']):
|
||||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
|
||||
exports.main(['search', str(self.path)] + tail)
|
||||
|
||||
def test_inventory_empty_and_existing_profile(self):
|
||||
self.assertFalse(exports.inventory(self.folder / 'absent')['exists'])
|
||||
store = self.folder / 'Local Storage/leveldb'
|
||||
store.mkdir(parents=True)
|
||||
(store / 'sample.ldb').write_bytes(b'123')
|
||||
report = exports.inventory(self.folder)
|
||||
self.assertEqual(report['storage_inventory'][0]['files'], [{'name': 'sample.ldb', 'bytes': 3}])
|
||||
|
||||
|
||||
class ProbeContractTests(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which('node'), 'Node.js is needed for JavaScript fixtures')
|
||||
def test_self_message_guards_and_contact_export(self):
|
||||
from whatsapp_suite.self_message import PROBE as send
|
||||
from whatsapp_suite.contacts_export import PROBE as contacts
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
probe=Path(folder)/'probes.json'
|
||||
probe.write_text(json.dumps({'send':send.replace('__BODY__',json.dumps('fixture')),'contacts':contacts}),encoding='utf-8')
|
||||
result=subprocess.run(['node','-',str(probe)],input=resource_bytes('tests/self_and_contacts.cjs').decode('utf-8'),capture_output=True,text=True,encoding='utf-8',timeout=20)
|
||||
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||
|
||||
def test_self_send_refuses_existing_receipt_without_retry(self):
|
||||
from whatsapp_suite.self_message import send_self
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'receipt.json'
|
||||
path.write_text('original',encoding='utf-8')
|
||||
with patch('whatsapp_suite.self_message.run_math') as bridge:
|
||||
with self.assertRaises(ValueError):send_self(path)
|
||||
bridge.assert_not_called()
|
||||
self.assertEqual(path.read_text(),'original')
|
||||
|
||||
@unittest.skipUnless(shutil.which('node'), 'Node.js is needed for JavaScript fixtures')
|
||||
def test_oldest_history_read_without_cdp(self):
|
||||
from whatsapp_suite.history_read import PROBE
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
probe = Path(folder) / 'probe.json'
|
||||
probe.write_text(json.dumps(PROBE), encoding='utf-8')
|
||||
result = subprocess.run(['node', '-', str(probe)], input=resource_bytes('tests/history_read.cjs').decode('utf-8'), capture_output=True, text=True, encoding='utf-8', timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_ram_search_requires_verified_membership(self):
|
||||
rows = [{'body': 'café', 'chat_id': 'a', 'in_message_collection': valid} for valid in (True, False)]
|
||||
result = ram_search.search_models(rows, 'CAFE\u0301', chat_id='a', redact=True)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertNotIn('body', result[0])
|
||||
self.assertEqual(result[0]['body_length'], 4)
|
||||
self.assertEqual(ram_search.search_models(rows, 'café', chat_id='b'), [])
|
||||
with self.assertRaises(ValueError):
|
||||
ram_search.search_models(rows, ' ')
|
||||
|
||||
def test_loopback_endpoint_validation(self):
|
||||
self.assertEqual(endpoint('http://127.0.0.1:9223/'), 'http://127.0.0.1:9223')
|
||||
self.assertEqual(endpoint('http://[::1]:9223'), 'http://[::1]:9223')
|
||||
for url in ('https://example.com', 'file:///tmp/x', 'http://localhost:9223', 'http://user:[email protected]', 'http://127.0.0.1?x=1'):
|
||||
with self.assertRaises(argparse.ArgumentTypeError):
|
||||
endpoint(url)
|
||||
|
||||
def test_native_assets_match_verified_hashes(self):
|
||||
from whatsapp_suite.native_search import EMBEDDED_PROBE_SHA256
|
||||
for filename, expected in (('math_probe.dll', live.EMBEDDED_SHA256), ('search_probe.dll', EMBEDDED_PROBE_SHA256)):
|
||||
payload = asset_bytes(filename)
|
||||
self.assertEqual(payload[:2], b'MZ')
|
||||
self.assertEqual(hashlib.sha256(payload).hexdigest(), expected)
|
||||
|
||||
def test_math_response_binding_and_float_tolerance(self):
|
||||
data = {'probe_id': 'test', 'cases': [{'expression': expression, 'value': expected} for expression, expected in live.CASES]}
|
||||
self.assertTrue(all(row['passed'] for row in live.validate_result(json.dumps(data), 'test')))
|
||||
with self.assertRaises(ValueError):
|
||||
live.validate_result(json.dumps(data), 'other')
|
||||
data['cases'][0]['value'] = -1
|
||||
self.assertFalse(live.validate_result(json.dumps(data), 'test')[0]['passed'])
|
||||
|
||||
def test_capability_response_must_match_fixed_checks(self):
|
||||
data = {'probe_id': 'test', 'state': 'done', 'checks': [{'id': spec[0]} for spec in live.CAPABILITIES]}
|
||||
self.assertEqual(live.validate_capabilities(data, 'test'), data['checks'])
|
||||
data['checks'].reverse()
|
||||
with self.assertRaises(ValueError):
|
||||
live.validate_capabilities(data, 'test')
|
||||
|
||||
def test_pairing_payload_shape(self):
|
||||
key = base64.b64encode(bytes(range(32))).decode()
|
||||
value = ','.join(['reference123', key, key, key, '8'])
|
||||
self.assertTrue(qr_memory.valid_payload(value))
|
||||
self.assertTrue(qr_memory.valid_payload('https://wa.me/settings/linked_devices#' + value))
|
||||
self.assertFalse(qr_memory.valid_payload(value.replace(',8', ',7')))
|
||||
self.assertFalse(qr_memory.valid_payload('reference,invalid,invalid,invalid,8'))
|
||||
|
||||
def test_history_refuses_existing_export(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'history.jsonl'
|
||||
path.write_text('original', encoding='utf-8')
|
||||
with self.assertRaisesRegex(RuntimeError, 'already exists'):
|
||||
history.export(None, path)
|
||||
self.assertEqual(path.read_text(), 'original')
|
||||
|
||||
def test_desktop_search_escapes_query_and_enforces_bounds(self):
|
||||
from whatsapp_suite import native_search
|
||||
query = '"; window.unwanted = true; //'
|
||||
with patch.object(native_search, 'native_webview_test', return_value=0) as backend:
|
||||
self.assertEqual(native_search.run_live_probe(None, Path('unused.json'), query, 10), 0)
|
||||
config = backend.call_args.args[2]
|
||||
self.assertIn('query=' + json.dumps(query), config['start'])
|
||||
self.assertEqual(config['mode'], 'search')
|
||||
for query, limit in (('', 10), ('valid', 0), ('valid', 51), ('x' * 201, 10)):
|
||||
with self.assertRaises(ValueError):
|
||||
native_search.run_live_probe(None, Path('unused.json'), query, limit)
|
||||
|
||||
@unittest.skipUnless(shutil.which('node'), 'Node.js is needed for the JavaScript history fixtures')
|
||||
def test_history_javascript_pagination_decryption_redaction(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
probe = Path(folder) / 'probe.json'
|
||||
probe.write_text(json.dumps(history.PROBE), encoding='utf-8')
|
||||
result = subprocess.run(['node', '-', str(probe)], input=resource_bytes('tests/history_export.cjs').decode('utf-8'), capture_output=True, text=True, encoding='utf-8', timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
class CommandTests(unittest.TestCase):
|
||||
def test_all_commands_have_help(self):
|
||||
from whatsapp_suite.cli import COMMANDS
|
||||
for command in ['test'] + list(COMMANDS):
|
||||
with self.subTest(command=command):
|
||||
result = subprocess.run(cli_command(command, '--help'), cwd=BASE, capture_output=True, text=True, timeout=15)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('usage:', result.stdout)
|
||||
|
||||
def test_saved_search_dispatch(self):
|
||||
from whatsapp_suite.cli import main
|
||||
with patch('whatsapp_suite.exports.main', return_value=0) as target:
|
||||
self.assertEqual(main(['search', 'export.json', 'keyword']), 0)
|
||||
target.assert_called_once_with(['search', 'export.json', 'keyword'])
|
||||
|
||||
def test_live_probe_is_explicit(self):
|
||||
from whatsapp_suite.cli import main
|
||||
with patch('whatsapp_suite.runner.main', return_value=0) as target:
|
||||
self.assertEqual(main([]), 0)
|
||||
target.assert_called_once_with([])
|
||||
|
||||
|
||||
class ZipExportTests(unittest.TestCase):
|
||||
def test_pagination_zip_unicode_and_row_errors(self):
|
||||
from whatsapp_suite.zip_export import write_messages
|
||||
pages = iter([
|
||||
{'count': 3, 'upper': 'c'},
|
||||
{'keys': ['a', 'b'], 'last': 'b', 'messages': [{'id': 'a', 'body': 'Grüße 😺'}, {'id': 'b', 'error': 'decode_failed'}]},
|
||||
{'keys': ['c'], 'last': 'c', 'messages': [{'id': 'c', 'body': 'old message'}]},
|
||||
{'count': 3, 'upper': 'c'},
|
||||
])
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'test.zip'
|
||||
with zipfile.ZipFile(path, 'w') as archive, contextlib.redirect_stdout(io.StringIO()):
|
||||
result = write_messages(lambda _: next(pages), archive, 2, False)
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
self.assertIsNone(archive.testzip())
|
||||
rows = [json.loads(line) for line in archive.read('messages.jsonl').decode('utf-8').splitlines()]
|
||||
self.assertEqual(rows[0]['body'], 'Grüße 😺')
|
||||
self.assertEqual((result['rows'], result['row_errors']), (3, 1))
|
||||
self.assertTrue(result['reached_initial_upper'])
|
||||
|
||||
def test_duplicate_rows_fail(self):
|
||||
from whatsapp_suite.zip_export import write_messages
|
||||
pages = iter([{'count': 2, 'upper': 'b'}, {'keys': ['a', 'a'], 'last': 'a', 'messages': [{'id': 'a'}, {'id': 'a'}]}])
|
||||
with tempfile.TemporaryDirectory() as folder, zipfile.ZipFile(Path(folder) / 'test.zip', 'w') as archive:
|
||||
with self.assertRaisesRegex(RuntimeError, 'Duplicate'):
|
||||
write_messages(lambda _: next(pages), archive, 2, False)
|
||||
|
||||
def test_existing_output_preserved(self):
|
||||
from whatsapp_suite.zip_export import export_zip
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'test.zip'
|
||||
path.write_bytes(b'original')
|
||||
with self.assertRaises(ValueError):
|
||||
export_zip(path)
|
||||
self.assertEqual(path.read_bytes(), b'original')
|
||||
|
||||
def test_empty_database(self):
|
||||
from whatsapp_suite.zip_export import write_messages
|
||||
with tempfile.TemporaryDirectory() as folder, zipfile.ZipFile(Path(folder) / 'test.zip', 'w') as archive:
|
||||
result = write_messages(lambda _: {'count': 0, 'upper': None}, archive, 100, False)
|
||||
self.assertEqual(result['rows'], 0)
|
||||
self.assertTrue(result['reached_initial_upper'])
|
||||
|
||||
|
||||
class NativeCacheTests(unittest.TestCase):
|
||||
@unittest.skipUnless(sys.platform == 'win32', 'Native helper cache requires Windows')
|
||||
def test_verified_helper_reuses_and_repairs_cache(self):
|
||||
from whatsapp_suite import common
|
||||
payload=b'MZ synthetic fixture, never loaded'
|
||||
digest=hashlib.sha256(payload).hexdigest()
|
||||
with tempfile.TemporaryDirectory() as folder, patch.object(common,'BASE',Path(folder)), patch.object(common,'asset_bytes',return_value=payload):
|
||||
path,status=common.verified_helper('fixture.dll',digest,'cache','helper.dll')
|
||||
self.assertEqual(status,'extracted')
|
||||
self.assertEqual(common.verified_helper('fixture.dll',digest,'cache','helper.dll')[1],'reused')
|
||||
path.write_bytes(b'tampered')
|
||||
self.assertEqual(common.verified_helper('fixture.dll',digest,'cache','helper.dll')[1],'repaired')
|
||||
self.assertEqual(path.read_bytes(),payload)
|
||||
with self.assertRaisesRegex(ValueError,'SHA-256'):
|
||||
common.verified_helper('fixture.dll','0'*64,'cache','helper.dll')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user