aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_drive_sync.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_drive_sync.py')
-rw-r--r--packages/meshbay-hub/tests/test_drive_sync.py180
1 files changed, 180 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_drive_sync.py b/packages/meshbay-hub/tests/test_drive_sync.py
new file mode 100644
index 0000000..3861585
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_drive_sync.py
@@ -0,0 +1,180 @@
+"""
+The phone's files backup, as the page runs it (docs/MESHBAY_DESIGN.md §9.13).
+
+Run under Node against the real `drive-sync.js`, which is the photo backup's
+own runner (`createBackup` in photo-sync.js) over the files bridge: the same
+promises hold, once a day, on an unmetered network, each file under a slot
+and read from the phone a range at a time, nothing removed from the node,
+and only into a group the account owns alone.
+"""
+
+import json
+import re
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+SYNC = STATIC / "drive-sync.js"
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("node") is None or not SYNC.exists(),
+ reason="node or the SPA sources are not available")
+
+FAKE_POOL = r"""
+export class ConnectionPool {
+ async connect(groupId) {
+ globalThis.fake.log.push(['connect', groupId]);
+ return { transport: globalThis.fake.transport, ack: {} };
+ }
+ closeAll() { globalThis.fake.log.push(['close']); }
+}
+"""
+
+PRELUDE = r"""
+const fake = globalThis.fake = { log: [], uploadError: null, members: [{ user_id: 'u-bob' }] };
+const win = new EventTarget();
+const doc = new EventTarget();
+doc.visibilityState = 'visible';
+globalThis.window = win;
+globalThis.document = doc;
+globalThis.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} };
+globalThis.fetch = async (url) => {
+ const [path, query] = url.split('?');
+ const m = /range=(\d+)-(\d+)/.exec(query || '');
+ fake.log.push(['fetch', path, m ? `bytes=${m[1]}-${m[2]}` : null]);
+ return { ok: true, status: 200,
+ arrayBuffer: async () => new ArrayBuffer(m ? Number(m[2]) - Number(m[1]) + 1 : 0) };
+};
+fake.status = {
+ on: true, unmetered: true, sent: 0, now: Date.now(),
+ destination: { account: 'bob', groupId: 'g1', groupName: 'Mine', folder: 'Backups' },
+ dir: 'Backups/bob-drive', folders: [{ id: 'tree1', name: 'Documents' }],
+ lastCompleted: null, failure: null, failureAt: null,
+};
+fake.items = [
+ { token: 'd1', name: 'notes.txt', dir: 'Backups/bob-drive/Documents', size: 6, edited: false },
+ { token: 'd2', name: 'tax.pdf', dir: 'Backups/bob-drive/Documents/Tax/2026', size: 12, edited: false },
+];
+fake.index = { roots: [{ name: 'Backups', writable: true, available: true }], dirs: [], entries: [] };
+fake.transport = {
+ async fetchIndex() { return fake.index; },
+ async createDirectory(at, name) { fake.log.push(['mkdir', `${at}/${name}`]); return {}; },
+ openTransfer({ bytes }) {
+ return { tr: 'tr-' + bytes, acquire: async () => {}, release: () => fake.log.push(['release', bytes]) };
+ },
+ async uploadFile(file, opts) {
+ fake.log.push(['upload', opts.dir, file.name]);
+ if (fake.uploadError) { const e = new Error('refused'); e.code = fake.uploadError; throw e; }
+ await file.slice(0, file.size).arrayBuffer();
+ return { stored_as: file.name, dir: opts.dir };
+ },
+};
+const hubFetch = async () => ({ admin_id: 'u-bob', members: fake.members, invited: [] });
+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 }),
+ manifestSent: async () => true,
+ sent: async (token, dir, name) => { fake.log.push(['sent', token, dir, name]); return true; },
+ completed: async () => { fake.log.push(['completed']); fake.status.lastCompleted = Date.now(); return fake.status; },
+ failed: async (code, text) => { fake.log.push(['failed', code, text]); return true; },
+ keepAlive: async (on) => { fake.log.push(['keep', on]); return true; },
+} };
+"""
+
+
+def _run(body, tmp_path):
+ root = tmp_path / "static"
+ shutil.copytree(STATIC, root)
+ (root / "package.json").write_text('{"type":"module"}', encoding="utf-8")
+ (root / "connection-pool.js").write_text(FAKE_POOL, encoding="utf-8")
+ script = tmp_path / "case.mjs"
+ script.write_text(
+ PRELUDE
+ + f"const m = await import('{(root / 'drive-sync.js').as_uri()}');\n"
+ + "const ctx = { hub: 'https://hub.invalid', hubFetch, getUser: () => ({ username: 'bob', "
+ "userId: 'u-bob', token: 'tok' }), getBundleKey: async () => 'bk' };\n"
+ + body
+ + "\nm.stopDriveSync();\nconsole.log(JSON.stringify(fake.log));\n",
+ encoding="utf-8")
+ proc = subprocess.run(["node", str(script)], capture_output=True, text=True,
+ encoding="utf-8", timeout=30)
+ assert proc.returncode == 0, proc.stderr
+ return json.loads(proc.stdout.strip().splitlines()[-1])
+
+
+def _start(extra=""):
+ return extra + "\nm.startDriveSync(ctx);\nawait m.runDriveSync();\n"
+
+
+def _of(log, kind):
+ return [e[1:] for e in log if e[0] == kind]
+
+
+def test_a_due_run_sends_each_file_into_its_folder_a_range_at_a_time(tmp_path):
+ log = _run(_start(), tmp_path)
+ assert _of(log, "mkdir") == [["Backups/bob-drive"], ["Backups/bob-drive/Documents"],
+ ["Backups/bob-drive/Documents/Tax"],
+ ["Backups/bob-drive/Documents/Tax/2026"]]
+ assert _of(log, "fetch") == [["/drivesync/d1", "bytes=0-5"], ["/drivesync/d2", "bytes=0-11"]]
+ assert _of(log, "sent") == [["d1", "Backups/bob-drive/Documents", "notes.txt"],
+ ["d2", "Backups/bob-drive/Documents/Tax/2026", "tax.pdf"]]
+ assert ["completed"] in log
+
+
+def test_on_mobile_data_a_scheduled_run_waits(tmp_path):
+ log = _run(_start("fake.status.unmetered = false;"), tmp_path)
+ assert ["plan"] not in log
+
+
+def test_no_folder_chosen_means_nothing_runs(tmp_path):
+ log = _run(_start("fake.status.folders = [];"), tmp_path)
+ assert ["plan"] not in log and _of(log, "connect") == []
+
+
+def test_a_group_with_somebody_else_in_it_stops_it_before_anything_is_read(tmp_path):
+ log = _run(_start("fake.members.push({ user_id: 'u-carol' });"), tmp_path)
+ assert [e[0] for e in _of(log, "failed")] == ["not_private"]
+ assert ["plan"] not in log
+
+
+def test_a_lasting_refusal_names_the_files_backup(tmp_path):
+ log = _run(_start("fake.uploadError = 'disk_full';"), tmp_path)
+ failed = _of(log, "failed")
+ assert [e[0] for e in failed] == ["disk_full"] and failed[0][1]
+ 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")
+ for op in ("deleteFile", "deleteDirectory", "removeRoot", "file_delete", "dir_delete",
+ "root_remove", "rename"):
+ assert not re.search(rf"\b{op}\b", src), f"{path.name} reaches {op}"