diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 16:29:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 16:29:20 +0200 |
| commit | 7957fa686eb6e8cf57bcb62ff4e6d2785693030b (patch) | |
| tree | 562195088fa40bc4db85193956bdd8aaf00e31f5 /packages/meshbay-hub/tests | |
| parent | 24547e4efd3a45b36b3bea8c29cfc687230a6d3a (diff) | |
| download | meshbay-7957fa686eb6e8cf57bcb62ff4e6d2785693030b.tar.gz | |
feat(android): back up WhatsApp's own chat backups, and its media if asked
A WhatsApp section takes WhatsApp's folder through the picker, sends its
encrypted Databases and Backups (not the dated copies of earlier weeks) into
<folder>/<account>-whatsapp, and names the restore set in the manifest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/photo_sync_probe.py | 13 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_android_shell.py | 13 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_drive_sync.py | 26 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_first_load_is_lean.py | 2 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_hook_ordering.py | 2 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_photo_sync_ui.py | 8 |
6 files changed, 55 insertions, 9 deletions
diff --git a/packages/meshbay-hub/tests/harness/photo_sync_probe.py b/packages/meshbay-hub/tests/harness/photo_sync_probe.py index 2a75749..3fcce8a 100644 --- a/packages/meshbay-hub/tests/harness/photo_sync_probe.py +++ b/packages/meshbay-hub/tests/harness/photo_sync_probe.py @@ -118,6 +118,17 @@ window.meshbay = { sent: async () => true, completed: async () => ({}), failed: async () => true, keepAlive: async () => true, }, + whatsappSync: { + status: async () => ({ + on: MODE === 'configured', unmetered: true, destination: dest, options: { media: false }, + dir: dest && '__FOLDER__/bob-whatsapp', sent: 4, lastCompleted: Date.now(), failure: null, + folders: [{ id: 'wa', name: 'WhatsApp' }], + }), + chooseFolder: async () => ({}), forgetFolder: async () => ({}), configure: async () => ({}), + plan: async () => ({ items: [] }), sent: async () => true, manifest: async () => ({ item: null }), + manifestSent: async () => true, completed: async () => ({}), failed: async () => true, + keepAlive: async () => true, + }, messageSync: { status: async () => ({ permission: 'granted', on: MODE === 'configured', destination: dest, @@ -177,7 +188,7 @@ setTimeout(() => { .map((e) => `${e.tagName}.${e.className} ${Math.round(e.getBoundingClientRect().right)}`); out[f.id] = { vw, scrollW: d.documentElement.scrollWidth, wide: wide.slice(0, 6), - text: d.querySelector('#root').innerText.slice(0, 3000), + text: d.querySelector('#root').innerText.slice(0, 8000), err: d.documentElement.dataset.err || '', checked: [...d.querySelectorAll('#root input:checked')] .map((i) => i.closest('label').innerText.split('\\n')[0]), diff --git a/packages/meshbay-hub/tests/test_android_shell.py b/packages/meshbay-hub/tests/test_android_shell.py index 9275d67..001f6d9 100644 --- a/packages/meshbay-hub/tests/test_android_shell.py +++ b/packages/meshbay-hub/tests/test_android_shell.py @@ -136,20 +136,24 @@ def test_the_shim_offers_desktop_channels_and_native_answers_each(): native = set() for path in (SRC / "bridge" / "Channels.kt", SRC / "bridge" / "KeyChannels.kt", SRC / "cast" / "CastChannels.kt", SRC / "notify" / "PushChannels.kt", - SRC / "photos" / "PhotoChannels.kt", SRC / "phonesync" / "Destination.kt", - SRC / "drive" / "DriveChannels.kt"): + SRC / "photos" / "PhotoChannels.kt", SRC / "phonesync" / "Destination.kt"): native |= set(re.findall(r'^\s*"([\w:-]+)" ->', _read(path), flags=re.M)) # Contacts and messages share one class, its channels named by a prefix. docs = set(re.findall(r'^\s*"(\w+)" ->', _read(SRC / "phonesync" / "DocChannels.kt"), flags=re.M)) for prefix in re.findall(r'DocChannels\("(\w+)"', _read(SRC / "MainActivity.kt")): native |= {f"{prefix}:{name}" for name in docs} + # The chosen-folder backups too, their prefixes in DriveKind.kt. + drive = set(re.findall(r'^\s*"([\w-]+)" ->', _read(SRC / "drive" / "DriveChannels.kt"), flags=re.M)) + for prefix in re.findall(r'prefix = "(\w+)"', _read(SRC / "drive" / "DriveKind.kt")): + native |= {f"{prefix}:{name}" for name in drive} shim = _shim_channels() assert shim, "no channel found in the shim" # A phone has a push distributor to talk to and a camera roll and an # address book to back up, and a desktop has none of them; those families are the ones the desktop lacks # rather than the ones it shares. phone_only = {c for c in shim if c.startswith(("push:", "photosync:", "phonesync:", "contactsync:", - "calendarsync:", "messagesync:", "drivesync:"))} + "calendarsync:", "messagesync:", "drivesync:", + "wasync:"))} assert any(c.startswith("push:") for c in phone_only), "the push channels are gone" assert any(c.startswith("photosync:") for c in phone_only), "the backup channels are gone" assert shim - phone_only <= preload, f"channels the desktop does not have: {shim - preload}" @@ -268,7 +272,8 @@ def test_only_the_personal_profile_is_backed_up(): assert "Profiles.isPersonal(this)" in activity and "const BACKUP = $backups;" in activity shim = _strip_js_comments(_read(SHIM)) gated = shim.split("...(BACKUP ? {", 1)[1].split("} : {}),", 1)[0] - for name in ("photoSync:", "phoneSync:", "contactSync:", "calendarSync:", "driveSync:"): + for name in ("photoSync:", "phoneSync:", "contactSync:", "calendarSync:", "driveSync:", + "whatsappSync:"): assert name in gated, name for path in [*(SRC / "phonesync").rglob("*.kt"), *(SRC / "photos").rglob("*.kt"), *(SRC / "drive").rglob("*.kt"), diff --git a/packages/meshbay-hub/tests/test_drive_sync.py b/packages/meshbay-hub/tests/test_drive_sync.py index 7fc35d2..342b310 100644 --- a/packages/meshbay-hub/tests/test_drive_sync.py +++ b/packages/meshbay-hub/tests/test_drive_sync.py @@ -73,7 +73,19 @@ fake.transport = { }, }; const hubFetch = async () => ({ admin_id: 'u-bob', members: fake.members, invited: [] }); -win.meshbay = { driveSync: { +fake.wa = { on: false }; +win.meshbay = { whatsappSync: { + status: async () => ({ ...fake.status, on: fake.wa.on, dir: 'Backups/bob-whatsapp', + folders: [{ id: 'wa', name: 'WhatsApp' }] }), + plan: async () => ({ items: [{ token: 'w1', name: 'msgstore.db.crypt14', + dir: 'Backups/bob-whatsapp/Databases', size: 8, edited: false }], + manifest: false }), + sent: async (token, dir, name) => { fake.log.push(['sent', token, dir, name]); return true; }, + manifest: async () => ({ item: null }), manifestSent: async () => true, + completed: async () => { fake.log.push(['wa-completed']); return fake.status; }, + failed: async (code) => { fake.log.push(['failed', code]); return true; }, + keepAlive: async () => true, +}, driveSync: { status: async () => fake.status, plan: async () => { fake.log.push(['plan']); return { items: fake.items, manifest: false }; }, manifest: async () => ({ item: null }), @@ -148,6 +160,18 @@ def test_a_lasting_refusal_names_the_files_backup(tmp_path): assert ["completed"] not in log +def test_whatsapps_chats_go_into_their_own_folder_on_their_own_runner(tmp_path): + log = _run(""" +fake.wa.on = true; +fake.items = []; +m.startDriveSync(ctx); +await m.runWhatsAppSync(); +""", tmp_path) + assert ["upload", "Backups/bob-whatsapp/Databases", "msgstore.db.crypt14"] in log + assert ["fetch", "/wasync/w1", "bytes=0-7"] in log + assert ["wa-completed"] in log + + def test_the_backup_has_no_way_to_remove_anything_on_the_node(): for path in (SYNC, STATIC / "photo-sync.js"): src = path.read_text(encoding="utf-8") diff --git a/packages/meshbay-hub/tests/test_first_load_is_lean.py b/packages/meshbay-hub/tests/test_first_load_is_lean.py index 854d755..2c191a5 100644 --- a/packages/meshbay-hub/tests/test_first_load_is_lean.py +++ b/packages/meshbay-hub/tests/test_first_load_is_lean.py @@ -27,7 +27,7 @@ ON_DEMAND = { # The Android application's backups: no browser or desktop has them. "photo-sync.js", "photo-sync-settings.js", "android-sync-page.js", "phone-sync.js", "phone-sync-settings.js", "sync-destination.js", - "drive-sync.js", "drive-sync-settings.js", + "drive-sync.js", "drive-sync-settings.js", "whatsapp-sync-settings.js", } diff --git a/packages/meshbay-hub/tests/test_hook_ordering.py b/packages/meshbay-hub/tests/test_hook_ordering.py index 3a01e53..33408bb 100644 --- a/packages/meshbay-hub/tests/test_hook_ordering.py +++ b/packages/meshbay-hub/tests/test_hook_ordering.py @@ -56,7 +56,7 @@ STATIC_FILES = [ "ask.js", # The Android application's backups, on their own page loaded on demand. "settings-page.js", "android-sync-page.js", "photo-sync-settings.js", - "phone-sync-settings.js", "drive-sync-settings.js", + "phone-sync-settings.js", "drive-sync-settings.js", "whatsapp-sync-settings.js", ] pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailable") diff --git a/packages/meshbay-hub/tests/test_photo_sync_ui.py b/packages/meshbay-hub/tests/test_photo_sync_ui.py index b3ab59e..da0ceeb 100644 --- a/packages/meshbay-hub/tests/test_photo_sync_ui.py +++ b/packages/meshbay-hub/tests/test_photo_sync_ui.py @@ -57,7 +57,7 @@ def test_limited_access_is_said_rather_than_failing_quietly(probe): def test_where_backups_go_lists_each_kinds_folder(probe): text = probe["dest360"]["text"] - assert all(f"Media/bob-{k}" in text for k in ("photos", "contacts", "calendar", "messages", "drive")) + assert all(f"Media/bob-{k}" in text for k in ("photos", "contacts", "calendar", "messages", "drive", "whatsapp")) def test_photos_the_photos_tab_will_not_show_are_said(probe): @@ -85,3 +85,9 @@ def test_the_files_backup_lists_its_folders_and_what_it_leaves_out(probe): text = probe["configured360"]["text"] assert "Documents" in text and "Scans_from_the_office_printer" in text assert "DCIM, Pictures and Movies cannot be chosen" in text + + +def test_the_whatsapp_backup_says_how_to_restore(probe): + text = probe["configured360"]["text"] + assert "WhatsApp's folder is chosen" in text + assert "To restore on a new phone" in text |