예제 #1
0
async def test_tab_login_logout(gui_factory, core_config, alice, monkeypatch):
    password = "******"
    save_device_with_password_in_config(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
예제 #2
0
 async def prepare_enrollment_request(self):
     try:
         self.context = await PkiEnrollmentSubmitterInitialCtx.new(self.addr
                                                                   )
         self.widget_user_info.setVisible(True)
         self.label_cert_error.setVisible(False)
         self.line_edit_user_name.setText(
             self.context.x509_certificate.subject_common_name)
         self.line_edit_user_email.setText(
             self.context.x509_certificate.subject_email_address)
         self.line_edit_device.setText(desktop.get_default_device())
         self.button_select_cert.setText(
             str(self.context.x509_certificate.certificate_id))
     except PkiEnrollmentCertificateNotFoundError:
         # User did not provide a certificate (cancelled the prompt). We do nothing.
         pass
     except PkiEnrollmentCertificatePinCodeUnavailableError:
         # User did not provide a pin code (cancelled the prompt). We do nothing.
         pass
     except Exception as exc:
         show_error(self,
                    translate("TEXT_ENROLLMENT_ERROR_LOADING_CERTIFICATE"),
                    exception=exc)
         self.widget_user_info.setVisible(False)
         self.label_cert_error.setText(
             translate("TEXT_ENROLLMENT_ERROR_LOADING_CERTIFICATE"))
         self.label_cert_error.setVisible(True)
         self.button_ask_to_join.setEnabled(False)
 async def _create_new_device(self, device_label, file_path, passphrase):
     try:
         recovery_device = await load_recovery_device(file_path, passphrase)
         new_device = await generate_new_device_from_recovery(
             recovery_device, device_label)
         return new_device
     except LocalDeviceError as exc:
         self.button_validate.setEnabled(True)
         if "Decryption failed" in str(exc):
             show_error(self,
                        translate("TEXT_IMPORT_KEY_WRONG_PASSPHRASE"),
                        exception=exc)
         else:
             show_error(self,
                        translate("IMPORT_KEY_LOCAL_DEVICE_ERROR"),
                        exception=exc)
         raise JobResultError("error") from exc
     except BackendNotAvailable as exc:
         show_error(self,
                    translate("IMPORT_KEY_BACKEND_OFFLINE"),
                    exception=exc)
         raise JobResultError("backend-error") from exc
     except BackendConnectionError as exc:
         show_error(self,
                    translate("IMPORT_KEY_BACKEND_ERROR"),
                    exception=exc)
         raise JobResultError("backend-error") from exc
     except Exception as exc:
         show_error(self, translate("IMPORT_KEY_ERROR"), exception=exc)
         raise JobResultError("error") from exc
예제 #4
0
async def test_tab_login_logout_two_tabs(aqtbot, gui_factory, core_config, alice, monkeypatch):
    password = "******"
    save_device_with_password_in_config(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")
    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..."
    logged_tab = gui.test_get_tab()

    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")

    gui.switch_to_tab(0)

    def _logged_tab_displayed():
        assert logged_tab == gui.test_get_tab()

    await aqtbot.wait_until(_logged_tab_displayed)
    await gui.test_logout()
    assert gui.tab_center.count() == 1
    assert gui.tab_center.tabText(0) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN")
    assert gui.test_get_tab() != first_created_tab
 async def _export_recovery_device(self, config_dir, device, export_path):
     try:
         recovery_device = await generate_recovery_device(device)
         file_name = get_recovery_device_file_name(recovery_device)
         file_path = export_path / file_name
         passphrase = await save_recovery_device(file_path, recovery_device)
         return recovery_device, file_path, passphrase
     except BackendNotAvailable as exc:
         show_error(self,
                    translate("EXPORT_KEY_BACKEND_OFFLINE"),
                    exception=exc)
         raise JobResultError("backend-error") from exc
     except BackendConnectionError as exc:
         show_error(self,
                    translate("EXPORT_KEY_BACKEND_ERROR"),
                    exception=exc)
         raise JobResultError("backend-error") from exc
     except LocalDeviceAlreadyExistsError as exc:
         show_error(self,
                    translate("TEXT_RECOVERY_DEVICE_FILE_ALREADY_EXISTS"),
                    exception=exc)
         raise JobResultError("already-exists") from exc
     except Exception as exc:
         show_error(self, translate("EXPORT_KEY_ERROR"), exception=exc)
         raise JobResultError("error") from exc
     self.button_validate.setEnabled(True)
 def __init__(self, parent=None):
     super().__init__(parent)
     self.setupUi(self)
     self.combo_auth_method.addItem(translate("TEXT_AUTH_METHOD_PASSWORD"),
                                    DeviceFileType.PASSWORD)
     if is_smartcard_extension_available():
         self.combo_auth_method.addItem(
             translate("TEXT_AUTH_METHOD_SMARTCARD"),
             DeviceFileType.SMARTCARD)
     self.combo_auth_method.setCurrentIndex(0)
     if self.combo_auth_method.count() == 1:
         self.combo_auth_method.setEnabled(False)
         self.combo_auth_method.setToolTip(
             translate("TEXT_ONLY_ONE_AUTH_METHOD_AVAILABLE"))
     self.auth_widgets = {
         DeviceFileType.PASSWORD: PasswordAuthenticationWidget(),
         DeviceFileType.SMARTCARD: SmartCardAuthenticationWidget(),
     }
     self.current_auth_method = DeviceFileType.PASSWORD
     self.auth_widgets[
         DeviceFileType.PASSWORD].authentication_state_changed.connect(
             self._on_password_state_changed)
     self.auth_widgets[
         DeviceFileType.SMARTCARD].authentication_state_changed.connect(
             self._on_smartcard_state_changed)
     self.main_layout.addWidget(self.auth_widgets[self.current_auth_method])
     self.combo_auth_method.currentIndexChanged.connect(
         self._on_auth_method_changed)
예제 #7
0
    def __init__(self, pending):
        super().__init__()
        self.setupUi(self)
        self.pending = pending
        accept_pix = Pixmap(":/icons/images/material/done.svg")
        accept_pix.replace_color(QColor(0x00, 0x00, 0x00),
                                 QColor(0xFF, 0xFF, 0xFF))
        reject_pix = Pixmap(":/icons/images/material/clear.svg")
        reject_pix.replace_color(QColor(0x00, 0x00, 0x00),
                                 QColor(0xFF, 0xFF, 0xFF))
        self.button_accept.setIcon(QIcon(accept_pix))
        self.button_reject.setIcon(QIcon(reject_pix))
        self.label_date.setText(format_datetime(pending.submitted_on))

        if isinstance(self.pending, PkiEnrollementAccepterInvalidSubmittedCtx):
            if self.pending.submitter_x509_certificate:
                self.widget_cert_infos.setVisible(True)
                self.widget_cert_error.setVisible(False)
                self.label_name.setText(
                    self.pending.submitter_x509_certificate.subject_common_name
                )
                self.label_email.setText(
                    self.pending.submitter_x509_certificate.
                    subject_email_address)
                self.label_issuer.setText(
                    self.pending.submitter_x509_certificate.issuer_common_name)
            else:
                self.widget_cert_infos.setVisible(False)
                self.widget_cert_error.setVisible(True)
                self.label_error.setText(
                    translate("TEXT_ENROLLMENT_ERROR_WITH_CERTIFICATE"))
            self.button_accept.setVisible(False)
            self.label_cert_validity.setStyleSheet(
                "#label_cert_validity { color: #F44336; }")
            self.label_cert_validity.setText(
                "✘ " + translate("TEXT_ENROLLMENT_CERTIFICATE_IS_INVALID"))
            self.label_cert_validity.setToolTip(
                textwrap.fill(str(self.pending.error), 80))
        else:
            assert isinstance(self.pending,
                              PkiEnrollementAccepterValidSubmittedCtx)
            self.widget_cert_infos.setVisible(True)
            self.widget_cert_error.setVisible(False)
            self.button_accept.setVisible(True)
            self.label_name.setText(
                self.pending.submitter_x509_certificate.subject_common_name)
            self.label_email.setText(
                self.pending.submitter_x509_certificate.subject_email_address)
            self.label_issuer.setText(
                self.pending.submitter_x509_certificate.issuer_common_name)
            self.label_cert_validity.setStyleSheet(
                "#label_cert_validity { color: #8BC34A; }")
            self.label_cert_validity.setText(
                "✔ " + translate("TEXT_ENROLLMENT_CERTIFICATE_IS_VALID"))

        self.button_accept.clicked.connect(
            lambda: self.accept_clicked.emit(self))
        self.button_reject.clicked.connect(
            lambda: self.reject_clicked.emit(self))
예제 #8
0
 def _overwrite_key(self, dest):
     if dest.exists():
         rep = ask_question(
             parent=self,
             title=translate("ASK_OVERWRITE_KEY"),
             message=translate("TEXT_OVERWRITE_KEY"),
             button_texts=(translate("ACTION_OVERWRITE_KEY_YES"), translate("ACTION_IMPORT_NO")),
         )
         return rep == translate("ACTION_OVERWRITE_KEY_YES")
     return True
예제 #9
0
    def _on_import_key(self):
        key_file, _ = QFileDialog.getOpenFileName(
            parent=self,
            caption=translate("ACTION_IMPORT_KEY"),
            filter=translate("IMPORT_KEY_FILTERS"),
            initialFilter=translate("IMPORT_KEY_INITIAL_FILTER"),
        )
        if not key_file:
            return
        new_device = load_device_file(Path(key_file))
        if new_device is None:
            show_error(self, translate("TEXT_INVALID_DEVICE_KEY"))
            return
        rep = ask_question(
            parent=self,
            title=translate("ASK_IMPORT_KEY"),
            message=translate("TEXT_IMPORT_KEY_CONFIRM_organization-user-device").format(
                organization=new_device.organization_id,
                user=new_device.short_user_display,
                device=new_device.device_label,
            ),
            button_texts=(translate("ACTION_IMPORT_YES"), translate("ACTION_IMPORT_NO")),
        )
        if rep == translate("ACTION_IMPORT_YES"):
            key_name = new_device.slughash + ".keys"
            dest = get_devices_dir(self.config.config_dir).joinpath(key_name)
            if self._overwrite_key(dest):

                shutil.copyfile(
                    new_device.key_file_path,
                    os.path.join(get_devices_dir(self.config.config_dir), key_name),
                )
                self.reload_devices()
                self.key_imported.emit()
예제 #10
0
 async def reject_recruit(self, enrollment_button):
     try:
         await enrollment_button.pending.reject()
     except Exception:
         SnackbarManager.warn(translate("TEXT_ENROLLMENT_REJECT_FAILURE"))
         enrollment_button.set_buttons_enabled(True)
     else:
         SnackbarManager.inform(translate("TEXT_ENROLLMENT_REJECT_SUCCESS"))
         self.main_layout.removeWidget(enrollment_button)
         enrollment_button.hide()
         enrollment_button.setParent(None)
 def _on_import_key_clicked(self):
     key_file, _ = QDialogInProcess.getOpenFileName(
         self,
         translate("ACTION_IMPORT_KEY"),
         str(Path.home()),
         filter=translate("RECOVERY_KEY_FILTERS"),
         initialFilter=translate("RECOVERY_KEY_INITIAL_FILTER"),
     )
     if key_file:
         self.label_key_file.setText(key_file)
     self._check_infos()
예제 #12
0
async def test_workspace_reencryption_do_one_batch_error(
    caplog,
    aqtbot,
    running_backend,
    logged_gui,
    autoclose_dialog,
    monkeypatch,
    reencryption_needed_workspace,
    error_type,
):

    expected_errors = {
        FSBackendOfflineError:
        translate("TEXT_WORKPACE_REENCRYPT_OFFLINE_ERROR"),
        FSError:
        translate("TEXT_WORKPACE_REENCRYPT_FS_ERROR"),
        FSWorkspaceNoAccess:
        translate("TEXT_WORKPACE_REENCRYPT_ACCESS_ERROR"),
        FSWorkspaceNotFoundError:
        translate("TEXT_WORKPACE_REENCRYPT_NOT_FOUND_ERROR"),
        Exception:
        translate("TEXT_WORKSPACE_REENCRYPT_UNKOWN_ERROR"),
    }

    w_w = await logged_gui.test_switch_to_workspaces_widget()
    await display_reencryption_button(aqtbot, monkeypatch, w_w)

    wk_button = w_w.layout_workspaces.itemAt(0).widget()

    async def mocked_start_reencryption(self, workspace_id):
        class Job:
            async def do_one_batch(self, *args, **kwargs):
                raise error_type("")

        return Job()

    w_w.core.user_fs.workspace_start_reencryption = mocked_start_reencryption.__get__(
        w_w.core.user_fs)
    await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton)

    def _assert_error():
        assert len(autoclose_dialog.dialogs) == 1
        assert autoclose_dialog.dialogs == [("Error",
                                             expected_errors[error_type])]
        assert wk_button.button_reencrypt.isVisible()

    await aqtbot.wait_until(_assert_error)
    # Unexpected error is logged
    if error_type is Exception:
        caplog.assert_occured(
            "[exception] Uncatched error                [parsec.core.gui.trio_thread]"
        )
예제 #13
0
        async def cancelled_step_3_exchange_greeter_sas(self):
            expected_message = translate("TEXT_GREET_USER_WAIT_PEER_TRUST_ERROR")
            await self._cancel_invitation()

            await aqtbot.wait_until(partial(self._greet_restart, expected_message))

            return None
예제 #14
0
        async def cancelled_step_2_start_claimer(self):
            expected_message = translate("TEXT_GREET_USER_WAIT_PEER_ERROR")
            await self._cancel_invitation()

            await aqtbot.wait_until(partial(self._greet_restart, expected_message))

            return None
예제 #15
0
        async def reset_step_5_provide_claim_info(self):
            expected_message = translate("TEXT_GREET_USER_PEER_RESET")
            async with self._reset_claimer():
                await aqtbot.wait_until(partial(self._greet_restart, expected_message))

            await self.bootstrap_after_restart()
            return None
예제 #16
0
        async def cancelled_step_4_exchange_claimer_sas(self):
            expected_message = translate("TEXT_CLAIM_DEVICE_WAIT_PEER_TRUST_ERROR")
            await self._cancel_invitation()

            await aqtbot.wait_until(partial(self._claim_restart, expected_message))

            return None
예제 #17
0
        async def reset_step_5_provide_claim_info(self):
            expected_message = translate("TEXT_CLAIM_DEVICE_PEER_RESET")
            cdpi_w = self.claim_device_provide_info_widget
            device_label = self.requested_device_label

            async with self._reset_greeter():
                cdpi_w.line_edit_device.clear()
                await aqtbot.key_clicks(cdpi_w.line_edit_device,
                                        device_label.str)
                await aqtbot.key_clicks(
                    cdpi_w.widget_auth.main_layout.itemAt(
                        0).widget().line_edit_password,
                    self.password,
                )
                await aqtbot.key_clicks(
                    cdpi_w.widget_auth.main_layout.itemAt(
                        0).widget().line_edit_password_check,
                    self.password,
                )
                aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton)
                await aqtbot.wait_until(
                    partial(self._claim_restart, expected_message))

            await self.bootstrap_after_restart()
            return None
예제 #18
0
        async def offline_step_6_validate_claim_info(self):
            expected_message = translate("TEXT_CLAIM_DEVICE_CLAIM_ERROR")
            with running_backend.offline():
                await aqtbot.wait_until(
                    partial(self._claim_aborted, expected_message))

            return None
async def test_unshare_workspace_while_connected(aqtbot, running_backend,
                                                 logged_gui, autoclose_dialog,
                                                 qt_thread_gateway,
                                                 alice_user_fs, bob):
    w_w = await logged_gui.test_switch_to_workspaces_widget()
    wid = await alice_user_fs.workspace_create("Workspace")

    await alice_user_fs.workspace_share(wid, bob.user_id,
                                        WorkspaceRole.MANAGER)

    def _one_workspace_listed():
        assert w_w.layout_workspaces.count() == 1
        wk_button = w_w.layout_workspaces.itemAt(0).widget()
        assert isinstance(wk_button, WorkspaceButton)
        wk_button.name == "Workspace"

    await aqtbot.wait_until(_one_workspace_listed, timeout=2000)

    await alice_user_fs.workspace_share(wid, bob.user_id, None)

    def _no_workspace_listed():
        assert w_w.layout_workspaces.count() == 1
        label = w_w.layout_workspaces.itemAt(0).widget()
        assert isinstance(label, QtWidgets.QLabel)

    await aqtbot.wait_until(_no_workspace_listed, timeout=2000)

    assert autoclose_dialog.dialogs[0] == (
        "Error",
        translate("TEXT_FILE_SHARING_REVOKED_workspace").format(
            workspace="Workspace"),
    )
예제 #20
0
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)
예제 #21
0
 def _assert_dialogs():
     assert len(autoclose_dialog.dialogs) == 1
     assert autoclose_dialog.dialogs == [(
         "",
         translate("TEXT_FILE_LINK_PLEASE_LOG_IN_organization").format(
             organization=bob.organization_id),
     )]
예제 #22
0
        async def reset_step_6_validate_claim_info(self):
            expected_message = translate("TEXT_CLAIM_DEVICE_PEER_RESET")
            async with self._reset_greeter():
                await aqtbot.wait_until(partial(self._claim_restart, expected_message))

            await self.bootstrap_after_restart()
            return None
예제 #23
0
        async def cancelled_step_6_validate_claim_info(self):
            expected_message = translate("TEXT_INVITATION_ALREADY_USED")
            await self._cancel_invitation()

            await aqtbot.wait_until(partial(self._claim_restart, expected_message))

            return None
예제 #24
0
 def _assert_error():
     assert len(autoclose_dialog.dialogs) == 2
     assert (
         "Error",
         translate("TEXT_WORKPACE_REENCRYPT_ACCESS_ERROR"),
     ) in autoclose_dialog.dialogs
     assert wk_button.button_reencrypt.isVisible()
예제 #25
0
        async def cancelled_step_5_provide_claim_info(self):
            expected_message = translate("TEXT_GREET_USER_GET_REQUESTS_ERROR")
            await self._cancel_invitation()

            await aqtbot.wait_until(partial(self._greet_restart, expected_message))

            return None
예제 #26
0
        async def offline_step_2_start_greeter(self):
            expected_message = translate("TEXT_CLAIM_DEVICE_WAIT_PEER_ERROR")
            with running_backend.offline():
                await aqtbot.wait_until(
                    partial(self._claim_aborted, expected_message))

            return None
 def _error_shown():
     assert len(autoclose_dialog.dialogs) == 1
     assert autoclose_dialog.dialogs[0] == (
         "Error",
         translate("TEXT_WORKSPACE_SHARING_SHARE_ERROR_workspace-user").
         format(workspace="Workspace", user="******"),
     )
예제 #28
0
 def _on_export_key(self, device):
     default_key_name = f"parsec-{device.organization_id}-{device.human_handle.label}-{device.device_label}.keys"
     key_path, _ = QFileDialog.getSaveFileName(
         self,
         translate("TEXT_EXPORT_KEY"),
         str(Path.home().joinpath(default_key_name)),
         filter=translate("IMPORT_KEY_FILTERS"),
         initialFilter=translate("IMPORT_KEY_INITIAL_FILTER"),
     )
     if not key_path:
         return
     keys_dest = Path(key_path)
     try:
         shutil.copyfile(device.key_file_path, keys_dest)
     except IOError as err:
         show_error(self, translate("EXPORT_KEY_ERROR"), err)
예제 #29
0
async def test_link_file_unknown_org(
    aqtbot, core_config, gui_factory, autoclose_dialog, running_backend, alice
):
    password = "******"
    save_device_with_password_in_config(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, OrganizationID("UnknownOrg"), alice.organization_addr.root_verify_key
    )

    file_link = BackendOrganizationFileLinkAddr.build(
        organization_addr=org_addr, workspace_id=EntryID.new(), encrypted_path=b"<whatever>"
    )

    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")

    def _devices_listed():
        assert lw.widget.layout().count() > 0

    await aqtbot.wait_until(_devices_listed)

    accounts_w = lw.widget.layout().itemAt(0).widget()
    assert accounts_w

    assert isinstance(accounts_w, LoginPasswordInputWidget)
예제 #30
0
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)