def test_merge_local_user_manifest_changes_placeholder(gen_date, alice, speculative_placeholder): d1, d2, d3, d4 = [gen_date() for _ in range(4)] w1 = WorkspaceEntry.new(name=EntryName("w1"), timestamp=d2) w2 = WorkspaceEntry.new(name=EntryName("w2"), timestamp=d2) w3 = WorkspaceEntry.new(name=EntryName("w3"), timestamp=d2) diverged = LocalUserManifest.new_placeholder( alice.device_id, id=alice.user_manifest_id, timestamp=d4, speculative=speculative_placeholder, ).evolve(last_processed_message=30, workspaces=(w1, w3)) target = UserManifest( author=alice.device_id, timestamp=d2, id=alice.user_manifest_id, version=3, created=d1, updated=d3, last_processed_message=20, workspaces=(w1, w2), ) expected_merged = LocalUserManifest( base=target, updated=d4, last_processed_message=30, workspaces=(w1, w2, w3), need_sync=True, speculative=False, ) merged = merge_local_user_manifests(diverged, target) assert merged == expected_merged
async def test_sharing_event_on_sync_if_same_role( running_backend, alice_user_fs, alice2_user_fs, bob_user_fs, alice, bob ): # Share a workspace, alice2 knows about it with freeze_time("2000-01-02"): wid = await create_shared_workspace("w", bob_user_fs, alice_user_fs, alice2_user_fs) expected_entry_v1 = WorkspaceEntry( name="w (shared by bob)", id=wid, key=ANY, encryption_revision=1, encrypted_on=Pendulum(2000, 1, 2), role_cached_on=Pendulum(2000, 1, 2), role=WorkspaceRole.MANAGER, ) # Then change alice's role... await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.OWNER) with freeze_time("2000-01-03"): await alice_user_fs.process_last_messages() await alice_user_fs.sync() # ...and give back alice the same role await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.MANAGER) with freeze_time("2000-01-04"): await alice_user_fs.process_last_messages() expected_entry_v3 = expected_entry_v1.evolve(role_cached_on=Pendulum(2000, 1, 4)) await alice_user_fs.sync() # A single sharing event should be triggered with alice2_user_fs.event_bus.listen() as spy: await alice2_user_fs.sync() spy.assert_event_occured( "sharing.updated", {"new_entry": expected_entry_v3, "previous_entry": expected_entry_v1} )
def test_created_field_modified_by_remote(gen_date, alice, with_local_changes): d1, d2, d3, d4 = [gen_date() for _ in range(4)] w1 = WorkspaceEntry.new(name=EntryName("w1"), timestamp=d2) base = UserManifest( author=alice.device_id, timestamp=d2, id=alice.user_manifest_id, version=1, created=d1, updated=d2, last_processed_message=0, workspaces=(w1,), ) local = LocalUserManifest.from_remote(base) if with_local_changes: w2 = WorkspaceEntry.new(name=EntryName("w1"), timestamp=d3) local = local.evolve( need_sync=True, updated=d3, last_processed_message=1, workspaces=(w1, w2) ) target = base.evolve(created=d4, version=2) expected_merged = local.evolve(base=target) merged = merge_local_user_manifests(local, target) # Remote always control the value of the create field assert merged == expected_merged
async def test_reshare_workspace(running_backend, alice_user_fs, bob_user_fs, alice, bob): # Share a workspace... with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create("w1") await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.MANAGER) with freeze_time("2000-01-03"): await bob_user_fs.process_last_messages() # ...and unshare it... await alice_user_fs.workspace_share(wid, bob.user_id, None) with freeze_time("2000-01-04"): await bob_user_fs.process_last_messages() # ...and re-share it ! await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.MANAGER) with bob_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-05"): await bob_user_fs.process_last_messages() spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name="w1", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 5), role=WorkspaceRole.MANAGER, ), "previous_entry": WorkspaceEntry( name="w1", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 4), role=None, ), }, ) # Check access aum = alice_user_fs.get_user_manifest() bum = bob_user_fs.get_user_manifest() assert len(aum.workspaces) == 1 assert len(bum.workspaces) == 1 aw = aum.workspaces[0] bw = bum.workspaces[0] assert bw.name == "w1" assert bw.id == aw.id assert bw.role == WorkspaceRole.MANAGER
async def test_unshare_ok(running_backend, alice_user_fs, bob_user_fs, alice, bob): # Share a workspace... with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create(EntryName("w1")) await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.OWNER) await bob_user_fs.process_last_messages() # ...and unshare it await bob_user_fs.workspace_share(wid, alice.user_id, None) with alice_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-03"): await alice_user_fs.process_last_messages() new_events = [] for event in spy.events: if event.event == CoreEvent.SHARING_UPDATED: event.kwargs["new_entry"] = event.kwargs["new_entry"].evolve( key=KEY) event.kwargs["previous_entry"] = event.kwargs[ "previous_entry"].evolve(key=KEY) new_events.append(event) spy.events = new_events spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name=EntryName("w1"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 3), role=None, ), "previous_entry": WorkspaceEntry( name=EntryName("w1"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), }, ) aum = alice_user_fs.get_user_manifest() aw = aum.workspaces[0] assert not aw.role
async def test_sharing_event_on_sync_if_same_role(running_backend, alice_user_fs, alice2_user_fs, bob_user_fs, alice, bob): # Share a workspace, alice2 knows about it with freeze_time("2000-01-02"): wid = await create_shared_workspace(EntryName("w"), bob_user_fs, alice_user_fs, alice2_user_fs) expected_entry_v1 = WorkspaceEntry( name=EntryName("w"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.MANAGER, ) # Then change alice's role... await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.OWNER) with freeze_time("2000-01-03"): await alice_user_fs.process_last_messages() await alice_user_fs.sync() # ...and give back alice the same role await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.MANAGER) with freeze_time("2000-01-04"): await alice_user_fs.process_last_messages() expected_entry_v3 = expected_entry_v1.evolve( role_cached_on=datetime(2000, 1, 4)) await alice_user_fs.sync() # A single sharing event should be triggered with alice2_user_fs.event_bus.listen() as spy: await alice2_user_fs.sync() new_events = [] for event in spy.events: if event.event == CoreEvent.SHARING_UPDATED: event.kwargs["new_entry"] = event.kwargs["new_entry"].evolve( key=KEY) event.kwargs["previous_entry"] = event.kwargs[ "previous_entry"].evolve(key=KEY) new_events.append(event) spy.events = new_events spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": expected_entry_v3, "previous_entry": expected_entry_v1 }, )
def test_merge_local_user_manifest_no_changes_in_diverged_placeholder( gen_date, alice, alice2, with_ignored_changes ): w1 = WorkspaceEntry.new(name="w1") d1, d2, d3, d4 = [gen_date() for _ in range(4)] base = UserManifest( author=alice.device_id, timestamp=d2, id=alice.user_manifest_id, version=1, created=d1, updated=d2, last_processed_message=0, workspaces=(w1,), ) diverged = LocalUserManifest.from_remote(base) if with_ignored_changes: diverged = diverged.evolve(updated=d4, need_sync=True) target = UserManifest( author=alice2.device_id, timestamp=d2, id=alice2.user_manifest_id, version=2, created=d1, updated=d3, last_processed_message=0, workspaces=(w1,), ) expected_merged = LocalUserManifest.from_remote(target) merged = merge_local_user_manifests(diverged, target) assert merged == expected_merged
def test_merge_local_user_manifest_no_changes_in_diverged_placeholder( gen_date, alice, alice2, with_ignored_changes ): d1, d2, d3, d4, d5, d6, d7 = [gen_date() for _ in range(7)] w1 = WorkspaceEntry.new(name=EntryName("w1"), timestamp=d2) base = UserManifest( author=alice.device_id, timestamp=d4, id=alice.user_manifest_id, version=1, created=d1, updated=d3, last_processed_message=0, workspaces=(w1,), ) diverged = LocalUserManifest.from_remote(base) if with_ignored_changes: w1_bis = WorkspaceEntry( name=w1.name, id=w1.id, key=w1.key, # Same encryption revision than remote (so encryption date should be ignored) encryption_revision=1, encrypted_on=d5, # Cache older than remote role_cached_on=d1, role=RealmRole.MANAGER, ) diverged = diverged.evolve(updated=d4, need_sync=True, workspaces=(w1_bis,)) target = UserManifest( author=alice2.device_id, timestamp=d7, id=alice2.user_manifest_id, version=2, created=d1, updated=d6, last_processed_message=0, workspaces=(w1,), ) expected_merged = LocalUserManifest.from_remote(target) merged = merge_local_user_manifests(diverged, target) assert merged == expected_merged
async def test_event_bus_internal_connection(aqtbot, running_backend, logged_gui, autoclose_dialog): w_w = await logged_gui.test_switch_to_workspaces_widget() uuid = UUID("1bc1e17b-157a-462f-86f2-7f64657ba16a") w_entry = WorkspaceEntry( name="w", id=ANY, key=ANY, encryption_revision=1, encrypted_on=ANY, role_cached_on=ANY, role=None, ) async with aqtbot.wait_signal(w_w.fs_synced_qt): w_w.event_bus.send(CoreEvent.FS_ENTRY_SYNCED, workspace_id=None, id=uuid) async with aqtbot.wait_signal(w_w.fs_updated_qt): w_w.event_bus.send(CoreEvent.FS_ENTRY_UPDATED, workspace_id=uuid, id=None) async with aqtbot.wait_signal(w_w._workspace_created_qt): w_w.event_bus.send(CoreEvent.FS_WORKSPACE_CREATED, new_entry=w_entry) async with aqtbot.wait_signal(w_w.sharing_updated_qt): w_w.event_bus.send(CoreEvent.SHARING_UPDATED, new_entry=w_entry, previous_entry=None) async with aqtbot.wait_signal(w_w.entry_downsynced_qt): w_w.event_bus.send(CoreEvent.FS_ENTRY_DOWNSYNCED, workspace_id=uuid, id=uuid) async with aqtbot.wait_signal(w_w.mountpoint_started): w_w.event_bus.send( CoreEvent.MOUNTPOINT_STARTED, mountpoint=None, workspace_id=uuid, timestamp=pendulum.now(), ) assert not autoclose_dialog.dialogs async with aqtbot.wait_signal(w_w.mountpoint_stopped): w_w.event_bus.send( CoreEvent.MOUNTPOINT_STOPPED, mountpoint=None, workspace_id=uuid, timestamp=pendulum.now(), ) assert autoclose_dialog.dialogs == [( "Error", "Your permissions on this workspace have been revoked. You no longer have access to theses files.", )]
def test_merge_speculative_with_it_unsuspected_former_self(alice, local_changes): d1 = datetime(2000, 1, 1) d2 = datetime(2000, 1, 2) d3 = datetime(2000, 1, 3) # 1) User manifest is originally created by our device local = LocalUserManifest.new_placeholder( author=alice.device_id, id=alice.user_manifest_id, timestamp=d1, speculative=False ) w1 = WorkspaceEntry.new(EntryName("foo"), timestamp=d1) local = local.evolve(workspaces=(w1,), last_processed_message=1) # 2) We sync the user manifest v1 = local.to_remote(author=alice.device_id, timestamp=d2) # 3) Now let's pretend we lost local storage, hence creating a new speculative manifest new_local = LocalUserManifest.new_placeholder( author=alice.device_id, id=alice.user_manifest_id, timestamp=d3, speculative=True ) if local_changes: w2 = WorkspaceEntry.new(EntryName("bar"), timestamp=d3) new_local = new_local.evolve(workspaces=(w2,), last_processed_message=2) # 4) When syncing the manifest, we shouldn't remove any data from the remote merged = merge_local_user_manifests(new_local, v1) if local_changes: assert merged == LocalUserManifest( base=v1, updated=d3, last_processed_message=2, workspaces=(w2, w1), need_sync=True, speculative=False, ) else: assert merged == LocalUserManifest( base=v1, updated=v1.updated, last_processed_message=1, workspaces=(w1,), need_sync=False, speculative=False, )
async def test_sharing_notifs(aqtbot, logged_gui, snackbar_catcher, monkeypatch): c_w = logged_gui.test_get_central_widget() def _snackbar_shown(sb): assert snackbar_catcher.snackbars == sb ne = WorkspaceEntry.new(EntryName("Workspace"), datetime(2000, 1, 2)) ne = ne.evolve(role=RealmRole.CONTRIBUTOR) pe = WorkspaceEntry.new(EntryName("Workspace"), datetime(2000, 1, 1)) pe = pe.evolve(role=RealmRole.READER) c_w.handle_event(CoreEvent.SHARING_UPDATED, new_entry=ne, previous_entry=pe) await aqtbot.wait_until(lambda: _snackbar_shown([( "INFO", translate("TEXT_NOTIF_INFO_WORKSPACE_ROLE_UPDATED_workspace").format( workspace="Workspace"), )])) snackbar_catcher.reset() c_w.handle_event(CoreEvent.SHARING_UPDATED, new_entry=ne, previous_entry=None) await aqtbot.wait_until(lambda: _snackbar_shown([( "INFO", translate("TEXT_NOTIF_INFO_WORKSPACE_SHARED_workspace").format( workspace="Workspace"), )])) ne = ne.evolve(role=None) snackbar_catcher.reset() c_w.handle_event(CoreEvent.SHARING_UPDATED, new_entry=ne, previous_entry=pe) await aqtbot.wait_until(lambda: _snackbar_shown([( "INFO", translate("TEXT_NOTIF_INFO_WORKSPACE_UNSHARED_workspace").format( workspace="Workspace"), )]))
async def test_unshare_ok(running_backend, alice_user_fs, bob_user_fs, alice, bob): # Share a workspace... with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create("w1") await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.OWNER) await bob_user_fs.process_last_messages() # ...and unshare it await bob_user_fs.workspace_share(wid, alice.user_id, None) with alice_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-03"): await alice_user_fs.process_last_messages() spy.assert_event_occured( "sharing.updated", { "new_entry": WorkspaceEntry( name="w1", id=wid, key=ANY, encryption_revision=1, encrypted_on=Pendulum(2000, 1, 2), role_cached_on=Pendulum(2000, 1, 3), role=None, ), "previous_entry": WorkspaceEntry( name="w1", id=wid, key=ANY, encryption_revision=1, encrypted_on=Pendulum(2000, 1, 2), role_cached_on=Pendulum(2000, 1, 2), role=WorkspaceRole.OWNER, ), }, ) aum = alice_user_fs.get_user_manifest() aw = aum.workspaces[0] assert not aw.role
def merge_workspace_entry(base: Optional[WorkspaceEntry], diverged: WorkspaceEntry, target: WorkspaceEntry) -> WorkspaceEntry: assert diverged.id == target.id assert not base or base.id == target.id # If the name has been modified on both sides, target always wins if base and base.name != target.name: name = target.name elif base and base.name != diverged.name: name = diverged.name else: name = target.name # Keep last encryption if diverged.encryption_revision <= target.encryption_revision: # Note `diverged.encryption_revision == target.encryption_revision` # should imply `diverged.encrypted_on == target.encrypted_on`, but # there is no way to enforce this (e.g. a buggy client may have change # this value...). However we'd better keep the remote value to avoid # constant sync fight between two client if they endup with a different # value they both consider the "right" one. encryption_revision = target.encryption_revision encrypted_on = target.encrypted_on key = target.key else: encryption_revision = diverged.encryption_revision encrypted_on = diverged.encrypted_on key = diverged.key # Keep most recent cache info on role if target.role == diverged.role: role = target.role role_cached_on = max(target.role_cached_on, diverged.role_cached_on) elif target.role_cached_on > diverged.role_cached_on: role = target.role role_cached_on = target.role_cached_on else: role = diverged.role role_cached_on = diverged.role_cached_on return WorkspaceEntry( name=name, id=target.id, key=key, encryption_revision=encryption_revision, encrypted_on=encrypted_on, role_cached_on=role_cached_on, role=role, )
async def test_share_with_sharing_name_already_taken(running_backend, alice_user_fs, bob_user_fs, alice, bob): # Bob and Alice both has a workspace with similar name with freeze_time("2000-01-01"): awid = await alice_user_fs.workspace_create("w") bwid = await bob_user_fs.workspace_create("w") bw2id = await bob_user_fs.workspace_create("w") # Sharing them shouldn't be a trouble await bob_user_fs.sync() await alice_user_fs.workspace_share(awid, bob.user_id, WorkspaceRole.MANAGER) # Bob should get a notification with bob_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-02"): await bob_user_fs.process_last_messages() spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name="w", id=awid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.MANAGER, ), "previous_entry": None, }, ) assert len(bob_user_fs.get_user_manifest().workspaces) == 3 b_aw_stat = await bob_user_fs.get_workspace(awid).path_info("/") a_aw_stat = await alice_user_fs.get_workspace(awid).path_info("/") b_aw_stat.pop("need_sync") a_aw_stat.pop("need_sync") assert b_aw_stat == a_aw_stat b_bw_stat = await bob_user_fs.get_workspace(bwid).path_info("/") assert b_bw_stat["id"] == bwid b_bw2_stat = await bob_user_fs.get_workspace(bw2id).path_info("/") assert b_bw2_stat["id"] == bw2id
async def test_share_ok(running_backend, alice_user_fs, bob_user_fs, alice, bob, presynced): with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create("w1") if presynced: await alice_user_fs.sync() await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.MANAGER) with bob_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-03"): await bob_user_fs.process_last_messages() spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name="w1", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 3), role=WorkspaceRole.MANAGER, ), "previous_entry": None, }, ) aum = alice_user_fs.get_user_manifest() bum = bob_user_fs.get_user_manifest() assert len(aum.workspaces) == 1 assert len(bum.workspaces) == 1 awe = aum.get_workspace_entry(wid) bwe = bum.get_workspace_entry(wid) assert bwe.name == "w1" assert bwe.id == awe.id assert bwe.role == WorkspaceRole.MANAGER aw = alice_user_fs.get_workspace(wid) bw = bob_user_fs.get_workspace(wid) aw_stat = await aw.path_info("/") bw_stat = await bw.path_info("/") assert aw_stat == bw_stat
def merge_workspace_entry(base: Optional[WorkspaceEntry], diverged: WorkspaceEntry, target: WorkspaceEntry) -> WorkspaceEntry: assert diverged.id == target.id assert not base or base.id == target.id # If the name has been modified on both sides, target always wins if base and base.name != target.name: name = target.name elif base and base.name != diverged.name: name = diverged.name else: name = target.name # Keep last encryption if diverged.encryption_revision < target.encryption_revision: encryption_revision = target.encryption_revision encrypted_on = target.encrypted_on key = target.key else: encryption_revision = diverged.encryption_revision encrypted_on = diverged.encrypted_on key = diverged.key # Keep most recent cache info on role if target.role == diverged.role: role = target.role role_cached_on = max(target.role_cached_on, diverged.role_cached_on) elif target.role_cached_on > diverged.role_cached_on: role = target.role role_cached_on = target.role_cached_on else: role = diverged.role role_cached_on = diverged.role_cached_on return WorkspaceEntry( name=name, id=target.id, key=key, encryption_revision=encryption_revision, encrypted_on=encrypted_on, role_cached_on=role_cached_on, role=role, )
async def test_share_ok(running_backend, alice_user_fs, bob_user_fs, alice, bob, presynced): with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create(EntryName("w1")) if presynced: await alice_user_fs.sync() await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.MANAGER) with bob_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-03"): await bob_user_fs.process_last_messages() expected_bob_w1_workspace_entry = WorkspaceEntry( name=EntryName("w1"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 3), role=WorkspaceRole.MANAGER, ) new_events = [] for event in spy.events: if event.event == CoreEvent.SHARING_UPDATED: event.kwargs["new_entry"] = event.kwargs["new_entry"].evolve( key=KEY) new_events.append(event) spy.events = new_events spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": expected_bob_w1_workspace_entry, "previous_entry": None }, ) aum = alice_user_fs.get_user_manifest() bum = bob_user_fs.get_user_manifest() aum = _update_user_manifest_key(aum) bum = _update_user_manifest_key(bum) assert bum.workspaces == (expected_bob_w1_workspace_entry, ) assert bum.get_workspace_entry(wid).key == aum.get_workspace_entry(wid).key
async def test_sharing_events_triggered_on_sync( running_backend, alice_user_fs, alice2_user_fs, bob_user_fs, alice, bob ): # Share a first workspace with freeze_time("2000-01-02"): wid = await create_shared_workspace("w", bob_user_fs, alice_user_fs) with alice2_user_fs.event_bus.listen() as spy: await alice2_user_fs.sync() expected_entry_v1 = WorkspaceEntry( name="w", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.MANAGER, ) spy.assert_event_occured( CoreEvent.SHARING_UPDATED, {"new_entry": expected_entry_v1, "previous_entry": None} ) # Change role await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.OWNER) with freeze_time("2000-01-03"): await alice_user_fs.process_last_messages() await alice_user_fs.sync() with alice2_user_fs.event_bus.listen() as spy: await alice2_user_fs.sync() expected_entry_v2 = WorkspaceEntry( name="w", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 3), role=WorkspaceRole.OWNER, ) spy.assert_event_occured( CoreEvent.SHARING_UPDATED, {"new_entry": expected_entry_v2, "previous_entry": expected_entry_v1}, ) # Revoke await bob_user_fs.workspace_share(wid, alice.user_id, None) with freeze_time("2000-01-04"): await alice_user_fs.process_last_messages() await alice_user_fs.sync() with alice2_user_fs.event_bus.listen() as spy: await alice2_user_fs.sync() expected_entry_v3 = WorkspaceEntry( name="w", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 4), role=None, ) spy.assert_event_occured( CoreEvent.SHARING_UPDATED, {"new_entry": expected_entry_v3, "previous_entry": expected_entry_v2}, )
async def test_share_workspace_then_conflict_on_rights( running_backend, alice_user_fs, alice2_user_fs, bob_user_fs, alice, alice2, bob, first_to_sync ): # Bob shares a workspace with Alice... with freeze_time("2000-01-01"): wid = await bob_user_fs.workspace_create("w") with freeze_time("2000-01-02"): await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.MANAGER) # ...but only Alice's first device get the information with freeze_time("2000-01-03"): await alice_user_fs.process_last_messages() # Now Bob change the sharing rights... with freeze_time("2000-01-04"): await bob_user_fs.workspace_share(wid, alice.user_id, WorkspaceRole.CONTRIBUTOR) # ...this time it's Alice's second device which get the info with freeze_time("2000-01-05"): # Note we will process the 2 sharing messages bob sent us, this # will attribute role_cached_on to the first message timestamp even # if we cache the second message role... await alice2_user_fs.process_last_messages() if first_to_sync == "alice": first = alice_user_fs second = alice2_user_fs synced_timestamp = datetime(2000, 1, 7) synced_version = 3 else: first = alice2_user_fs second = alice_user_fs synced_timestamp = datetime(2000, 1, 6) synced_version = 2 # Finally Alice devices try to reconciliate with freeze_time("2000-01-06"): await first.sync() with freeze_time("2000-01-07"): await second.sync() # Resync first device to get changes from the 2nd with freeze_time("2000-01-08"): await first.sync() am = alice_user_fs.get_user_manifest() a2m = alice2_user_fs.get_user_manifest() expected_remote = UserManifest( author=alice2.device_id, timestamp=synced_timestamp, id=alice2.user_manifest_id, version=synced_version, created=datetime(2000, 1, 1), updated=datetime(2000, 1, 5), last_processed_message=2, workspaces=( WorkspaceEntry( name="w", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 5), role=WorkspaceRole.CONTRIBUTOR, ), ), ) expected = LocalUserManifest( base=expected_remote, need_sync=False, updated=expected_remote.updated, last_processed_message=expected_remote.last_processed_message, workspaces=expected_remote.workspaces, ) assert am == expected assert a2m == expected a_w = alice_user_fs.get_workspace(wid) a2_w = alice2_user_fs.get_workspace(wid) a_w_stat = await a_w.path_info("/") a2_w_stat = await a2_w.path_info("/") a_w_entry = a_w.get_workspace_entry() a2_w_entry = a2_w.get_workspace_entry() assert a_w_stat == { "type": "folder", "is_placeholder": False, "id": wid, "created": ANY, "updated": ANY, "base_version": 1, "need_sync": False, "children": [], "confinement_point": None, } assert a_w_stat == a2_w_stat assert a_w_entry == WorkspaceEntry( name=f"w", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 5), role=WorkspaceRole.CONTRIBUTOR, ) assert a2_w_entry == a_w_entry
async def test_concurrent_devices_agree_on_user_manifest( running_backend, backend_data_binder, data_base_dir, user_fs_factory, coolorg, alice, alice2, with_speculative, ): KEY = SecretKey.generate() async def _switch_running_backend_offline(task_status): should_switch_online = trio.Event() backend_online = trio.Event() async def _switch_backend_online(): should_switch_online.set() await backend_online.wait() with running_backend.offline(): task_status.started(_switch_backend_online) await should_switch_online.wait() backend_online.set() # I call this "diagonal programming"... async with trio.open_nursery() as nursery: switch_back_online = await nursery.start( _switch_running_backend_offline) with freeze_time("2000-01-01"): if with_speculative != "both": await user_storage_non_speculative_init( data_base_dir=data_base_dir, device=alice) async with user_fs_factory( alice, data_base_dir=data_base_dir) as user_fs1: wksp1_id = await user_fs1.workspace_create(EntryName("wksp1")) with freeze_time("2000-01-02"): if with_speculative not in ("both", "alice2"): await user_storage_non_speculative_init( data_base_dir=data_base_dir, device=alice2) async with user_fs_factory( alice2, data_base_dir=data_base_dir) as user_fs2: wksp2_id = await user_fs2.workspace_create( EntryName("wksp2")) with freeze_time("2000-01-03"): # Only now the backend appear offline, this is to ensure each # userfs has created a user manifest in isolation await backend_data_binder.bind_organization( coolorg, alice, initial_user_manifest="not_synced") await backend_data_binder.bind_device( alice2, certifier=alice) await switch_back_online() # Sync user_fs2 first to ensure created_on field is # kept even if further syncs have an earlier value with freeze_time("2000-01-04"): await user_fs2.sync() with freeze_time("2000-01-05"): await user_fs1.sync() with freeze_time("2000-01-06"): await user_fs2.sync() # Now, both user fs should have the same view on data expected_workspaces_entries = ( WorkspaceEntry( name=EntryName("wksp1"), id=wksp1_id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.OWNER, ), WorkspaceEntry( name=EntryName("wksp2"), id=wksp2_id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ) expected_user_manifest = LocalUserManifest( base=UserManifest( id=alice.user_manifest_id, version=2, timestamp=datetime(2000, 1, 5), author=alice.device_id, created=datetime(2000, 1, 2), updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_workspaces_entries, ), need_sync=False, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_workspaces_entries, speculative=False, ) user_fs1_manifest = user_fs1.get_user_manifest() user_fs2_manifest = user_fs2.get_user_manifest() # We use to use ANY for the "key" argument in expected_user_manifest, # so that we could compare the two instances safely. Sadly, ANY doesn't # play nicely with the Rust bindings, so we instead update the instances # to change the key. user_fs1_manifest = user_fs1_manifest.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs1_manifest.workspaces), base=user_fs1_manifest.base.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs1_manifest.base.workspaces)), ) user_fs2_manifest = user_fs2_manifest.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs2_manifest.workspaces), base=user_fs2_manifest.base.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs2_manifest.base.workspaces)), ) assert user_fs1_manifest == expected_user_manifest assert user_fs2_manifest == expected_user_manifest
async def test_share_with_sharing_name_already_taken(running_backend, alice_user_fs, bob_user_fs, alice, bob): # Bob and Alice both has a workspace with similar name with freeze_time("2000-01-01"): awid = await alice_user_fs.workspace_create(EntryName("w")) bwid = await bob_user_fs.workspace_create(EntryName("w")) bw2id = await bob_user_fs.workspace_create(EntryName("w")) # Sharing them shouldn't be a trouble await bob_user_fs.sync() await alice_user_fs.workspace_share(awid, bob.user_id, WorkspaceRole.MANAGER) # Bob should get a notification with bob_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-02"): await bob_user_fs.process_last_messages() new_events = [] for event in spy.events: if event.event == CoreEvent.SHARING_UPDATED: event.kwargs["new_entry"] = event.kwargs["new_entry"].evolve( key=KEY) new_events.append(event) spy.events = new_events spy.assert_event_occured( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name=EntryName("w"), id=awid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.MANAGER, ), "previous_entry": None, }, ) bob_user_manifest = bob_user_fs.get_user_manifest() assert len(bob_user_manifest.workspaces) == 3 bob_user_manifest = _update_user_manifest_key(bob_user_manifest) assert (WorkspaceEntry( name=EntryName("w"), id=bwid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=RealmRole.OWNER, ) in bob_user_manifest.workspaces) assert (WorkspaceEntry( name=EntryName("w"), id=bw2id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=RealmRole.OWNER, ) in bob_user_manifest.workspaces) assert (WorkspaceEntry( name=EntryName("w"), id=awid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 2), role=RealmRole.MANAGER, ) in bob_user_manifest.workspaces)