async def test_new_sharing_trigger_event(alice_core, bob_core, running_backend): # First, create a folder and sync it on backend with freeze_time("2000-01-01"): wid = await alice_core.user_fs.workspace_create("foo") workspace = alice_core.user_fs.get_workspace(wid) with freeze_time("2000-01-02"): await workspace.sync() # Now we can share this workspace with Bob with bob_core.event_bus.listen() as spy: with freeze_time("2000-01-03"): await alice_core.user_fs.workspace_share( wid, recipient="bob", role=WorkspaceRole.MANAGER) # Bob should get a notification await spy.wait_with_timeout( "sharing.updated", { "new_entry": WorkspaceEntry( name="foo (shared by alice)", id=wid, key=ANY, encryption_revision=1, encrypted_on=Pendulum(2000, 1, 1), role_cached_on=ANY, role=WorkspaceRole.MANAGER, ), "previous_entry": None, }, )
async def test_share_workspace_then_rename_it( running_backend, alice_user_fs, bob_user_fs, alice, bob ): # Share a workspace between Alice and Bob with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create("w") 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() # Now Bob and alice both rename the workpsace for there own taste await bob_user_fs.workspace_rename(wid, "from_alice") await alice_user_fs.workspace_rename(wid, "to_bob") await bob_user_fs.sync() await alice_user_fs.sync() # This should have not changed the workspace in any way bw = bob_user_fs.get_workspace(wid) aw = alice_user_fs.get_workspace(wid) await bw.touch("/ping_bob.txt") await aw.mkdir("/ping_alice") await bw.sync() await aw.sync() await bw.sync() aw_stat = await aw.path_info("/") bw_stat = await bw.path_info("/") assert aw_stat == bw_stat assert aw_stat["id"] == wid
async def test_sync_then_clean_start(autojump_clock, running_backend, client_factory, alice, alice2): # Given the clients are initialized while the backend is online, we are # guaranteed they are connected async with client_factory() as alice_client: await alice_client.login(alice) async with wait_for_entries_synced(alice_client, ("/", "/foo.txt")): with freeze_time("2000-01-02"): await alice_client.user_fs.touch("/foo.txt") with freeze_time("2000-01-03"): await alice_client.user_fs.file_write("/foo.txt", b"v1") await alice_client.user_fs.sync("/foo.txt") async with client_factory() as alice2_client2, wait_for_entries_synced( alice2_client2, ["/"]): await alice2_client2.login(alice2) for path in ("/", "/foo.txt"): stat = await alice_client.user_fs.stat(path) stat2 = await alice2_client2.user_fs.stat(path) assert stat2 == stat
async def test_create_already_existing_block(running_backend, alice_user_fs, alice2_user_fs): # First create&sync an empty file with freeze_time("2000-01-02"): wid = await create_shared_workspace("w", alice_user_fs, alice2_user_fs) workspace = alice_user_fs.get_workspace(wid) workspace2 = alice2_user_fs.get_workspace(wid) await workspace.touch("/foo.txt") foo_id = await workspace.path_id("/foo.txt") await workspace.sync_by_id(foo_id) path_info = await workspace.path_info("/foo.txt") assert path_info["base_version"] == 1 # Now hack a bit the fs to simulate poor connection with backend vanilla_backend_block_create = alice_user_fs._syncer._backend_block_create async def mocked_backend_block_create(*args, **kwargs): await vanilla_backend_block_create(*args, **kwargs) raise BackendNotAvailable() alice_user_fs._syncer._backend_block_create = mocked_backend_block_create # Write into the file locally and try to sync this. # We should end up with a block synced in the backend but still considered # as a dirty block in the fs. with freeze_time("2000-01-03"): await workspace.write_bytes("/foo.txt", b"data") with pytest.raises(BackendNotAvailable): await workspace.sync_by_id(foo_id) # Now retry the sync with a good connection, we should be able to reach # eventual consistency. alice_user_fs._syncer._backend_block_create = vanilla_backend_block_create await workspace.sync_by_id(foo_id) # Finally test this so-called consistency ;-) data = await workspace.read_bytes("/foo.txt") assert data == b"data" path_info = await workspace.path_info("/foo.txt") assert path_info == { "type": "file", "id": ANY, "is_placeholder": False, "need_sync": False, "created": datetime(2000, 1, 2), "updated": datetime(2000, 1, 3), "base_version": 2, "size": 4, "confinement_point": None, } await workspace2.sync() data2 = await workspace2.read_bytes("/foo.txt") assert data2 == b"data"
async def testbed(running_backend, alice_user_fs, alice, bob): with freeze_time("2000-01-01"): wid = await alice_user_fs.workspace_create(EntryName("w1")) workspace = alice_user_fs.get_workspace(wid) await workspace.sync() local_manifest = await workspace.local_storage.get_manifest(wid) with freeze_time("2000-01-03"): await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.MANAGER) class TestBed: def __init__(self): self._next_version = 2 self.defaults = { "local_manifest": local_manifest, "blob": None, "signed_author": alice.device_id, "backend_author": alice.device_id, "signed_timestamp": datetime(2000, 1, 2), "backend_timestamp": datetime(2000, 1, 2), "author_signkey": alice.signing_key, "key": workspace.get_workspace_entry().key, } async def run(self, exc_msg, **kwargs): options = {**self.defaults, **kwargs} if options["blob"] is None: to_sync_um = options["local_manifest"].to_remote( author=options["signed_author"], timestamp=options["signed_timestamp"]) options["blob"] = to_sync_um.dump_sign_and_encrypt( author_signkey=options["author_signkey"], key=options["key"]) await running_backend.backend.vlob.update( organization_id=alice.organization_id, author=options["backend_author"], encryption_revision=1, vlob_id=VlobID(wid.uuid), version=self._next_version, timestamp=options["backend_timestamp"], blob=options["blob"], ) self._next_version += 1 # This should trigger FSError with pytest.raises(FSError) as exc: await workspace.sync() assert str(exc.value) == exc_msg # Also test timestamped workspace # Note: oxidation doesn't implement WorkspaceStorageTimestamped if not IS_OXIDIZED: with pytest.raises(FSError) as exc: await workspace.to_timestamped(options["backend_timestamp"] ) assert str(exc.value) == exc_msg return TestBed()
async def test_retrieve_user(running_backend, alice_remote_devices_manager, bob): remote_devices_manager = alice_remote_devices_manager d1 = datetime(2000, 1, 1) with freeze_time(d1): # Offline with no cache with pytest.raises(RemoteDevicesManagerBackendOfflineError): with running_backend.offline(): await remote_devices_manager.get_user(bob.user_id) # Online user, revoked_user = await remote_devices_manager.get_user(bob.user_id) assert user.user_id == bob.user_id assert user.public_key == bob.public_key assert revoked_user is None # Offline with cache with running_backend.offline(): user2, revoked_user2 = await remote_devices_manager.get_user(bob.user_id) assert user2 is user assert revoked_user2 is None d2 = d1.add(remote_devices_manager.cache_validity + 1) with freeze_time(d2): # Offline with cache expired with pytest.raises(RemoteDevicesManagerBackendOfflineError): with running_backend.offline(): await remote_devices_manager.get_user(bob.user_id) # Online with cache expired user, revoked_user = await remote_devices_manager.get_user(bob.user_id) assert user.user_id == bob.user_id assert user.public_key == bob.public_key assert revoked_user is None
async def test_retrieve_device(running_backend, alice_remote_devices_manager, bob): remote_devices_manager = alice_remote_devices_manager d1 = datetime(2000, 1, 1) with freeze_time(d1): # Offline with no cache with pytest.raises(RemoteDevicesManagerBackendOfflineError): with running_backend.offline(): await remote_devices_manager.get_device(bob.device_id) # Online device = await remote_devices_manager.get_device(bob.device_id) assert device.device_id == bob.device_id assert device.verify_key == bob.verify_key # Offline with cache with running_backend.offline(): device2 = await remote_devices_manager.get_device(bob.device_id) assert device2 is device d2 = d1.add(remote_devices_manager.cache_validity + 1) with freeze_time(d2): # Offline with cache expired with pytest.raises(RemoteDevicesManagerBackendOfflineError): with running_backend.offline(): await remote_devices_manager.get_device(bob.device_id) # Online with cache expired device = await remote_devices_manager.get_device(bob.device_id) assert device.device_id == bob.device_id assert device.verify_key == bob.verify_key
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} )
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
async def test_get_role_certificates_multiple( backend, alice, bob, adam, bob_backend_sock, realm, backend_realm_generate_certif_and_update_roles, ): # Realm is created on 2000-01-02 with freeze_time("2000-01-03"): c3 = await backend_realm_generate_certif_and_update_roles( backend, alice, realm, bob.user_id, RealmRole.OWNER) with freeze_time("2000-01-04"): c4 = await backend_realm_generate_certif_and_update_roles( backend, bob, realm, adam.user_id, RealmRole.MANAGER) with freeze_time("2000-01-05"): c5 = await backend_realm_generate_certif_and_update_roles( backend, bob, realm, alice.user_id, RealmRole.READER) with freeze_time("2000-01-06"): c6 = await backend_realm_generate_certif_and_update_roles( backend, bob, realm, alice.user_id, None) rep = await realm_get_role_certificates(bob_backend_sock, realm) assert rep == {"status": "ok", "certificates": [ANY, c3, c4, c5, c6]}
async def test_update_invalid_timestamp(running_backend, alice_user_fs, alice2_user_fs): with freeze_time("2000-01-01"): wid = await create_shared_workspace(EntryName("w"), alice_user_fs, alice2_user_fs) workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/foo.txt") with freeze_time("2000-01-02") as t2: await workspace.sync() await workspace.write_bytes("/foo.txt", b"ok") with freeze_time("2000-01-03") as t3: await workspace.sync() await workspace.write_bytes("/foo.txt", b"ko") with freeze_time(t2): with pytest.raises(FSRemoteOperationError) as context: await workspace.sync() cause = context.value.__cause__ assert isinstance(cause, BackendOutOfBallparkError) rep, = cause.args assert rep == { "status": "bad_timestamp", "client_timestamp": t3.add(microseconds=MANIFEST_STAMP_AHEAD_US), "backend_timestamp": t2, "ballpark_client_early_offset": BALLPARK_CLIENT_EARLY_OFFSET, "ballpark_client_late_offset": BALLPARK_CLIENT_LATE_OFFSET, }
async def test_online_sync2(autojump_clock, running_backend, client_factory, alice, alice2): # Given the clients are initialized while the backend is online, we are # guaranteed they are connected async with client_factory() as alice_client, client_factory( ) as alice2_client2: await alice_client.login(alice) await alice2_client2.login(alice2) # FS does a full sync at startup, wait for it to finish await alice_client.user_fs.wait_not_syncing() await alice2_client2.user_fs.wait_not_syncing() async with wait_for_entries_synced(alice2_client2, ["/"]), wait_for_entries_synced( alice_client, ("/", "/foo.txt")): with freeze_time("2000-01-02"): await alice_client.user_fs.touch("/foo.txt") with freeze_time("2000-01-03"): await alice_client.user_fs.file_write("/foo.txt", b"hello world !") await alice_client.user_fs.sync("/foo.txt") stat = await alice_client.user_fs.stat("/foo.txt") stat2 = await alice2_client2.user_fs.stat("/foo.txt") assert stat2 == stat
async def test_rename_workspace(initial_user_manifest_state, alice_user_fs, alice): with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create("w1") with freeze_time("2000-01-03"): await alice_user_fs.workspace_rename(wid, "w2") um = alice_user_fs.get_user_manifest() expected_base_um = initial_user_manifest_state.get_user_manifest_v1_for_backend( alice) expected_um = LocalUserManifest( base=expected_base_um, need_sync=True, updated=datetime(2000, 1, 3), last_processed_message=expected_base_um.last_processed_message, workspaces=(WorkspaceEntry( name="w2", id=wid, key=ANY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ), ) assert um == expected_um
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_fast_forward_on_offline_during_sync(autojump_clock, server_factory, backend, client_factory, alice, alice2): # Create two servers to be able to turn offline a single one async with server_factory( backend.handle_client) as server1, server_factory( backend.handle_client) as server2: # Given the clients are initialized while the backend is online, we are # guaranteed they are connected async with client_factory( config={"backend_addr": server1.addr} ) as alice_client, client_factory( config={"backend_addr": server2.addr}) as alice2_client2: await alice_client.login(alice) await alice2_client2.login(alice2) # TODO: shouldn't need this... await trio.testing.wait_all_tasks_blocked(cushion=0.1) async with wait_for_entries_synced(alice2_client2, ["/"]), wait_for_entries_synced( alice_client, ("/", "/foo.txt")): with freeze_time("2000-01-02"): await alice_client.user_fs.touch("/foo.txt") with freeze_time("2000-01-03"): await alice_client.user_fs.file_write("/foo.txt", b"v1") # Sync should be done in the background by the sync monitor ########### shouldn't need to do that... ####### await alice_client.user_fs.sync("/foo.txt") # TODO: shouldn't need this... await trio.testing.wait_all_tasks_blocked(cushion=0.1) # client goes offline, other client2 is still connected to backend async with wait_for_entries_synced(alice_client, ("/", "/foo.txt")): stat2 = await alice2_client2.user_fs.stat("/foo.txt") with offline(server1.addr): with freeze_time("2000-01-04"): await alice2_client2.user_fs.file_write( "/foo.txt", b"v2") await alice2_client2.user_fs.folder_create("/bar") async with wait_for_entries_synced( alice2_client2, ("/", "/bar", "/foo.txt")): await alice2_client2.user_fs.sync() for path in ("/", "/bar", "/foo.txt"): stat = await alice_client.user_fs.stat(path) stat2 = await alice2_client2.user_fs.stat(path) assert stat2 == stat
async def test_create_certif_too_old(alice, alice_backend_sock): now = pendulum.now() # Generate a certificate realm_id = RealmID.from_hex("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=now, realm_id=realm_id).dump_and_sign(alice.signing_key) # Create a realm a tiny bit too late later = now.add(seconds=BALLPARK_CLIENT_LATE_OFFSET) with freeze_time(later): rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "bad_timestamp", "backend_timestamp": later, "ballpark_client_early_offset": BALLPARK_CLIENT_EARLY_OFFSET, "ballpark_client_late_offset": BALLPARK_CLIENT_LATE_OFFSET, "client_timestamp": now, } # Create a realm late but right before the deadline later = now.add(seconds=BALLPARK_CLIENT_LATE_OFFSET, microseconds=-1) with freeze_time(later): rep = await realm_create(alice_backend_sock, certif) assert rep["status"] == "ok" # Generate a new certificate realm_id = RealmID.from_hex("C0000000000000000000000000000001") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=now, realm_id=realm_id).dump_and_sign(alice.signing_key) # Create a realm a tiny bit too soon sooner = now.subtract(seconds=BALLPARK_CLIENT_EARLY_OFFSET) with freeze_time(sooner): rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "bad_timestamp", "backend_timestamp": sooner, "ballpark_client_early_offset": BALLPARK_CLIENT_EARLY_OFFSET, "ballpark_client_late_offset": BALLPARK_CLIENT_LATE_OFFSET, "client_timestamp": now, } # Create a realm soon but after the limit sooner = now.subtract(seconds=BALLPARK_CLIENT_EARLY_OFFSET, microseconds=-1) with freeze_time(sooner): rep = await realm_create(alice_backend_sock, certif) assert rep["status"] == "ok"
async def test_remove_role_idempotent( alice, bob, alice_backend_sock, realm, start_with_existing_role, realm_generate_certif_and_update_roles_or_fail, ): if start_with_existing_role: with freeze_time("2000-01-03"): rep = await realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, RealmRole.MANAGER) assert rep == {"status": "ok"} with freeze_time("2000-01-04"): rep = await realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, None) if start_with_existing_role: assert rep == {"status": "ok"} else: assert rep == {"status": "already_granted"} with freeze_time("2000-01-05"): rep = await realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, None) assert rep == {"status": "already_granted"} certifs = await _realm_get_clear_role_certifs(alice_backend_sock, realm) expected_certifs = [ RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 2), realm_id=realm, user_id=alice.user_id, role=RealmRole.OWNER, ) ] if start_with_existing_role: expected_certifs += [ RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 3), realm_id=realm, user_id=bob.user_id, role=RealmRole.MANAGER, ), RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 4), realm_id=realm, user_id=bob.user_id, role=None, ), ] assert certifs == expected_certifs
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 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_new_empty_entry(type, running_backend, alice_user_fs, alice2_user_fs): with freeze_time("2000-01-01"): wid = await create_shared_workspace("w", alice_user_fs, alice2_user_fs) workspace = alice_user_fs.get_workspace(wid) with freeze_time("2000-01-02"): if type == "file": await workspace.touch("/foo") else: await workspace.mkdir("/foo") info = await workspace.path_info("/foo") fid = info["id"] with alice_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-03"): await workspace.sync() if type == "file": # TODO: file and folder should generate the same events after the migration expected_events = [ ("fs.entry.synced", {"workspace_id": wid, "id": wid}, Pendulum(2000, 1, 3)) ] else: expected_events = [ ("fs.entry.synced", {"workspace_id": wid, "id": fid}, Pendulum(2000, 1, 3)), ("fs.entry.synced", {"workspace_id": wid, "id": wid}, Pendulum(2000, 1, 3)), ] spy.assert_events_occured(expected_events) workspace2 = alice2_user_fs.get_workspace(wid) await workspace2.sync() info = await workspace.path_info("/foo") if type == "file": assert info == { "type": "file", "id": ANY, "is_placeholder": False, "need_sync": False, "base_version": 1, "created": Pendulum(2000, 1, 2), "updated": Pendulum(2000, 1, 2), "size": 0, } else: assert info == { "type": "folder", "id": ANY, "is_placeholder": False, "need_sync": False, "base_version": 1, "created": Pendulum(2000, 1, 2), "updated": Pendulum(2000, 1, 2), "children": [], } info2 = await workspace2.path_info("/foo") assert info == info2
async def test_reloading_v0_user_manifest(running_backend, backend_data_binder, local_storage_factory, user_fs_factory, coolorg, alice): # Initialize backend and local storage with freeze_time("2000-01-01"): await backend_data_binder.bind_organization( coolorg, alice, initial_user_manifest_in_v0=True) local_storage = await local_storage_factory(alice, user_manifest_in_v0=True) # Create a workspace without syncronizing async with user_fs_factory(alice, local_storage) as user_fs: with freeze_time("2000-01-02"): wid = await user_fs.workspace_create("foo") workspace = user_fs.get_workspace(wid) await local_storage.clear_memory_cache() # Reload version 0 manifest async with user_fs_factory(alice, local_storage) as user_fs: with freeze_time("2000-01-02"): path_info = await workspace.path_info("/") assert path_info == { "type": "root", "id": alice.user_manifest_id, "created": datetime(2000, 1, 2), "updated": datetime(2000, 1, 2), "base_version": 0, "is_folder": True, "is_placeholder": True, "need_sync": True, "children": ["foo"], } await local_storage.clear_memory_cache() # Syncronize version 0 manifest async with user_fs_factory(alice, local_storage) as user_fs: with freeze_time("2000-01-03"): await user_fs.sync() path_info = await workspace.path_info("/") assert path_info == { "type": "root", "id": ANY, "created": datetime(2000, 1, 2), "updated": datetime(2000, 1, 2), "base_version": 1, "is_folder": True, "is_placeholder": False, "need_sync": False, "children": ["foo"], }
async def test_create_workspace_same_name(alice_user_fs): with freeze_time("2000-01-02"): w1id = await alice_user_fs.workspace_create("w") with freeze_time("2000-01-03"): w2id = await alice_user_fs.workspace_create("w") um = alice_user_fs.get_user_manifest() assert um.updated == Pendulum(2000, 1, 3) assert len(um.workspaces) == 2 assert [(x.id, x.name) for x in um.workspaces] == [(w1id, "w"), (w2id, "w")]
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 }, )
async def test_create_workspace_same_name(alice_user_fs): with freeze_time("2000-01-02"): w1id = await alice_user_fs.workspace_create(EntryName("w")) with freeze_time("2000-01-03"): w2id = await alice_user_fs.workspace_create(EntryName("w")) um = alice_user_fs.get_user_manifest() assert um.updated == datetime(2000, 1, 3) assert len(um.workspaces) == 2 assert sorted((x.id, x.name) for x in um.workspaces) == sorted([(w1id, EntryName("w")), (w2id, EntryName("w"))])
async def test_user_manifest_access_while_speculative(user_fs_factory, alice): with freeze_time("2000-01-01"): async with user_fs_factory(alice) as user_fs: with freeze_time("2000-01-02"): user_manifest = user_fs.get_user_manifest() assert user_manifest.to_stats() == { "id": alice.user_manifest_id, "base_version": 0, "created": datetime(2000, 1, 1), "updated": datetime(2000, 1, 1), "is_placeholder": True, "need_sync": True, }
async def test_update_invalid_timestamp(running_backend, alice_user_fs, alice2_user_fs): with freeze_time("2000-01-01"): wid = await create_shared_workspace("w", alice_user_fs, alice2_user_fs) workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/foo.txt") with freeze_time("2000-01-01"): await workspace.sync() await workspace.write_bytes("/foo.txt", b"ok") with freeze_time("2000-01-03"): await workspace.sync() await workspace.write_bytes("/foo.txt", b"ko") with freeze_time("2000-01-02"): with pytest.raises(FSBackendOfflineError): await workspace.sync()
async def test_concurrent_devices_agree_on_workspace_manifest( running_backend, user_fs_factory, data_base_dir, initialize_local_user_manifest, alice, alice2): await initialize_local_user_manifest(data_base_dir, alice, initial_user_manifest="v1") await initialize_local_user_manifest(data_base_dir, alice2, initial_user_manifest="v1") async with user_fs_factory(alice) as alice_user_fs: async with user_fs_factory(alice2) as alice2_user_fs: with freeze_time("2000-01-01"): wksp_id = await alice_user_fs.workspace_create( EntryName("wksp")) # Sync user manifest (containing the workspace entry), but # not the corresponding workspace manifest ! with freeze_time("2000-01-02"): await alice_user_fs.sync() # Retrieve the user manifest but not the workpace manifest, Alice2 hence has a speculative workspace manifest with freeze_time("2000-01-03"): await alice2_user_fs.sync() # Now workspace diverge between devices alice_wksp = alice_user_fs.get_workspace(wksp_id) alice2_wksp = alice2_user_fs.get_workspace(wksp_id) with freeze_time("2000-01-04"): await alice_wksp.mkdir("/from_alice") with freeze_time("2000-01-05"): await alice2_wksp.mkdir("/from_alice2") # Sync user_fs2 first to ensure created_on field is # kept even if further syncs have an earlier value with freeze_time("2000-01-06"): await alice2_wksp.sync() with freeze_time("2000-01-07"): await alice_wksp.sync() with freeze_time("2000-01-08"): await alice2_wksp.sync() # Now, both user fs should have the same view on workspace expected_alice_wksp_stat = { "id": wksp_id, "base_version": 3, "created": datetime(2000, 1, 1), "updated": datetime(2000, 1, 7), "is_placeholder": False, "need_sync": False, "type": "folder", "children": [EntryName("from_alice"), EntryName("from_alice2")], "confinement_point": None, } alice_wksp_stat = await alice_wksp.path_info("/") alice2_wksp_stat = await alice2_wksp.path_info("/") assert alice_wksp_stat == expected_alice_wksp_stat assert alice2_wksp_stat == expected_alice_wksp_stat
async def test_get_role_certificates_no_longer_allowed( backend, alice, bob, alice_backend_sock, realm, backend_realm_generate_certif_and_update_roles): # Realm is created on 2000-01-02 with freeze_time("2000-01-03"): await backend_realm_generate_certif_and_update_roles( backend, alice, realm, bob.user_id, RealmRole.OWNER) with freeze_time("2000-01-04"): await backend_realm_generate_certif_and_update_roles( backend, bob, realm, alice.user_id, None) rep = await realm_get_role_certificates(alice_backend_sock, realm) assert rep == {"status": "not_allowed"}
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