aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_phone_sync.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-10 14:21:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-10 14:21:46 +0200
commit2f2a9a6542d2194e50ffba6f382e4fdffd481838 (patch)
tree5437330f9e8b963f8af0904f3c5d80d1afc6dc3b /packages/meshbay-hub/tests/test_phone_sync.py
parent914d2f0af848fb80d70d517901f44420a4e47764 (diff)
downloadmeshbay-2f2a9a6542d2194e50ffba6f382e4fdffd481838.tar.gz
feat(android): one backup destination, a group the account owns alone
Chosen once at the top of Android Sync for every kind; photos and contacts go into <folder>/<account>-photos and -contacts. Owner and sole member are checked at set-up and before every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_phone_sync.py')
-rw-r--r--packages/meshbay-hub/tests/test_phone_sync.py45
1 files changed, 32 insertions, 13 deletions
diff --git a/packages/meshbay-hub/tests/test_phone_sync.py b/packages/meshbay-hub/tests/test_phone_sync.py
index 7e5a88e..1a35614 100644
--- a/packages/meshbay-hub/tests/test_phone_sync.py
+++ b/packages/meshbay-hub/tests/test_phone_sync.py
@@ -43,7 +43,8 @@ export class ConnectionPool {
PRELUDE = r"""
const fake = globalThis.fake = { log: [], pools: [], refuseConnect: null, uploadError: null,
- members: [{ username: 'bob' }], invited: [], membersError: null };
+ members: [{ user_id: 'u-bob', username: 'bob' }], invited: [],
+ adminId: 'u-bob', membersError: null };
const win = new EventTarget();
const doc = new EventTarget();
doc.visibilityState = 'visible';
@@ -56,8 +57,8 @@ globalThis.fetch = async (url) => {
};
fake.status = {
- permission: 'granted', sent: 0, now: Date.now(),
- config: { account: 'bob', groupId: 'g1', groupName: 'Mine', owner: 'bob', folder: 'Backups' },
+ permission: 'granted', sent: 0, now: Date.now(), on: true,
+ destination: { account: 'bob', groupId: 'g1', groupName: 'Mine', folder: 'Backups' },
dir: 'Backups/bob-contacts',
lastCompleted: null, lastSent: null, failure: null, failureAt: null,
};
@@ -86,7 +87,7 @@ fake.transport = {
const hubFetch = async (path) => {
fake.log.push(['hub', path]);
if (fake.membersError) throw new Error(fake.membersError);
- return { members: fake.members, invited: fake.invited };
+ return { admin_id: fake.adminId, members: fake.members, invited: fake.invited };
};
win.meshbay = { contactSync: {
status: async () => fake.status,
@@ -111,6 +112,7 @@ def _run(body, tmp_path):
script.write_text(
PRELUDE
+ f"const m = await import('{(root / 'phone-sync.js').as_uri()}');\n"
+ + f"const d = await import('{(root / 'sync-destination.js').as_uri()}');\n"
+ "const ctx = { hub: 'https://hub.invalid', hubFetch, getUser: () => ({ username: 'bob', "
"userId: 'u-bob', token: 'tok' }), getBundleKey: async () => 'bk' };\n"
+ body
@@ -183,8 +185,9 @@ def test_the_group_is_asked_about_at_every_run(tmp_path):
@pytest.mark.parametrize("setup", [
- "fake.members = [{ username: 'bob' }, { username: 'carol' }];",
- "fake.invited = [{ username: 'carol' }];",
+ "fake.members.push({ user_id: 'u-carol', username: 'carol' });",
+ "fake.invited = [{ user_id: 'u-carol', username: 'carol' }];",
+ "fake.adminId = 'u-carol';",
])
def test_somebody_else_in_the_group_stops_it_before_anything_is_read(tmp_path, setup):
log = _run(_start(setup), tmp_path)
@@ -202,15 +205,18 @@ def test_an_unreachable_hub_is_not_a_lasting_refusal(tmp_path):
assert _of(log, "failed") == [] and _of(log, "plan") == []
-def test_only_groups_this_account_is_alone_in_are_offered(tmp_path):
+def test_only_groups_this_account_owns_and_is_alone_in_are_offered(tmp_path):
log = _run("""
m.startPhoneSync(ctx);
const real = ctx.hubFetch;
ctx.hubFetch = async (path) => path.includes('/g2/')
- ? { members: [{ username: 'bob' }, { username: 'carol' }] }
- : path.includes('/g3/') ? Promise.reject(new Error('offline')) : real(path);
-fake.log.push(['offered', (await m.privateGroups(
- [{ id: 'g1', name: 'Mine' }, { id: 'g2', name: 'Family' }, { id: 'g3', name: 'Far' }]))
+ ? { admin_id: 'u-bob', members: [{ user_id: 'u-bob' }, { user_id: 'u-carol' }] }
+ : path.includes('/g3/') ? Promise.reject(new Error('offline'))
+ : path.includes('/g4/') ? { admin_id: 'u-carol', members: [{ user_id: 'u-bob' }] }
+ : real(path);
+fake.log.push(['offered', (await d.privateGroups(
+ [{ id: 'g1', name: 'Mine' }, { id: 'g2', name: 'Family' }, { id: 'g3', name: 'Far' },
+ { id: 'g4', name: 'Given' }]))
.map((g) => g.id)]);
""", tmp_path)
assert _of(log, "offered") == [[["g1"]]]
@@ -239,7 +245,7 @@ def test_without_the_permission_it_stops_and_says_so(tmp_path):
def test_another_account_on_the_phone_sends_nothing(tmp_path):
- log = _run(_start("fake.status.config.account = 'alice';"), tmp_path)
+ log = _run(_start("fake.status.destination.account = 'alice';"), tmp_path)
assert _of(log, "plan") == [] and _of(log, "hub") == []
@@ -247,7 +253,7 @@ def test_another_account_on_the_phone_sends_nothing(tmp_path):
def test_every_writable_folder_is_offered_not_only_photo_ones(tmp_path):
log = _run("""
-fake.log.push(['folders', m.writableFolders(
+fake.log.push(['folders', d.writableFolders(
{ roots: [{ name: 'Media', writable: true, available: true },
{ name: 'Archive', writable: false, available: true },
{ name: 'Ejected', writable: true, available: false }],
@@ -256,6 +262,19 @@ fake.log.push(['folders', m.writableFolders(
assert _of(log, "folders") == [[["Media", "Media/Films", "Media/Photos"]]]
+def test_photos_outside_the_photos_tab_folders_are_said(tmp_path):
+ log = _run("""
+fake.log.push(['shows', ['Media/Photos/bob-photos', 'Media/Other/bob-photos', 'Media/PhotosX']
+ .map((dir) => d.photosTabShows(dir, ['Media/Photos']))]);
+""", tmp_path)
+ assert _of(log, "shows") == [[[True, False, False]]]
+
+
+def test_turned_off_it_sends_nothing(tmp_path):
+ log = _run(_start("fake.status.on = false;"), tmp_path)
+ assert _of(log, "plan") == [] and _of(log, "hub") == []
+
+
# ── nothing is ever removed ─────────────────────────────────────────────────
def test_the_backup_has_no_way_to_remove_anything_on_the_node():