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_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_sync(running_backend, alice2_user_fs, alice2): with freeze_time("2000-01-02"): wid = await alice2_user_fs.workspace_create("w1") with freeze_time("2000-01-03"): await alice2_user_fs.sync() um = alice2_user_fs.get_user_manifest() expected_base_um = UserManifest( author=alice2.device_id, timestamp=datetime(2000, 1, 3), id=alice2.user_manifest_id, version=2, created=datetime(2000, 1, 1), updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=(WorkspaceEntry( name="w1", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest.from_remote(expected_base_um) assert um == expected_um
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
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_sync_placeholder( running_backend, backend_data_binder, local_device_factory, user_fs_factory, with_workspace ): device = local_device_factory() await backend_data_binder.bind_device(device, initial_user_manifest_in_v0=True) async with user_fs_factory(device, initialize_in_v0=True) as user_fs: um_v0 = user_fs.get_user_manifest() expected_um = LocalUserManifest.new_placeholder( id=device.user_manifest_id, now=um_v0.created ) assert um_v0 == expected_um if with_workspace: with freeze_time("2000-01-02"): wid = await user_fs.workspace_create("w1") um = user_fs.get_user_manifest() expected_um = um_v0.evolve( updated=Pendulum(2000, 1, 2), workspaces=( 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, ), ), ) assert um == expected_um with freeze_time("2000-01-02"): await user_fs.sync() um = user_fs.get_user_manifest() expected_base_um = UserManifest( author=device.device_id, timestamp=Pendulum(2000, 1, 2), id=device.user_manifest_id, version=1, created=expected_um.created, updated=expected_um.updated, last_processed_message=0, workspaces=expected_um.workspaces, ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=expected_um.updated, last_processed_message=0, workspaces=expected_base_um.workspaces, ) assert um == expected_um
async def test_sync_under_concurrency(running_backend, alice_user_fs, alice2_user_fs, alice, alice2): with freeze_time("2000-01-02"): waid = await alice_user_fs.workspace_create(EntryName("wa")) with freeze_time("2000-01-03"): wa2id = await alice2_user_fs.workspace_create(EntryName("wa2")) with freeze_time("2000-01-04"): await alice_user_fs.sync() with freeze_time("2000-01-05"): await alice2_user_fs.sync() # Fetch back alice2's changes with freeze_time("2000-01-06"): await alice_user_fs.sync() um = alice_user_fs.get_user_manifest() um2 = alice2_user_fs.get_user_manifest() expected_base_um = UserManifest( author=alice2.device_id, timestamp=datetime(2000, 1, 5), id=alice2.user_manifest_id, version=3, created=datetime(2000, 1, 1), updated=datetime(2000, 1, 3), last_processed_message=0, workspaces=( WorkspaceEntry( name=EntryName("wa"), id=waid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), WorkspaceEntry( name=EntryName("wa2"), id=wa2id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 3), role_cached_on=datetime(2000, 1, 3), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest.from_remote(expected_base_um) um = _update_user_manifest_key(um) um2 = _update_user_manifest_key(um2) assert um == expected_um assert um2 == expected_um
async def _fetch_remote_user_manifest(self, version: int = None) -> UserManifest: """ Raises: FSError FSWorkspaceInMaintenance FSBackendOfflineError """ try: # Note encryption_revision is always 1 given we never reencrypt # the user manifest's realm rep = await self.backend_cmds.vlob_read(1, self.user_manifest_id, version) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc if rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( "Cannot access workspace data while it is in maintenance") elif rep["status"] != "ok": raise FSError(f"Cannot fetch user manifest from backend: {rep}") expected_author = rep["author"] expected_timestamp = rep["timestamp"] expected_version = rep["version"] blob = rep["blob"] try: author = await self.remote_devices_manager.get_device( expected_author) except RemoteDevicesManagerBackendOfflineError as exc: raise FSBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise FSError(f"Cannot retrieve author public key: {exc}") from exc try: manifest = UserManifest.decrypt_verify_and_load( blob, key=self.device.user_manifest_key, author_verify_key=author.verify_key, expected_id=self.device.user_manifest_id, expected_author=expected_author, expected_timestamp=expected_timestamp, expected_version=version if version is not None else expected_version, ) except DataError as exc: raise FSError(f"Invalid user manifest: {exc}") from exc return manifest
async def test_sync_remote_changes(running_backend, alice_user_fs, alice2_user_fs, alice, alice2): # Alice 2 update the user manifest with freeze_time("2000-01-02"): wid = await alice2_user_fs.workspace_create(EntryName("wa")) with freeze_time("2000-01-03"): await alice2_user_fs.sync() # Alice retrieve the changes um = alice_user_fs.get_user_manifest() await alice_user_fs.sync() um = alice_user_fs.get_user_manifest() um2 = alice2_user_fs.get_user_manifest() expected_base_um = UserManifest( author=alice2.device_id, timestamp=datetime(2000, 1, 3), id=alice2.user_manifest_id, version=2, created=datetime(2000, 1, 1), updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=(WorkspaceEntry( name=EntryName("wa"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_base_um.workspaces, speculative=False, ) um = _update_user_manifest_key(um) um2 = _update_user_manifest_key(um2) assert um == expected_um assert um2 == expected_um
def _generate_or_retrieve_user_manifest_v1(self, device): try: return self._v1[device.user_id] except KeyError: now = pendulum.now() remote_user_manifest = UserManifest( author=device.device_id, timestamp=now, id=device.user_manifest_id, version=1, created=now, updated=now, last_processed_message=0, workspaces=(), ) local_user_manifest = LocalUserManifest.from_remote( remote_user_manifest) self._v1[device.user_id] = (remote_user_manifest, local_user_manifest) return self._v1[device.user_id]
def _generate_or_retrieve_user_manifest_v1(self, device): try: return self._v1[(device.organization_id, device.user_id)] except KeyError: timestamp = device.timestamp() remote_user_manifest = UserManifest( author=device.device_id, timestamp=timestamp, id=device.user_manifest_id, version=1, created=timestamp, updated=timestamp, last_processed_message=0, workspaces=(), ) local_user_manifest = LocalUserManifest.from_remote(remote_user_manifest) self._v1[(device.organization_id, device.user_id)] = ( remote_user_manifest, local_user_manifest, ) return self._v1[(device.organization_id, device.user_id)]
async def test_concurrent_sync_placeholder(running_backend, backend_data_binder, local_device_factory, user_fs_factory, dev2_has_changes): device1 = local_device_factory("a@1") await backend_data_binder.bind_device(device1, initial_user_manifest_in_v0=True) device2 = local_device_factory("a@2") await backend_data_binder.bind_device(device2, initial_user_manifest_in_v0=True) async with user_fs_factory( device1, initialize_in_v0=True) as user_fs1, user_fs_factory( device2, initialize_in_v0=True) as user_fs2: # fs2's created value is different and will be overwritten when # merging synced manifest from fs1 um_created_v0_fs1 = user_fs1.get_user_manifest().created with freeze_time("2000-01-01"): w1id = await user_fs1.workspace_create("w1") if dev2_has_changes: with freeze_time("2000-01-02"): w2id = await user_fs2.workspace_create("w2") with freeze_time("2000-01-03"): await user_fs1.sync() with freeze_time("2000-01-04"): await user_fs2.sync() if dev2_has_changes: with freeze_time("2000-01-05"): await user_fs1.sync() um1 = user_fs1.get_user_manifest() um2 = user_fs2.get_user_manifest() if dev2_has_changes: expected_base_um = UserManifest( author=device2.device_id, id=device2.user_manifest_id, timestamp=datetime(2000, 1, 4), version=2, created=um_created_v0_fs1, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=( WorkspaceEntry( name="w1", id=w1id, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.OWNER, ), WorkspaceEntry( name="w2", id=w2id, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_base_um.workspaces, ) else: expected_base_um = UserManifest( author=device1.device_id, timestamp=datetime(2000, 1, 3), id=device1.user_manifest_id, version=1, created=um_created_v0_fs1, updated=datetime(2000, 1, 1), last_processed_message=0, workspaces=(WorkspaceEntry( name="w1", id=w1id, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=datetime(2000, 1, 1), last_processed_message=0, workspaces=expected_base_um.workspaces, ) assert um1 == expected_um assert um2 == expected_um
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_concurrent_sync_placeholder( running_backend, backend_data_binder, local_device_factory, user_fs_factory, data_base_dir, initialize_local_user_manifest, dev2_has_changes, ): device1 = local_device_factory("a@1") await backend_data_binder.bind_device(device1, initial_user_manifest="not_synced") await initialize_local_user_manifest( data_base_dir, device1, initial_user_manifest="non_speculative_v0") device2 = local_device_factory("a@2") await backend_data_binder.bind_device(device2) await initialize_local_user_manifest( data_base_dir, device2, initial_user_manifest="speculative_v0") async with user_fs_factory(device1) as user_fs1, user_fs_factory( device2) as user_fs2: # fs2's created value is different and will be overwritten when # merging synced manifest from fs1 um_created_v0_fs1 = user_fs1.get_user_manifest().created with freeze_time("2000-01-01"): # Sync user manifests now to avoid extra milliseconds from restamping await user_fs1.sync() await user_fs2.sync() w1id = await user_fs1.workspace_create(EntryName("w1")) if dev2_has_changes: with freeze_time("2000-01-02"): w2id = await user_fs2.workspace_create(EntryName("w2")) with freeze_time("2000-01-03"): await user_fs1.sync() with freeze_time("2000-01-04"): await user_fs2.sync() if dev2_has_changes: with freeze_time("2000-01-05"): await user_fs1.sync() um1 = user_fs1.get_user_manifest() um2 = user_fs2.get_user_manifest() if dev2_has_changes: expected_base_um = UserManifest( author=device2.device_id, id=device2.user_manifest_id, timestamp=datetime(2000, 1, 4), version=3, created=um_created_v0_fs1, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=( WorkspaceEntry( name=EntryName("w1"), id=w1id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.OWNER, ), WorkspaceEntry( name=EntryName("w2"), id=w2id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_base_um.workspaces, speculative=False, ) else: expected_base_um = UserManifest( author=device1.device_id, timestamp=datetime(2000, 1, 3), id=device1.user_manifest_id, version=2, created=um_created_v0_fs1, updated=datetime(2000, 1, 1), last_processed_message=0, workspaces=(WorkspaceEntry( name=EntryName("w1"), id=w1id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.OWNER, ), ), ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=datetime(2000, 1, 1), last_processed_message=0, workspaces=expected_base_um.workspaces, speculative=False, ) um1 = _update_user_manifest_key(um1) um2 = _update_user_manifest_key(um2) assert um1 == expected_um assert um2 == expected_um
async def test_sync_placeholder( running_backend, backend_data_binder, local_device_factory, user_fs_factory, data_base_dir, initialize_local_user_manifest, with_workspace, initial_user_manifest, ): device = local_device_factory() await backend_data_binder.bind_device(device, initial_user_manifest="not_synced") await initialize_local_user_manifest( data_base_dir, device, initial_user_manifest=initial_user_manifest) async with user_fs_factory(device) as user_fs: um_v0 = user_fs.get_user_manifest() expected_um = LocalUserManifest.new_placeholder( device.device_id, id=device.user_manifest_id, timestamp=um_v0.created, speculative=(initial_user_manifest == "speculative_v0"), ) assert um_v0 == expected_um if with_workspace: with freeze_time("2000-01-02"): wid = await user_fs.workspace_create(EntryName("w1")) um = user_fs.get_user_manifest() expected_um = um_v0.evolve( updated=datetime(2000, 1, 2), workspaces=(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, ), ), ) um = _update_user_manifest_key(um) assert um == expected_um with freeze_time("2000-01-02"): await user_fs.sync() um = user_fs.get_user_manifest() expected_base_um = UserManifest( author=device.device_id, # Add extra time due to the user realm being already created at 2000-01-02 timestamp=datetime(2000, 1, 2).add(microseconds=MANIFEST_STAMP_AHEAD_US), id=device.user_manifest_id, version=1, created=expected_um.created, updated=expected_um.updated, last_processed_message=0, workspaces=expected_um.workspaces, ) expected_um = LocalUserManifest( base=expected_base_um, need_sync=False, updated=expected_um.updated, last_processed_message=0, workspaces=expected_base_um.workspaces, speculative=False, ) um = _update_user_manifest_key(um) assert um.base == expected_base_um assert um == expected_um