async def test_mountpoint_access_unicode(base_mountpoint, alice_user_fs, event_bus): weird_name = "ÉŸ奇怪😀🔫🐍" wid = await alice_user_fs.workspace_create(weird_name) workspace = alice_user_fs.get_workspace(wid) await workspace.touch(f"/{weird_name}") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) root_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(f"/")) # Work around trio issue #1308 (https://github.com/python-trio/trio/issues/1308) items = await trio.to_thread.run_sync( lambda: [path.name for path in Path(root_path).iterdir()]) assert items == [weird_name] item_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(f"/{weird_name}")) assert await trio.Path(item_path).exists()
async def test_cancel_mount_workspace(base_mountpoint, alice_user_fs, event_bus, timeout): """ This function tests the race conditions between the mounting of a workspace and trio cancellation. In particular, it produces interesting results when trying to unmount a workspace while it's still initializing. The following timeout values are useful for more thorough testing: [x * 0.00001 for x in range(2000, 2500)] """ wid = await alice_user_fs.workspace_create("w") # The timeout for `_stop_fuse_thread` is 1 second (3 seconds for macOS), # so let's use a slightly lower timeout to make sure a potential failure # doesn't go undetected. timeout = 3.0 if sys.platform == "darwin" else 1.0 with trio.fail_after(timeout * 0.9): async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: with trio.move_on_after(timeout) as cancel_scope: await mountpoint_manager.mount_workspace(wid) if cancel_scope.cancelled_caught: with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/")) else: path = trio.Path( mountpoint_manager.get_path_in_mountpoint( wid, FsPath("/"))) await path.exists() assert not await (path / "foo").exists()
async def test_get_path_in_mountpoint(base_mountpoint, alice_user_fs, event_bus): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create("mounted_wksp") wid2 = await alice_user_fs.workspace_create("not_mounted_wksp") workspace1 = alice_user_fs.get_workspace(wid) workspace2 = alice_user_fs.get_workspace(wid2) await workspace1.touch("/bar.txt") await workspace2.touch("/foo.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath("/bar.txt")) assert isinstance(bar_path, PurePath) # Windows uses drives, not base_mountpoint if os.name != "nt": expected = base_mountpoint / "mounted_wksp" / "bar.txt" assert str(bar_path) == str(expected.absolute()) assert await trio.Path(bar_path).exists() with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint(wid2, FsPath("/foo.txt"))
async def _mount_alice2_w_mountpoint(*, task_status=trio.TASK_STATUS_IGNORED): async with mountpoint_manager_factory( alice2_user_fs, alice2_user_fs.event_bus, base_mountpoint ) as alice2_mountpoint_manager: await alice2_mountpoint_manager.mount_workspace(wid) task_status.started() await trio.sleep_forever()
async def test_idempotent_mount(base_mountpoint, alice_user_fs, event_bus, manual_unmount): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/bar.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_txt = get_path_in_mountpoint(mountpoint_manager, wid, "/bar.txt") assert await bar_txt.exists() with pytest.raises(MountpointAlreadyMounted): await mountpoint_manager.mount_workspace(wid) assert await bar_txt.exists() await mountpoint_manager.unmount_workspace(wid) assert not await bar_txt.exists() with pytest.raises(MountpointNotMounted): await mountpoint_manager.unmount_workspace(wid) assert not await bar_txt.exists() await mountpoint_manager.mount_workspace(wid) assert await bar_txt.exists()
async def test_mountpoint_path_already_in_use_concurrent_with_non_empty_dir( monkeypatch, base_mountpoint, alice_user_fs): wid = await alice_user_fs.workspace_create("w") mountpoint_path = base_mountpoint.absolute() / "w" # Here instead of checking the path can be used as a mountpoint, we # actually make it unsuitable to check the following behavior async def _mocked_bootstrap_mountpoint(*args): trio_mountpoint_path = trio.Path(f"{mountpoint_path}") await trio_mountpoint_path.mkdir(parents=True) file_path = trio_mountpoint_path / "bar.txt" await file_path.touch() st_dev = (await trio_mountpoint_path.stat()).st_dev return mountpoint_path, st_dev monkeypatch.setattr( "parsec.core.mountpoint.fuse_runner._bootstrap_mountpoint", _mocked_bootstrap_mountpoint) # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as alice_mountpoint_manager: with pytest.raises(MountpointDriverCrash) as exc: await alice_mountpoint_manager.mount_workspace(wid) assert exc.value.args == ( f"Fuse has crashed on {mountpoint_path}: EPERM", )
async def test_unmount_with_fusermount(base_mountpoint, alice, alice_user_fs, event_bus): wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/bar.txt") async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: with event_bus.listen() as spy: mountpoint_path = await mountpoint_manager.mount_workspace(wid) command = f"fusermount -u {mountpoint_path}".split() expected = { "mountpoint": mountpoint_path, "workspace_id": wid, "timestamp": None } completed_process = await trio.run_process(command) with trio.fail_after(1): # fusermount might fail for some reasons while completed_process.returncode: completed_process = await trio.run_process(command) await spy.wait(CoreEvent.MOUNTPOINT_STOPPED, expected) assert not await trio.Path(mountpoint_path / "bar.txt").exists() # Mountpoint path should be removed on umounting assert not await trio.Path(mountpoint_path).exists()
async def test_mountpoint_iterdir_with_many_files(n, base_path, base_mountpoint, alice_user_fs, event_bus): wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.mkdir(base_path, parents=True, exist_ok=True) names = [f"some_file_{i:03d}.txt" for i in range(n)] path_list = [FsPath(f"{base_path}/{name}") for name in names] for path in path_list: await workspace.touch(path) # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) test_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(base_path)) # Work around trio issue #1308 (https://github.com/python-trio/trio/issues/1308) items = await trio.to_thread.run_sync( lambda: [path.name for path in Path(test_path).iterdir()]) assert items == names for path in path_list: item_path = mountpoint_manager.get_path_in_mountpoint(wid, path) assert await trio.Path(item_path).exists()
async def test_mount_and_explore_workspace(base_mountpoint, alice_user_fs, event_bus, manual_unmount): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.mkdir("/foo") await workspace.touch("/bar.txt") await workspace.write_bytes("/bar.txt", b"Hello world !") # Now we can start fuse with event_bus.listen() as spy: async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) mountpoint_path = get_path_in_mountpoint(mountpoint_manager, wid, "/") expected = { "mountpoint": mountpoint_path, "workspace_id": wid, "timestamp": None } spy.assert_events_occured([ (CoreEvent.MOUNTPOINT_STARTING, expected), (CoreEvent.MOUNTPOINT_STARTED, expected), ]) # Finally explore the mountpoint def inspect_mountpoint(): wksp_children = set(os.listdir(mountpoint_path)) assert wksp_children == {"foo", "bar.txt"} bar_stat = os.stat(f"{mountpoint_path}/bar.txt") assert bar_stat.st_size == len(b"Hello world !") with open(f"{mountpoint_path}/bar.txt", "rb") as fd: bar_txt = fd.read() assert bar_txt == b"Hello world !" # Note given python fs api is blocking, we must run it inside a thread # to avoid blocking the trio loop and ending up in a deadlock await trio.to_thread.run_sync(inspect_mountpoint) if manual_unmount: await mountpoint_manager.unmount_workspace(wid) # Mountpoint should be stopped by now spy.assert_events_occured([(CoreEvent.MOUNTPOINT_STOPPED, expected)]) if not manual_unmount: # Mountpoint should be stopped by now spy.assert_events_occured([(CoreEvent.MOUNTPOINT_STOPPED, expected) ])
async def test_mount_unknown_workspace(base_mountpoint, alice_user_fs, event_bus): async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: wid = uuid4() with pytest.raises(MountpointConfigurationError) as exc: await mountpoint_manager.mount_workspace(wid) assert exc.value.args == (f"Workspace `{wid}` doesn't exist", )
async def logged_core_factory(config: CoreConfig, device: LocalDevice, event_bus: Optional[EventBus] = None): event_bus = event_bus or EventBus() prevent_sync_pattern = get_prevent_sync_pattern( config.prevent_sync_pattern_path) backend_conn = BackendAuthenticatedConn( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, event_bus=event_bus, max_cooldown=config.backend_max_cooldown, max_pool=config.backend_max_connections, keepalive=config.backend_connection_keepalive, ) remote_devices_manager = RemoteDevicesManager(backend_conn.cmds, device.root_verify_key) async with UserFS.run( data_base_dir=config.data_base_dir, device=device, backend_cmds=backend_conn.cmds, remote_devices_manager=remote_devices_manager, event_bus=event_bus, prevent_sync_pattern=prevent_sync_pattern, preferred_language=config.gui_language, workspace_storage_cache_size=config.workspace_storage_cache_size, ) as user_fs: backend_conn.register_monitor( partial(monitor_messages, user_fs, event_bus)) backend_conn.register_monitor(partial(monitor_sync, user_fs, event_bus)) async with backend_conn.run(): async with mountpoint_manager_factory( user_fs, event_bus, config.mountpoint_base_dir, mount_all=config.mountpoint_enabled, mount_on_workspace_created=config.mountpoint_enabled, mount_on_workspace_shared=config.mountpoint_enabled, unmount_on_workspace_revoked=config.mountpoint_enabled, exclude_from_mount_all=config.disabled_workspaces, ) as mountpoint_manager: yield LoggedCore( config=config, device=device, event_bus=event_bus, mountpoint_manager=mountpoint_manager, user_fs=user_fs, remote_devices_manager=remote_devices_manager, backend_conn=backend_conn, )
async def test_inconsistent_folder_with_network(base_mountpoint, running_backend, alice_user_fs): async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as alice_mountpoint_manager: workspace = await create_inconsistent_workspace(alice_user_fs) mountpoint_path = Path(await alice_mountpoint_manager.mount_workspace( workspace.workspace_id)) await trio.to_thread.run_sync(_os_tests, mountpoint_path, errno.EACCES, WINDOWS_ERROR_PERMISSION_DENIED)
async def test_inconsistent_folder_no_network(base_mountpoint, running_backend, alice_user_fs): async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as alice_mountpoint_manager: workspace = await create_inconsistent_workspace(alice_user_fs) mountpoint_path = Path(await alice_mountpoint_manager.mount_workspace( workspace.workspace_id)) with running_backend.offline(): await trio.to_thread.run_sync(_os_tests, mountpoint_path, errno.EHOSTUNREACH, WINDOWS_ERROR_HOST_UNREACHABLE)
async def test_unmount_due_to_cancelled_scope(base_mountpoint, alice, alice_user_fs, event_bus): mountpoint_path = base_mountpoint / "w" wid = await alice_user_fs.workspace_create(EntryName("w")) with trio.CancelScope() as cancel_scope: async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint ) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) cancel_scope.cancel() # Mountpoint path should be removed on umounting assert not await trio.Path(mountpoint_path).exists()
async def test_mountpoint_path_already_in_use(base_mountpoint, running_backend, alice_user_fs, alice2_user_fs): # Create a workspace and make it available in two devices wid = await alice_user_fs.workspace_create("w") await alice_user_fs.sync() await alice2_user_fs.sync() # Easily differenciate alice&alice2 await alice2_user_fs.get_workspace(wid).touch("/I_am_alice2.txt") await alice_user_fs.get_workspace(wid).touch("/I_am_alice.txt") naive_workspace_path = (base_mountpoint / "w").absolute() # Default workspace path already exists, souldn't be able to use it await trio.Path(base_mountpoint / "w").mkdir(parents=True) await trio.Path(base_mountpoint / "w" / "bar.txt").touch() async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint ) as alice_mountpoint_manager, mountpoint_manager_factory( alice2_user_fs, alice2_user_fs.event_bus, base_mountpoint) as alice2_mountpoint_manager: # Alice mount the workspace first alice_mountpoint_path = await alice_mountpoint_manager.mount_workspace( wid) assert str(alice_mountpoint_path) == f"{naive_workspace_path} (2)" # Alice2 should also be able to mount the workspace without name clashing alice2_mountpoint_path = await alice2_mountpoint_manager.mount_workspace( wid) assert str(alice2_mountpoint_path) == f"{naive_workspace_path} (3)" # Finally make sure each workspace is well mounted assert await trio.Path(alice_mountpoint_path / "I_am_alice.txt" ).exists() assert await trio.Path(alice2_mountpoint_path / "I_am_alice2.txt" ).exists()
async def _run_mountpoint( base_mountpoint, bootstrap_cb, *, task_status=trio.TASK_STATUS_IGNORED ): device = local_device_factory() async with user_fs_factory(device) as user_fs: async with mountpoint_manager_factory( user_fs, user_fs.event_bus, base_mountpoint, debug=False ) as mountpoint_manager: if bootstrap_cb: await bootstrap_cb(user_fs, mountpoint_manager) task_status.started((user_fs, mountpoint_manager)) await trio.sleep_forever()
async def test_base_mountpoint_not_created(base_mountpoint, alice_user_fs, event_bus): # Path should be created if it doesn' exist base_mountpoint = base_mountpoint / "dummy/dummy/dummy" wid = await alice_user_fs.workspace_create("w") workspace = alice_user_fs.get_workspace(wid) await workspace.touch("/bar.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_txt = get_path_in_mountpoint(mountpoint_manager, wid, "/bar.txt") assert await bar_txt.exists()
async def test_runner_not_available(monkeypatch, alice_user_fs, event_bus): base_mountpoint = Path("/foo") def _import(name): if name == "winfspy": raise RuntimeError() else: raise ImportError() monkeypatch.setattr("parsec.core.mountpoint.manager.import_function", _import) with pytest.raises( (MountpointFuseNotAvailable, MountpointWinfspNotAvailable)): async with mountpoint_manager_factory(alice_user_fs, event_bus, base_mountpoint): pass
async def test_cancel_mount_workspace(base_mountpoint, alice_user_fs, event_bus, timeout): wid = await alice_user_fs.workspace_create("w") async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: with trio.move_on_after(timeout) as cancel_scope: await mountpoint_manager.mount_workspace(wid) if cancel_scope.cancelled_caught: with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/")) else: path = trio.Path( mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/"))) await path.exists() assert not await (path / "foo").exists()
async def test_mountpoint_path_already_in_use_concurrent_with_mountpoint( monkeypatch, base_mountpoint, running_backend, alice_user_fs, alice2_user_fs): # Create a workspace and make it available in two devices wid = await alice_user_fs.workspace_create("w") await alice_user_fs.sync() await alice2_user_fs.sync() mountpoint_path = base_mountpoint.absolute() / "w" async def _mount_alice2_w_mountpoint(*, task_status=trio.TASK_STATUS_IGNORED): async with mountpoint_manager_factory( alice2_user_fs, alice2_user_fs.event_bus, base_mountpoint) as alice2_mountpoint_manager: await alice2_mountpoint_manager.mount_workspace(wid) task_status.started() await trio.sleep_forever() async with trio.open_service_nursery() as nursery: await nursery.start(_mount_alice2_w_mountpoint) # Here instead of checking the path can be used as a mountpoint, we # actually lead it into error async def _mocked_bootstrap_mountpoint(*args): trio_mountpoint_path = trio.Path(f"{mountpoint_path}") st_dev = (await trio_mountpoint_path.stat()).st_dev return mountpoint_path, st_dev monkeypatch.setattr( "parsec.core.mountpoint.fuse_runner._bootstrap_mountpoint", _mocked_bootstrap_mountpoint) # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as alice_mountpoint_manager: with pytest.raises(MountpointDriverCrash) as exc: await alice_mountpoint_manager.mount_workspace(wid) assert exc.value.args == ( f"Fuse has crashed on {mountpoint_path}: EPERM", ) # Test is over, stop alice2 mountpoint and exit nursery.cancel_scope.cancel()
async def logged_core_factory( config: CoreConfig, device: LocalDevice, event_bus: Optional[EventBus] = None ): event_bus = event_bus or EventBus() backend_conn = BackendAuthenticatedConn( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, event_bus=event_bus, max_cooldown=config.backend_max_cooldown, max_pool=config.backend_max_connections, keepalive=config.backend_connection_keepalive, ) path = config.data_base_dir / device.slug remote_devices_manager = RemoteDevicesManager(backend_conn.cmds, device.root_verify_key) async with UserFS.run( device, path, backend_conn.cmds, remote_devices_manager, event_bus ) as user_fs: backend_conn.register_monitor(partial(monitor_messages, user_fs, event_bus)) backend_conn.register_monitor(partial(monitor_sync, user_fs, event_bus)) async with backend_conn.run(): async with mountpoint_manager_factory( user_fs, event_bus, config.mountpoint_base_dir ) as mountpoint_manager: yield LoggedCore( config=config, device=device, event_bus=event_bus, remote_devices_manager=remote_devices_manager, mountpoint_manager=mountpoint_manager, backend_conn=backend_conn, user_fs=user_fs, )
async def test_cancel_mount_workspace(base_mountpoint, alice_user_fs, event_bus): """ This function tests the race conditions between the mounting of a workspace and trio cancellation. In particular, it produces interesting results when trying to unmount a workspace while it's still initializing. The following timeout values are useful for more thorough testing: [x * 0.00001 for x in range(2000, 2500)] """ wid = await alice_user_fs.workspace_create(EntryName("w")) # Reuse the same mountpoint manager for all the mountings to # make sure state is not polutated by previous mount attempts async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: for timeout in count(0, 0.002): print(f"timeout: {timeout}") async with real_clock_timeout(): with trio.move_on_after(timeout) as cancel_scope: await mountpoint_manager.mount_workspace(wid) if cancel_scope.cancelled_caught: with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint( wid, FsPath("/")) else: # Sanity check path = trio.Path( mountpoint_manager.get_path_in_mountpoint( wid, FsPath("/"))) await path.exists() # Timeout has become too high to be useful, time to stop the test break
async def test_hard_crash_in_fuse_thread(base_mountpoint, alice_user_fs): wid = await alice_user_fs.workspace_create("w") mountpoint_path = base_mountpoint / "w" class ToughLuckError(Exception): pass def _crash_fuse(*args, **kwargs): raise ToughLuckError("Tough luck...") with patch("parsec.core.mountpoint.fuse_runner.FUSE", new=_crash_fuse): async with mountpoint_manager_factory( alice_user_fs, alice_user_fs.event_bus, base_mountpoint) as mountpoint_manager: with pytest.raises(MountpointDriverCrash) as exc: await mountpoint_manager.mount_workspace(wid) assert exc.value.args == ( f"Fuse has crashed on {mountpoint_path}: Unknown error code: Tough luck...", ) # Mountpoint path should be removed on umounting assert not await trio.Path(mountpoint_path).exists()
async def logged_core_factory( config: CoreConfig, device: LocalDevice, event_bus: Optional[EventBus] = None, mountpoint: Optional[Path] = None, ): if config.mountpoint_enabled and os.name == "nt": logger.warning("Mountpoint disabled (not supported yet on Windows)") config = config.evolve(mountpoint_enabled=False) event_bus = event_bus or EventBus() # Plenty of nested scope to order components init/teardown async with trio.open_nursery() as root_nursery: # TODO: Currently backend_listen_events connect to backend and # switch to listen events mode, then monitors kick in and send it # events about which beacons to listen on, obliging to restart the # listen connection... backend_online = await root_nursery.start(backend_listen_events, device, event_bus) async with backend_cmds_factory( device.organization_addr, device.device_id, device.signing_key, config.backend_max_connections, ) as backend_cmds_pool: local_db = LocalDB(config.data_base_dir / device.device_id) encryption_manager = EncryptionManager(device, local_db, backend_cmds_pool) fs = FS(device, local_db, backend_cmds_pool, encryption_manager, event_bus) async with trio.open_nursery() as monitor_nursery: # Finally start monitors # Monitor connection must be first given it will watch on # other monitors' events await monitor_nursery.start(monitor_backend_connection, backend_online, event_bus) await monitor_nursery.start(monitor_beacons, device, fs, event_bus) await monitor_nursery.start(monitor_messages, backend_online, fs, event_bus) await monitor_nursery.start(monitor_sync, backend_online, fs, event_bus) # TODO: rework mountpoint manager to avoid init/teardown mountpoint_manager = mountpoint_manager_factory(fs, event_bus) await mountpoint_manager.init(monitor_nursery) if config.mountpoint_enabled: if not mountpoint: mountpoint = config.mountpoint_base_dir / device.device_id await mountpoint_manager.start(mountpoint) try: yield LoggedCore( config=config, device=device, local_db=local_db, event_bus=event_bus, encryption_manager=encryption_manager, mountpoint_manager=mountpoint_manager, backend_cmds=backend_cmds_pool, fs=fs, ) root_nursery.cancel_scope.cancel() finally: if config.mountpoint_enabled: await mountpoint_manager.teardown()
async def test_mountpoint_revoke_access( base_mountpoint, alice_user_fs, alice2_user_fs, bob_user_fs, event_bus, running_backend, revoking, ): # Parametrization new_role = None if revoking == "read" else WorkspaceRole.READER # Bob creates and share two files with Alice wid = await create_shared_workspace("w", bob_user_fs, alice_user_fs, alice2_user_fs) workspace = bob_user_fs.get_workspace(wid) await workspace.touch("/foo.txt") await workspace.touch("/bar.txt") await workspace.touch("/to_delete.txt") await workspace.sync() def get_root_path(mountpoint_manager): root_path = mountpoint_manager.get_path_in_mountpoint(wid, FsPath("/")) # A trio path is required here, otherwise we risk a messy deadlock! return trio.Path(root_path) async def assert_cannot_read(mountpoint_manager, root_is_cached=False): root_path = get_root_path(mountpoint_manager) foo_path = root_path / "foo.txt" bar_path = root_path / "bar.txt" # For some reason, root_path.stat() does not trigger a new getattr call # to fuse operations if there has been a prior recent call to stat. if not root_is_cached: with pytest.raises(PermissionError): await root_path.stat() with pytest.raises(PermissionError): await foo_path.exists() with pytest.raises(PermissionError): await foo_path.read_bytes() with pytest.raises(PermissionError): await bar_path.exists() with pytest.raises(PermissionError): await bar_path.read_bytes() async def assert_cannot_write(mountpoint_manager, new_role): expected_error, expected_errno = PermissionError, errno.EACCES # On linux, errno.EROFS is not translated to a PermissionError if new_role is WorkspaceRole.READER and os.name != "nt": expected_error, expected_errno = OSError, errno.EROFS root_path = get_root_path(mountpoint_manager) foo_path = root_path / "foo.txt" bar_path = root_path / "bar.txt" with pytest.raises(expected_error) as ctx: await (root_path / "new_file.txt").touch() assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await (root_path / "new_directory").mkdir() assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await foo_path.write_bytes(b"foo contents") assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await foo_path.unlink() assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await bar_path.write_bytes(b"bar contents") assert ctx.value.errno == expected_errno with pytest.raises(expected_error) as ctx: await bar_path.unlink() async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: # Mount Bob workspace on Alice's side await mountpoint_manager.mount_workspace(wid) root_path = get_root_path(mountpoint_manager) # Alice can read await (root_path / "bar.txt").read_bytes() # Alice can write await (root_path / "bar.txt").write_bytes(b"test") # Alice can delete await (root_path / "to_delete.txt").unlink() assert not await (root_path / "to_delete.txt").exists() # Bob revokes Alice's read or write rights from her workspace await bob_user_fs.workspace_share(wid, alice_user_fs.device.user_id, new_role) # Let Alice process the info await alice_user_fs.process_last_messages() await alice2_user_fs.process_last_messages() # Alice still has read access if new_role is WorkspaceRole.READER: await (root_path / "bar.txt").read_bytes() # Alice no longer has read access else: await assert_cannot_read(mountpoint_manager, root_is_cached=True) # Alice no longer has write access await assert_cannot_write(mountpoint_manager, new_role) # Try again with Alice first device async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: # Mount alice workspace on bob's side once again await mountpoint_manager.mount_workspace(wid) root_path = get_root_path(mountpoint_manager) # Alice still has read access if new_role is WorkspaceRole.READER: await (root_path / "bar.txt").read_bytes() # Alice no longer has read access else: await assert_cannot_read(mountpoint_manager, root_is_cached=True) # Alice no longer has write access await assert_cannot_write(mountpoint_manager, new_role) # Try again with Alice second device async with mountpoint_manager_factory( alice2_user_fs, event_bus, base_mountpoint) as mountpoint_manager: # Mount alice workspace on bob's side once again await mountpoint_manager.mount_workspace(wid) root_path = get_root_path(mountpoint_manager) # Alice still has read access if new_role is WorkspaceRole.READER: await (root_path / "bar.txt").read_bytes() # Alice no longer has read access else: await assert_cannot_read(mountpoint_manager, root_is_cached=True) # Alice no longer has write access await assert_cannot_write(mountpoint_manager, new_role)