async def test_tab_login_logout(aqtbot, running_backend, gui_factory, autoclose_dialog, core_config, alice, monkeypatch): password = "******" save_device_with_password(core_config.config_dir, alice, password) gui = await gui_factory() # Fix the return value of ensure_string_size, because it can depend of the size of the window monkeypatch.setattr("parsec.core.gui.main_window.ensure_string_size", lambda s, size, font: (s[:16] + "...")) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate( "TEXT_TAB_TITLE_LOG_IN_SCREEN") assert not gui.add_tab_button.isEnabled() first_created_tab = gui.test_get_tab() await gui.test_switch_to_logged_in(alice) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.add_tab_button.isEnabled() assert gui.test_get_tab() == first_created_tab await gui.test_logout() assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate( "TEXT_TAB_TITLE_LOG_IN_SCREEN") assert not gui.add_tab_button.isEnabled() assert gui.test_get_tab() != first_created_tab
async def test_login_back_to_account_list(aqtbot, gui_factory, autoclose_dialog, core_config, alice, bob): # Create an existing device before starting the gui password = "******" save_device_with_password(core_config.config_dir, alice, password) save_device_with_password(core_config.config_dir, bob, password) gui = await gui_factory() lw = gui.test_get_login_widget() accounts_w = lw.widget.layout().itemAt(0).widget() assert accounts_w async with aqtbot.wait_signal(accounts_w.account_clicked): await aqtbot.mouse_click( accounts_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton) def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) await aqtbot.wait_until(_password_widget_shown) password_w = lw.widget.layout().itemAt(0).widget() async with aqtbot.wait_signal(password_w.back_clicked): await aqtbot.mouse_click(password_w.button_back, QtCore.Qt.LeftButton) def _account_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginAccountsWidget)
async def test_login(aqtbot, gui_factory, autoclose_dialog, core_config, alice): # Create an existing device before starting the gui password = "******" save_device_with_password(core_config.config_dir, alice, password) gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() assert lw is not None # Available device is automatically selected for login assert ( lw.combo_username.currentText() == f"{alice.organization_id}: {alice.human_handle} @ {alice.device_label}" ) await aqtbot.key_clicks(lw.line_edit_password, password) async with aqtbot.wait_signals( [lw.login_with_password_clicked, tabw.logged_in]): await aqtbot.mouse_click(lw.button_login, QtCore.Qt.LeftButton) lw = gui.test_get_login_widget() assert lw is None cw = gui.test_get_central_widget() assert cw is not None
def _on_finished(self, new_device, password): save_device_with_password(config_dir=self.config.config_dir, device=new_device, password=password) show_info(self, _("TEXT_CLAIM_DEVICE_SUCCESSFUL")) self.status = (new_device, password) self.dialog.accept()
async def test_expired_notification_logging(aqtbot, running_backend, autoclose_dialog, expiredorgalice, gui_factory, core_config): # Log has alice on an expired organization save_device_with_password(core_config.config_dir, expiredorgalice, "P@ssw0rd") gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) await aqtbot.wait_until(_password_widget_shown) password_w = lw.widget.layout().itemAt(0).widget() await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals( [lw.login_with_password_clicked, tabw.logged_in]): await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) # Assert dialog def _expired_notified(): assert autoclose_dialog.dialogs == [("Error", "The organization has expired")] await aqtbot.wait_until(_expired_notified)
def test_keys_import(qtbot, core_config, alice, bob, monkeypatch): password = "******" save_device_with_password(core_config.config_dir, alice, password) tmp_path = core_config.config_dir.joinpath("tmp") tmp_path.mkdir() tmp_path.joinpath("devices").mkdir() fake_config = type("fake_config", (), {"config_dir": tmp_path})() w = KeysWidget(fake_config, parent=None) qtbot.addWidget(w) keys_layout = w.scroll_content.layout() assert keys_layout.count() == 0 key_glob = list(core_config.config_dir.joinpath("devices").glob("*.keys")) assert len(key_glob) == 1 monkeypatch.setattr( "parsec.core.gui.keys_widget.QFileDialog.getOpenFileName", lambda *x, **y: (key_glob[0], None), ) monkeypatch.setattr( "parsec.core.gui.keys_widget.ask_question", lambda *x, **y: translate("ACTION_IMPORT_YES") ) qtbot.mouseClick(w.button_import_key, QtCore.Qt.LeftButton) def key_imported(): assert keys_layout.count() == 1 qtbot.wait_until(key_imported)
async def test_link_file_unknown_org(core_config, gui_factory, autoclose_dialog, running_backend, alice): password = "******" save_device_with_password(core_config.config_dir, alice, password) # Cheating a bit but it does not matter, we just want a link that appears valid with # an unknown organization org_addr = BackendOrganizationAddr.build( running_backend.addr, "UnknownOrg", alice.organization_addr.root_verify_key) file_link = BackendOrganizationFileLinkAddr.build( org_addr, EntryID(), FsPath("/doesntmattereither")) gui = await gui_factory(core_config=core_config, start_arg=file_link.to_url()) lw = gui.test_get_login_widget() assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs[0][0] == "Error" assert autoclose_dialog.dialogs[0][1] == translate( "TEXT_FILE_LINK_NOT_IN_ORG_organization").format( organization="UnknownOrg") accounts_w = lw.widget.layout().itemAt(0).widget() assert accounts_w assert isinstance(accounts_w, LoginPasswordInputWidget)
def test_password_save_and_load(path_exists, config_dir, alice): config_dir = config_dir if path_exists else config_dir / "dummy" save_device_with_password(config_dir, alice, "S3Cr37") key_file = get_key_file(config_dir, alice) alice_reloaded = load_device_with_password(key_file, "S3Cr37") assert alice == alice_reloaded
def test_share_workspace(tmpdir, alice, bob): # As usual Windows path require a big hack... config_dir = tmpdir.strpath.replace("\\", "\\\\") # Mocking factory_mock = MagicMock() share_mock = MagicMock() @asynccontextmanager async def logged_core_factory(*args, **kwargs): yield factory_mock(*args, **kwargs) async def share(*args, **kwargs): return share_mock(*args, **kwargs) factory_mock.return_value.user_fs.workspace_share = share password = "******" save_device_with_password(Path(config_dir), bob, password) with patch("parsec.core.cli.share_workspace.logged_core_factory", logged_core_factory): runner = CliRunner() args = (f"core share_workspace --password {password} " f"--device={bob.slughash} --config-dir={config_dir} " f"ws1 {alice.user_id}") result = runner.invoke(cli, args) print(result.output) assert result.exit_code == 0 assert result.output == "" factory_mock.assert_called_once_with(ANY, bob) share_mock.assert_called_once_with("/ws1", alice.user_id)
async def _claim_invitation(config, addr, password): async with backend_invited_cmds_factory(addr=addr) as cmds: try: async with spinner("Retrieving invitation info"): initial_ctx = await claimer_retrieve_info(cmds) except BackendConnectionRefused: raise RuntimeError("Invitation not found") if initial_ctx.greeter_human_handle: display_greeter = click.style(initial_ctx.greeter_human_handle, fg="yellow") else: display_greeter = click.style(initial_ctx.greeter_user_id, fg="yellow") click.echo(f"Invitation greeter: {display_greeter}") while True: try: if isinstance(initial_ctx, DeviceClaimInitialCtx): new_device = await _do_claim_device(initial_ctx) else: assert isinstance(initial_ctx, UserClaimInitialCtx) new_device = await _do_claim_user(initial_ctx) if new_device: break except InviteError as exc: click.secho(str(exc), fg="red") click.secho("Restarting the invitation process", fg="red") device_display = click.style(new_device.slughash, fg="yellow") with operation(f"Saving device {device_display}"): save_device_with_password(config.config_dir, new_device, password)
async def _claim_device(token, addr, device_id, password, config): device = await actual_claim_device( organization_addr=addr, new_device_id=device_id, token=token, keepalive=config.backend_connection_keepalive, ) save_device_with_password(config.config_dir, device, password)
def _on_finalize_clicked(self): password = self.widget_password.password try: save_device_with_password(self.config.config_dir, self.new_device, password) self.succeeded.emit(self.new_device, password) except LocalDeviceAlreadyExistsError as exc: show_error(self, _("TEXT_CLAIM_USER_DEVICE_ALREADY_EXISTS"), exception=exc) self.failed.emit(None)
def keys_widget(qtbot, core_config, alice, bob): password = "******" save_device_with_password(core_config.config_dir, alice, password) save_device_with_password(core_config.config_dir, bob, password) w = KeysWidget(core_config, parent=None) qtbot.addWidget(w) assert w.scroll_content.layout().count() == 2 return w
async def claim_task(): nonlocal bob_device bob_device = await retry_claim(claim_user, alice_device.organization_addr, bob_device_id, token) save_device_with_password(config_dir, bob_device, password, force=force)
def test_available_devices_slughash_uniqueness(organization_factory, local_device_factory, config_dir): def _to_available(device): return AvailableDevice( key_file_path=get_default_key_file(config_dir, device), organization_id=device.organization_id, device_id=device.device_id, human_handle=device.human_handle, device_label=device.device_label, slug=device.slug, ) def _assert_different_as_available(d1, d2): available_device_d1 = _to_available(d1) available_device_d2 = _to_available(d2) assert available_device_d1.slughash != available_device_d2.slughash # Make sure slughash is consistent between LocalDevice and AvailableDevice assert available_device_d1.slughash == d1.slughash assert available_device_d2.slughash == d2.slughash o1 = organization_factory("org1") o2 = organization_factory("org2") # Different user id o1u1d1 = local_device_factory("u1@d1", o1) o1u2d1 = local_device_factory("u2@d1", o1) _assert_different_as_available(o1u1d1, o1u2d1) # Different device name o1u1d2 = local_device_factory("u1@d2", o1) _assert_different_as_available(o1u1d1, o1u1d2) # Different organization id o2u1d1 = local_device_factory("u1@d1", o2) _assert_different_as_available(o1u1d1, o2u1d1) # Same organization_id, but different root verify key ! dummy_key = SigningKey.generate().verify_key o1u1d1_bad_rvk = o1u1d1.evolve( organization_addr=o1u1d1.organization_addr.build( backend_addr=o1u1d1.organization_addr, organization_id=o1u1d1.organization_addr.organization_id, root_verify_key=dummy_key, )) _assert_different_as_available(o1u1d1, o1u1d1_bad_rvk) # Finally make sure slughash is stable through save/load save_device_with_password(config_dir, o1u1d1, "S3Cr37") key_file = get_key_file(config_dir, o1u1d1) o1u1d1_reloaded = load_device_with_password(key_file, "S3Cr37") available_device = _to_available(o1u1d1) available_device_reloaded = _to_available(o1u1d1_reloaded) assert available_device.slughash == available_device_reloaded.slughash
async def _bootstrap_organization(debug, device_id, organization_bootstrap_addr, config_dir, force, password): root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr( root_verify_key) device_display = click.style(device_id, fg="yellow") device = generate_new_device(device_id, organization_addr, profile=UserProfile.ADMIN) with operation(f"Creating locally {device_display}"): save_device_with_password(config_dir, device, password, force=force) now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=device.user_id, public_key=device.public_key, profile=device.profile, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent(author=None, timestamp=now, device_id=device_id, verify_key=device.verify_key) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(root_signing_key) device_certificate = device_certificate.dump_and_sign(root_signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( root_signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( root_signing_key) async with spinner(f"Sending {device_display} to server"): async with apiv1_backend_anonymous_cmds_factory( organization_bootstrap_addr) as cmds: await cmds.organization_bootstrap( organization_id=organization_bootstrap_addr.organization_id, bootstrap_token=organization_bootstrap_addr.token, root_verify_key=root_verify_key, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) organization_addr_display = click.style(organization_addr.to_url(), fg="yellow") click.echo(f"Organization url: {organization_addr_display}")
async def test_expired_notification_from_connection(aqtbot, running_backend, autoclose_dialog, expiredorgalice, gui_factory, core_config): save_device_with_password(core_config.config_dir, expiredorgalice, "P@ssw0rd") gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() # Force logging on an expired organization with freeze_time("1989-12-17"): def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) await aqtbot.wait_until(_password_widget_shown) password_w = lw.widget.layout().itemAt(0).widget() await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals( [lw.login_with_password_clicked, tabw.logged_in]): await aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) # Assert logged in def _notified(): assert autoclose_dialog.dialogs == [] central_widget = gui.test_get_central_widget() assert central_widget is not None await aqtbot.wait_until(_notified) # Trigger another handshake with pytest.raises(BackendConnectionRefused): async with backend_authenticated_cmds_factory( expiredorgalice.organization_addr, expiredorgalice.device_id, expiredorgalice.signing_key, ) as cmds: async with cmds.acquire_transport(): # This shall never happen, we shall have been rejected while acquiring the transport assert False # Assert dialog def _expired_notified(): assert autoclose_dialog.dialogs == [("Error", "The organization has expired")] await aqtbot.wait_until(_expired_notified)
def bob_available_device(bob, tmp_path): key_file_path = tmp_path / "bob_device.keys" save_device_with_password(key_file=key_file_path, device=bob, password="", force=True) return AvailableDevice( key_file_path=key_file_path, organization_id=bob.organization_id, device_id=bob.device_id, human_handle=bob.human_handle, device_label=bob.device_label, slug=bob.slug, type=DeviceFileType.PASSWORD, )
def test_same_device_id_different_orginazations(config_dir, alice, otheralice): devices = (alice, otheralice) for device in devices: save_device_with_password(config_dir, device, f"S3Cr37-{device.organization_id}") for device in devices: key_file = get_key_file(config_dir, device) device_reloaded = load_device_with_password( key_file, f"S3Cr37-{device.organization_id}") assert device == device_reloaded
async def logged_gui(aqtbot, gui_factory, autoclose_dialog, core_config, alice, running_backend, monkeypatch): save_device_with_password(core_config.config_dir, alice, "P@ssw0rd") monkeypatch.setattr( "parsec.core.gui.workspaces_widget.WorkspacesWidget.RESET_TIMER_THRESHOLD", 0) gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() await aqtbot.key_clicks(lw.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals( [lw.login_with_password_clicked, tabw.logged_in]): await aqtbot.mouse_click(lw.button_login, QtCore.Qt.LeftButton) central_widget = gui.test_get_central_widget() assert central_widget is not None wk_widget = gui.test_get_workspaces_widget() async with aqtbot.wait_signal(wk_widget.list_success): pass add_button = wk_widget.button_add_workspace assert add_button is not None monkeypatch.setattr("parsec.core.gui.workspaces_widget.get_text_input", lambda *args, **kwargs: ("Workspace")) async with aqtbot.wait_signals( [ wk_widget.create_success, wk_widget.list_success, wk_widget.mountpoint_started ], timeout=2000, ): await aqtbot.mouse_click(add_button, QtCore.Qt.LeftButton) def workspace_button_ready(): assert wk_widget.layout_workspaces.count() == 1 wk_button = wk_widget.layout_workspaces.itemAt(0).widget() assert not isinstance(wk_button, QtWidgets.QLabel) await aqtbot.wait_until(workspace_button_ready) wk_button = wk_widget.layout_workspaces.itemAt(0).widget() assert wk_button.name == "Workspace" async with aqtbot.wait_signal(wk_widget.load_workspace_clicked): await aqtbot.mouse_click(wk_button, QtCore.Qt.LeftButton) yield gui
async def logged_gui(aqtbot, gui_factory, core_config, alice, bob, fixtures_customization): # Logged as bob (i.e. standard profile) by default if fixtures_customization.get("logged_gui_as_admin", False): device = alice else: device = bob save_device_with_password(core_config.config_dir, device, "P@ssw0rd") gui = await gui_factory() await gui.test_switch_to_logged_in(device) return gui
async def _claim_device(config, organization_addr, new_device_id, token, password): async with spinner("Waiting for referee to reply"): device = await actual_claim_device( organization_addr=organization_addr, new_device_id=new_device_id, token=token, keepalive=config.backend_connection_keepalive, ) device_display = click.style(new_device_id, fg="yellow") with operation(f"Saving locally {device_display}"): save_device_with_password(config.config_dir, device, password)
def test_change_password(config_dir, alice): old_password = "******" new_password = "******" save_device_with_password(config_dir, alice, old_password) key_file = get_key_file(config_dir, alice) change_device_password(key_file, old_password, new_password) alice_reloaded = load_device_with_password(key_file, new_password) assert alice == alice_reloaded with pytest.raises(LocalDeviceCryptoError): load_device_with_password(key_file, old_password)
async def test_tab_login_logout_two_tabs_logged_in(aqtbot, running_backend, gui_factory, autoclose_dialog, core_config, alice, bob, monkeypatch): password = "******" save_device_with_password(core_config.config_dir, alice, password) gui = await gui_factory() # Fix the return value of ensure_string_size, because it can depend of the size of the window monkeypatch.setattr("parsec.core.gui.main_window.ensure_string_size", lambda s, size, font: (s[:16] + "...")) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate( "TEXT_TAB_TITLE_LOG_IN_SCREEN") await gui.test_switch_to_logged_in(alice) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." alice_logged_tab = gui.test_get_tab() await aqtbot.mouse_click(gui.add_tab_button, QtCore.Qt.LeftButton) assert gui.tab_center.count() == 2 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.tab_center.tabText(1) == translate( "TEXT_TAB_TITLE_LOG_IN_SCREEN") save_device_with_password(core_config.config_dir, bob, password) await gui.test_switch_to_logged_in(bob) assert gui.tab_center.count() == 2 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.tab_center.tabText(1) == "CoolOrg - Boby M..." bob_logged_tab = gui.test_get_tab() assert bob_logged_tab != alice_logged_tab gui.switch_to_tab(0) def _logged_tab_displayed(): assert alice_logged_tab == gui.test_get_tab() await aqtbot.wait_until(_logged_tab_displayed) await gui.test_logout() assert gui.tab_center.count() == 2 assert gui.tab_center.tabText(0) == "CoolOrg - Boby M..." assert gui.tab_center.tabText(1) == translate( "TEXT_TAB_TITLE_LOG_IN_SCREEN")
async def _do_create_org(config, human_handle, device_name, password, backend_addr): try: async with apiv1_backend_anonymous_cmds_factory( addr=backend_addr) as cmds: new_device = await bootstrap_organization( cmds=cmds, human_handle=human_handle, device_label=device_name) save_device_with_password(config_dir=config.config_dir, device=new_device, password=password) return new_device, password except InviteAlreadyUsedError as exc: raise JobResultError("invite-already-used", exc=exc) except BackendConnectionRefused as exc: raise JobResultError("connection-refused", exc=exc) except BackendNotAvailable as exc: raise JobResultError("connection-error", exc=exc)
async def _do_claim_user( config, password: str, password_check: str, token: str, user_id: str, device_name: str, organization_addr: BackendOrganizationClaimUserAddr, ): if password != password_check: raise JobResultError("password-mismatch") if len(password) < 8: raise JobResultError("password-size") try: device_id = DeviceID(f"{user_id}@{device_name}") except ValueError as exc: raise JobResultError("bad-device_name") from exc try: device = await core_claim_user( organization_addr=organization_addr.to_organization_addr(), new_device_id=device_id, token=token, keepalive=config.backend_connection_keepalive, ) except InviteClaimBackendOfflineError as exc: raise JobResultError("backend-offline", info=str(exc)) from exc except InviteClaimError as exc: if "Cannot retrieve invitation creator" in str(exc): raise JobResultError("not_found", info=str(exc)) from exc else: raise JobResultError("refused-by-backend", info=str(exc)) from exc try: save_device_with_password(config.config_dir, device, password) except LocalDeviceAlreadyExistsError as exc: raise JobResultError("user-exists") from exc return device, password
async def _bootstrap_organization(config, addr, password, force): label = await aprompt("User fullname") email = await aprompt("User email") human_handle = HumanHandle(email=email, label=label) device_label = await aprompt("Device label", default=platform.node()) async with apiv1_backend_anonymous_cmds_factory(addr=addr) as cmds: async with spinner("Bootstrapping organization in the backend"): new_device = await do_bootstrap_organization( cmds=cmds, human_handle=human_handle, device_label=device_label) device_display = click.style(new_device.slughash, fg="yellow") with operation(f"Saving device {device_display}"): save_device_with_password(config_dir=config.config_dir, device=new_device, password=password, force=force)
def test_password_save_already_existing(config_dir, alice, alice2, otheralice): save_device_with_password(config_dir, alice, "S3Cr37") # Different devices should not overwrite each other save_device_with_password(config_dir, otheralice, "S3Cr37") save_device_with_password(config_dir, alice2, "S3Cr37") # Overwritting self is allowed save_device_with_password(config_dir, alice, "S3Cr37") devices = list_available_devices(config_dir) assert len(devices) == 3
def test_list_devices(organization_factory, local_device_factory, config_dir): org1 = organization_factory("org1") org2 = organization_factory("org2") o1d11 = local_device_factory("d1@1", org1) o1d12 = local_device_factory("d1@2", org1) o1d21 = local_device_factory("d2@1", org1) o2d11 = local_device_factory("d1@1", org2, has_human_handle=False) o2d12 = local_device_factory("d1@2", org2, has_device_label=False) o2d21 = local_device_factory("d2@1", org2, has_human_handle=False, has_device_label=False) for device in [o1d11, o1d12, o1d21]: save_device_with_password(config_dir, device, "S3Cr37") for device in [o2d11, o2d12, o2d21]: save_device_with_password(config_dir, device, "secret") # Also add dummy stuff that should be ignored device_dir = config_dir / "devices" (device_dir / "bad1").touch() (device_dir / "373955f566#corp#bob@laptop").mkdir() dummy_slug = "a54ed6df3a#corp#alice@laptop" (device_dir / dummy_slug).mkdir() (device_dir / dummy_slug / f"{dummy_slug}.keys").write_bytes(b"dummy") devices = list_available_devices(config_dir) expected_devices = { AvailableDevice( key_file_path=get_key_file(config_dir, d), organization_id=d.organization_id, device_id=d.device_id, human_handle=d.human_handle, device_label=d.device_label, slug=d.slug, ) for d in [o1d11, o1d12, o1d21, o2d11, o2d12, o2d21] } assert set(devices) == expected_devices
async def logged_gui(aqtbot, gui_factory, autoclose_dialog, core_config, alice, bob): save_device_with_password(core_config.config_dir, alice, "P@ssw0rd") gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() assert lw is not None await aqtbot.key_clicks(lw.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): await aqtbot.mouse_click(lw.button_login, QtCore.Qt.LeftButton) central_widget = gui.test_get_central_widget() assert central_widget is not None save_device_with_password(core_config.config_dir, bob, "P@ssw0rd") yield gui