From 25b4cc0b8bc2221bd7f7e474522d3ebccd6c56b9 Mon Sep 17 00:00:00 2001 From: jetchirag Date: Fri, 28 Jul 2023 13:09:10 +0530 Subject: [PATCH] Introduced password input widget (#1662) Move existing code for password input widgets into common classes to increase maintainability and reusability alongside reducing redundancy. This implements a `PasswordLineEdit` that can show a red border when an invalid password is entered. It also features a button for showing/hiding the password entered. When combining two of these entries for setting a new password `PasswordInput` can be used from now on. It combines a form for entering and confirming a password with a label to show a message when there is an issue with the password. It also checks the entered password against some rules regarding its length. This PR replaces existing widgets for entering passwords with these two new widgets. * src/vorta/views/partials/password_input.py : Implement common input widgets/classes * src/vorta/views/repo_add_dialog.py : Use new widgets. * src/vorta/assets/UI/repoadd.ui : ^^^ --- src/vorta/assets/UI/repoadd.ui | 68 +----- src/vorta/utils.py | 16 -- src/vorta/views/partials/password_input.py | 172 +++++++++++++++ src/vorta/views/repo_add_dialog.py | 242 ++++++++++----------- tests/test_password_input.py | 165 ++++++++++++++ tests/test_repo.py | 43 ++-- 6 files changed, 476 insertions(+), 230 deletions(-) create mode 100644 src/vorta/views/partials/password_input.py create mode 100644 tests/test_password_input.py diff --git a/src/vorta/assets/UI/repoadd.ui b/src/vorta/assets/UI/repoadd.ui index b0f861415..730f1df08 100644 --- a/src/vorta/assets/UI/repoadd.ui +++ b/src/vorta/assets/UI/repoadd.ui @@ -2,14 +2,6 @@ AddRepository - - - 0 - 0 - 583 - 338 - - true @@ -82,7 +74,7 @@ 5 - 5 + 20 @@ -169,47 +161,6 @@ - - - - Borg passphrase: - - - - - - - true - - - QLineEdit::Password - - - - - - - Confirm passphrase: - - - - - - - true - - - QLineEdit::Password - - - - - - - TextLabel - - - @@ -254,23 +205,6 @@ - - - - Encryption: - - - - - - - - 0 - 0 - - - - diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 89510e803..1793a5e88 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -18,7 +18,6 @@ from PyQt6.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon from vorta.borg._compatibility import BorgCompatibility -from vorta.i18n import trans_late from vorta.log import logger from vorta.network_status.abc import NetworkStatusMonitor @@ -507,21 +506,6 @@ def is_system_tray_available(): return is_available -def validate_passwords(first_pass, second_pass): - '''Validates the password for borg, do not use on single fields''' - pass_equal = first_pass == second_pass - pass_long = len(first_pass) > 8 - - if not pass_long and not pass_equal: - return trans_late('utils', "Passwords must be identical and greater than 8 characters long.") - if not pass_equal: - return trans_late('utils', "Passwords must be identical.") - if not pass_long: - return trans_late('utils', "Passwords must be greater than 8 characters long.") - - return "" - - def search(key, iterable: Iterable, func: Callable = None) -> Tuple[int, Any]: """ Search for a key in an iterable. diff --git a/src/vorta/views/partials/password_input.py b/src/vorta/views/partials/password_input.py new file mode 100644 index 000000000..bc8b0dd4c --- /dev/null +++ b/src/vorta/views/partials/password_input.py @@ -0,0 +1,172 @@ +from typing import Optional + +from PyQt6.QtCore import QObject +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QFormLayout, QLabel, QLineEdit, QWidget + +from vorta.i18n import translate +from vorta.views.utils import get_colored_icon + + +class PasswordLineEdit(QLineEdit): + def __init__(self, *, parent: Optional[QWidget] = None, show_visibility_button: bool = True) -> None: + super().__init__(parent) + + self._show_visibility_button = show_visibility_button + self._error_state = False + self._visible = False + + self.setEchoMode(QLineEdit.EchoMode.Password) + + if self._show_visibility_button: + self.showHideAction = QAction(self.tr("Show password"), self) + self.showHideAction.setCheckable(True) + self.showHideAction.toggled.connect(self.toggle_visibility) + self.showHideAction.setIcon(get_colored_icon("eye")) + self.addAction(self.showHideAction, QLineEdit.ActionPosition.TrailingPosition) + + def get_password(self) -> str: + """Return password text""" + return self.text() + + @property + def visible(self) -> bool: + """Return password visibility""" + return self._visible + + @visible.setter + def visible(self, value: bool) -> None: + """Set password visibility""" + if not isinstance(value, bool): + raise TypeError("visible must be a boolean value") + self._visible = value + self.setEchoMode(QLineEdit.EchoMode.Normal if self._visible else QLineEdit.EchoMode.Password) + + if self._show_visibility_button: + if self._visible: + self.showHideAction.setIcon(get_colored_icon("eye-slash")) + self.showHideAction.setText(self.tr("Hide password")) + + else: + self.showHideAction.setIcon(get_colored_icon("eye")) + self.showHideAction.setText(self.tr("Show password")) + + def toggle_visibility(self) -> None: + """Toggle password visibility""" + self.visible = not self._visible + + @property + def error_state(self) -> bool: + """Return error state""" + return self._error_state + + @error_state.setter + def error_state(self, error: bool) -> None: + """Set error state and update style""" + self._error_state = error + if error: + self.setStyleSheet("QLineEdit { border: 2px solid red; }") + else: + self.setStyleSheet('') + + +class PasswordInput(QObject): + def __init__(self, *, parent=None, minimum_length: int = 9, show_error: bool = True, label: list = None) -> None: + super().__init__(parent) + self._minimum_length = minimum_length + self._show_error = show_error + + if label: + self._label_password = QLabel(label[0]) + self._label_confirm = QLabel(label[1]) + else: + self._label_password = QLabel(self.tr("Enter passphrase:")) + self._label_confirm = QLabel(self.tr("Confirm passphrase:")) + + # Create password line edits + self.passwordLineEdit = PasswordLineEdit() + self.confirmLineEdit = PasswordLineEdit() + self.validation_label = QLabel("") + + self.passwordLineEdit.editingFinished.connect(self.on_editing_finished) + self.confirmLineEdit.textChanged.connect(self.validate) + + def on_editing_finished(self) -> None: + self.passwordLineEdit.editingFinished.disconnect(self.on_editing_finished) + self.passwordLineEdit.textChanged.connect(self.validate) + self.validate() + + def set_labels(self, label_1: str, label_2: str) -> None: + self._label_password.setText(label_1) + self._label_confirm.setText(label_2) + + def set_error_label(self, text: str) -> None: + self.validation_label.setText(text) + + def set_validation_enabled(self, enable: bool) -> None: + self._show_error = enable + self.passwordLineEdit.error_state = False + self.confirmLineEdit.error_state = False + if not enable: + self.set_error_label("") + + def clear(self) -> None: + self.passwordLineEdit.clear() + self.confirmLineEdit.clear() + self.passwordLineEdit.error_state = False + self.confirmLineEdit.error_state = False + self.set_error_label("") + + def get_password(self) -> str: + return self.passwordLineEdit.text() + + def validate(self) -> bool: + if not self._show_error: + return True + + first_pass = self.passwordLineEdit.text() + second_pass = self.confirmLineEdit.text() + + pass_equal = first_pass == second_pass + pass_long = len(first_pass) >= self._minimum_length + + self.passwordLineEdit.error_state = False + self.confirmLineEdit.error_state = False + self.set_error_label("") + + if not pass_long and not pass_equal: + self.passwordLineEdit.error_state = True + self.confirmLineEdit.error_state = True + self.set_error_label( + translate('PasswordInput', "Passwords must be identical and atleast {0} characters long.").format( + self._minimum_length + ) + ) + elif not pass_equal: + self.confirmLineEdit.error_state = True + self.set_error_label(translate('PasswordInput', "Passwords must be identical.")) + elif not pass_long: + self.passwordLineEdit.error_state = True + self.set_error_label( + translate('PasswordInput', "Passwords must be atleast {0} characters long.").format( + self._minimum_length + ) + ) + + return not bool(self.validation_label.text()) + + def add_form_to_layout(self, form_layout: QFormLayout) -> None: + """Adds form to layout""" + form_layout.addRow(self._label_password, self.passwordLineEdit) + form_layout.addRow(self._label_confirm, self.confirmLineEdit) + form_layout.addRow(self.validation_label) + + def create_form_widget(self, parent: Optional[QWidget] = None) -> QWidget: + """ "Creates and Returns a new QWidget with form layout""" + widget = QWidget(parent=parent) + form_layout = QFormLayout(widget) + form_layout.setContentsMargins(0, 0, 0, 0) + form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + self.add_form_to_layout(form_layout) + widget.setLayout(form_layout) + return widget diff --git a/src/vorta/views/repo_add_dialog.py b/src/vorta/views/repo_add_dialog.py index cdb4c3aa2..38cc65e25 100644 --- a/src/vorta/views/repo_add_dialog.py +++ b/src/vorta/views/repo_add_dialog.py @@ -1,28 +1,28 @@ import re from PyQt6 import QtCore, uic -from PyQt6.QtGui import QAction -from PyQt6.QtWidgets import QApplication, QDialogButtonBox, QLineEdit +from PyQt6.QtWidgets import ( + QApplication, + QComboBox, + QDialogButtonBox, + QFormLayout, + QLabel, + QSizePolicy, +) from vorta.borg.info_repo import BorgInfoRepoJob from vorta.borg.init import BorgInitJob -from vorta.i18n import translate from vorta.keyring.abc import VortaKeyring from vorta.store.models import RepoModel -from vorta.utils import ( - borg_compat, - choose_file_dialog, - get_asset, - get_private_keys, - validate_passwords, -) +from vorta.utils import borg_compat, choose_file_dialog, get_asset, get_private_keys +from vorta.views.partials.password_input import PasswordInput, PasswordLineEdit from vorta.views.utils import get_colored_icon uifile = get_asset('UI/repoadd.ui') AddRepoUI, AddRepoBase = uic.loadUiType(uifile) -class AddRepoWindow(AddRepoBase, AddRepoUI): +class RepoWindow(AddRepoBase, AddRepoUI): added_repo = QtCore.pyqtSignal(dict) def __init__(self, parent=None): @@ -32,7 +32,8 @@ def __init__(self, parent=None): self.result = None self.is_remote_repo = True - # dialogButtonBox + self.setMinimumWidth(583) + self.saveButton = self.buttonBox.button(QDialogButtonBox.StandardButton.Ok) self.saveButton.setText(self.tr("Add")) @@ -41,23 +42,11 @@ def __init__(self, parent=None): self.chooseLocalFolderButton.clicked.connect(self.choose_local_backup_folder) self.useRemoteRepoButton.clicked.connect(self.use_remote_repo_action) self.repoURL.textChanged.connect(self.set_password) - self.passwordLineEdit.textChanged.connect(self.password_listener) - self.confirmLineEdit.textChanged.connect(self.password_listener) - self.encryptionComboBox.activated.connect(self.display_backend_warning) - - # Add clickable icon to toggle password visibility to end of box - self.showHideAction = QAction(self.tr("Show my passwords"), self) - self.showHideAction.setCheckable(True) - self.showHideAction.toggled.connect(self.set_visibility) - - self.passwordLineEdit.addAction(self.showHideAction, QLineEdit.ActionPosition.TrailingPosition) self.tabWidget.setCurrentIndex(0) - self.init_encryption() self.init_ssh_key() self.set_icons() - self.display_backend_warning() def retranslateUi(self, dialog): """Retranslate strings in ui.""" @@ -70,25 +59,6 @@ def retranslateUi(self, dialog): def set_icons(self): self.chooseLocalFolderButton.setIcon(get_colored_icon('folder-open')) self.useRemoteRepoButton.setIcon(get_colored_icon('globe')) - self.showHideAction.setIcon(get_colored_icon("eye")) - - @property - def values(self): - out = dict( - ssh_key=self.sshComboBox.currentData(), - repo_url=self.repoURL.text(), - repo_name=self.repoName.text(), - password=self.passwordLineEdit.text(), - extra_borg_arguments=self.extraBorgArgumentsLineEdit.text(), - ) - if self.__class__ == AddRepoWindow: - out['encryption'] = self.encryptionComboBox.currentData() - return out - - def display_backend_warning(self): - '''Display password backend message based off current keyring''' - if self.encryptionComboBox.currentData() != 'none': - self.passwordLabel.setText(VortaKeyring.get_keyring().get_backend_warning()) def choose_local_backup_folder(self): def receive(): @@ -104,27 +74,6 @@ def receive(): dialog = choose_file_dialog(self, self.tr("Choose Location of Borg Repository")) dialog.open(receive) - def set_password(self, URL): - '''Autofill password from keyring only if current entry is empty''' - password = VortaKeyring.get_keyring().get_password('vorta-repo', URL) - if password and self.passwordLineEdit.text() == "": - self.passwordLabel.setText(self.tr("Autofilled password from password manager.")) - self.passwordLineEdit.setText(password) - if self.__class__ == AddRepoWindow: - self.confirmLineEdit.setText(password) - - def set_visibility(self, visible): - visibility = QLineEdit.EchoMode.Normal if visible else QLineEdit.EchoMode.Password - self.passwordLineEdit.setEchoMode(visibility) - self.confirmLineEdit.setEchoMode(visibility) - - if visible: - self.showHideAction.setIcon(get_colored_icon("eye-slash")) - self.showHideAction.setText(self.tr("Hide my passwords")) - else: - self.showHideAction.setIcon(get_colored_icon("eye")) - self.showHideAction.setText(self.tr("Show my passwords")) - def use_remote_repo_action(self): self.repoURL.setText('') self.repoURL.setEnabled(True) @@ -134,19 +83,6 @@ def use_remote_repo_action(self): self.repoLabel.setText(self.tr('Repository URL:')) self.is_remote_repo = True - # No need to add this function to JobsManager because repo is set for the first time - def run(self): - if self.validate() and self.password_listener(): - params = BorgInitJob.prepare(self.values) - if params['ok']: - self.saveButton.setEnabled(False) - job = BorgInitJob(params['cmd'], params) - job.updated.connect(self._set_status) - job.result.connect(self.run_result) - QApplication.instance().jobs_manager.add_job(job) - else: - self._set_status(params['message']) - def _set_status(self, text): self.errorText.setText(text) self.errorText.repaint() @@ -159,6 +95,73 @@ def run_result(self, result): else: self._set_status(self.tr('Unable to add your repository.')) + def init_ssh_key(self): + keys = get_private_keys() + for key in keys: + self.sshComboBox.addItem(f'{key}', key) + + def validate(self): + """Pre-flight check for valid input and borg binary.""" + if self.is_remote_repo and not re.match(r'.+:.+', self.values['repo_url']): + self._set_status(self.tr('Please enter a valid repo URL or select a local path.')) + return False + + if len(self.values['repo_name']) > 64: + self._set_status(self.tr('Repository name must be less than 64 characters.')) + return False + + if RepoModel.get_or_none(RepoModel.url == self.values['repo_url']) is not None: + self._set_status(self.tr('This repo has already been added.')) + return False + + return True + + @property + def values(self): + out = dict( + ssh_key=self.sshComboBox.currentData(), + repo_url=self.repoURL.text(), + repo_name=self.repoName.text(), + password=self.passwordInput.get_password(), + extra_borg_arguments=self.extraBorgArgumentsLineEdit.text(), + ) + return out + + +class AddRepoWindow(RepoWindow): + def __init__(self, parent=None): + super().__init__(parent) + + self.passwordInput = PasswordInput() + self.passwordInput.add_form_to_layout(self.repoDataFormLayout) + + self.encryptionLabel = QLabel(self.tr('Encryption:')) + self.encryptionComboBox = QComboBox() + self.encryptionComboBox.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + self.advancedFormLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.encryptionLabel) + self.advancedFormLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.encryptionComboBox) + + self.encryptionComboBox.activated.connect(self.display_backend_warning) + self.encryptionComboBox.currentIndexChanged.connect(self.encryption_listener) + + self.display_backend_warning() + self.init_encryption() + + def set_password(self, URL): + '''Autofill password from keyring only if current entry is empty''' + password = VortaKeyring.get_keyring().get_password('vorta-repo', URL) + if password and self.passwordInput.get_password() == "": + self.passwordInput.set_error_label(self.tr("Autofilled password from password manager.")) + self.passwordInput.passwordLineEdit.setText(password) + self.passwordInput.confirmLineEdit.setText(password) + + @property + def values(self): + out = super().values + out['encryption'] = self.encryptionComboBox.currentData() + return out + def init_encryption(self): if borg_compat.check('V2'): encryption_algos = [ @@ -191,64 +194,49 @@ def init_encryption(self): self.encryptionComboBox.model().item(2).setEnabled(False) self.encryptionComboBox.setCurrentIndex(1) - def init_ssh_key(self): - keys = get_private_keys() - for key in keys: - self.sshComboBox.addItem(f'{key}', key) - - def validate(self): - """Pre-flight check for valid input and borg binary.""" - if self.is_remote_repo and not re.match(r'.+:.+', self.values['repo_url']): - self._set_status(self.tr('Please enter a valid repo URL or select a local path.')) - return False - - if len(self.values['repo_name']) > 64: - self._set_status(self.tr('Repository name must be less than 64 characters.')) - return False - - if RepoModel.get_or_none(RepoModel.url == self.values['repo_url']) is not None: - self._set_status(self.tr('This repo has already been added.')) - return False - - return True - - def password_listener(self): + def encryption_listener(self): '''Validates passwords only if its going to be used''' if self.values['encryption'] == 'none': - self.passwordLabel.setText("") - return True + self.passwordInput.set_validation_enabled(False) else: - firstPass = self.passwordLineEdit.text() - secondPass = self.confirmLineEdit.text() - msg = validate_passwords(firstPass, secondPass) - self.passwordLabel.setText(translate('utils', msg)) - return not bool(msg) + self.passwordInput.set_validation_enabled(True) + def display_backend_warning(self): + '''Display password backend message based off current keyring''' + if self.encryptionComboBox.currentData() != 'none': + self.passwordInput.set_error_label(VortaKeyring.get_keyring().get_backend_warning()) + + def validate(self): + return super().validate() and self.passwordInput.validate() -class ExistingRepoWindow(AddRepoWindow): + def run(self): + if self.validate(): + params = BorgInitJob.prepare(self.values) + if params['ok']: + self.saveButton.setEnabled(False) + job = BorgInitJob(params['cmd'], params) + job.updated.connect(self._set_status) + job.result.connect(self.run_result) + QApplication.instance().jobs_manager.add_job(job) + else: + self._set_status(params['message']) + + +class ExistingRepoWindow(RepoWindow): def __init__(self): super().__init__() - self.encryptionComboBox.hide() - self.encryptionLabel.hide() self.title.setText(self.tr('Connect to existing Repository')) - self.showHideAction.setText(self.tr("Show my password")) - self.passwordLineEdit.textChanged.disconnect() - self.confirmLineEdit.textChanged.disconnect() - self.confirmLineEdit.hide() - self.confirmLabel.hide() - del self.confirmLineEdit - del self.confirmLabel - - def set_visibility(self, visible): - visibility = QLineEdit.EchoMode.Normal if visible else QLineEdit.EchoMode.Password - self.passwordLineEdit.setEchoMode(visibility) - - if visible: - self.showHideAction.setIcon(get_colored_icon("eye-slash")) - self.showHideAction.setText(self.tr("Hide my password")) - else: - self.showHideAction.setIcon(get_colored_icon("eye")) - self.showHideAction.setText(self.tr("Show my password")) + + self.passwordLabel = QLabel(self.tr('Password:')) + self.passwordInput = PasswordLineEdit() + self.repoDataFormLayout.addRow(self.passwordLabel, self.passwordInput) + + def set_password(self, URL): + '''Autofill password from keyring only if current entry is empty''' + password = VortaKeyring.get_keyring().get_password('vorta-repo', URL) + if password and self.passwordInput.get_password() == "": + self._set_status(self.tr("Autofilled password from password manager.")) + self.passwordInput.setText(password) def run(self): if self.validate(): diff --git a/tests/test_password_input.py b/tests/test_password_input.py new file mode 100644 index 000000000..429b99e41 --- /dev/null +++ b/tests/test_password_input.py @@ -0,0 +1,165 @@ +import pytest +from PyQt6.QtWidgets import QFormLayout, QWidget +from vorta.views.partials.password_input import PasswordInput, PasswordLineEdit + + +def test_create_password_line_edit(qtbot): + password_line_edit = PasswordLineEdit() + qtbot.addWidget(password_line_edit) + assert password_line_edit is not None + + +def test_password_line_get_password(qtbot): + password_line_edit = PasswordLineEdit() + qtbot.addWidget(password_line_edit) + + assert password_line_edit.get_password() == "" + + qtbot.keyClicks(password_line_edit, "test") + assert password_line_edit.get_password() == "test" + + +def test_password_line_visible(qtbot): + password_line_edit = PasswordLineEdit() + qtbot.addWidget(password_line_edit) + assert not password_line_edit.visible + + password_line_edit.toggle_visibility() + assert password_line_edit.visible + + with pytest.raises(TypeError): + password_line_edit.visible = "OK" + + +def test_password_line_error_state(qtbot): + password_line_edit = PasswordLineEdit() + qtbot.addWidget(password_line_edit) + assert not password_line_edit.error_state + assert password_line_edit.styleSheet() == "" + + password_line_edit.error_state = True + assert password_line_edit.error_state + assert password_line_edit.styleSheet() == "QLineEdit { border: 2px solid red; }" + + +def test_password_line_visibility_button(qtbot): + password_line_edit = PasswordLineEdit(show_visibility_button=False) + qtbot.addWidget(password_line_edit) + assert not password_line_edit._show_visibility_button + + password_line_edit = PasswordLineEdit() + qtbot.addWidget(password_line_edit) + assert password_line_edit._show_visibility_button + + # test visibility button + password_line_edit.showHideAction.trigger() + assert password_line_edit.visible + password_line_edit.showHideAction.trigger() + assert not password_line_edit.visible + + +# PasswordInput +def test_create_password_input(qapp, qtbot): + password_input = PasswordInput() + qtbot.addWidget(password_input.create_form_widget(parent=qapp.main_window)) + assert password_input is not None + + assert not password_input.passwordLineEdit.error_state + assert not password_input.confirmLineEdit.error_state + + +def test_password_input_get_password(qapp, qtbot): + password_input = PasswordInput() + qtbot.addWidget(password_input.create_form_widget(parent=qapp.main_window)) + + assert password_input.get_password() == "" + + password_input.passwordLineEdit.setText("test") + assert password_input.get_password() == "test" + + +def test_password_input_validation(qapp, qtbot): + password_input = PasswordInput(minimum_length=10) + qtbot.addWidget(password_input.create_form_widget(parent=qapp.main_window)) + + qtbot.keyClicks(password_input.passwordLineEdit, "123456789") + qtbot.keyClicks(password_input.confirmLineEdit, "123456789") + + assert password_input.passwordLineEdit.error_state + assert password_input.validation_label.text() == "Passwords must be atleast 10 characters long." + + password_input.clear() + qtbot.keyClicks(password_input.passwordLineEdit, "123456789") + qtbot.keyClicks(password_input.confirmLineEdit, "test") + + assert password_input.passwordLineEdit.error_state + assert password_input.confirmLineEdit.error_state + assert password_input.validation_label.text() == "Passwords must be identical and atleast 10 characters long." + + password_input.clear() + qtbot.keyClicks(password_input.passwordLineEdit, "1234567890") + qtbot.keyClicks(password_input.confirmLineEdit, "test") + + assert not password_input.passwordLineEdit.error_state + assert password_input.confirmLineEdit.error_state + assert password_input.validation_label.text() == "Passwords must be identical." + + password_input.clear() + qtbot.keyClicks(password_input.passwordLineEdit, "1234567890") + qtbot.keyClicks(password_input.confirmLineEdit, "1234567890") + + assert not password_input.passwordLineEdit.error_state + assert not password_input.confirmLineEdit.error_state + assert password_input.validation_label.text() == "" + + +def test_password_input_validation_disabled(qapp, qtbot): + password_input = PasswordInput(show_error=False) + qtbot.addWidget(password_input.create_form_widget(parent=qapp.main_window)) + + qtbot.keyClicks(password_input.passwordLineEdit, "test") + qtbot.keyClicks(password_input.confirmLineEdit, "test") + + assert not password_input.passwordLineEdit.error_state + assert not password_input.confirmLineEdit.error_state + assert password_input.validation_label.text() == "" + + password_input.set_validation_enabled(True) + qtbot.keyClicks(password_input.passwordLineEdit, "s") + qtbot.keyClicks(password_input.confirmLineEdit, "a") + + assert password_input.passwordLineEdit.error_state + assert password_input.confirmLineEdit.error_state + assert password_input.validation_label.text() == "Passwords must be identical and atleast 9 characters long." + + password_input.set_validation_enabled(False) + assert not password_input.passwordLineEdit.error_state + assert not password_input.confirmLineEdit.error_state + assert password_input.validation_label.text() == "" + + +def test_password_input_set_label(qapp, qtbot): + password_input = PasswordInput(label=["test", "test2"]) + qtbot.addWidget(password_input.create_form_widget(parent=qapp.main_window)) + + assert password_input._label_password.text() == "test" + assert password_input._label_confirm.text() == "test2" + + password_input.set_labels("test3", "test4") + assert password_input._label_password.text() == "test3" + assert password_input._label_confirm.text() == "test4" + + +def test_password_input_add_form_to_layout(qapp, qtbot): + password_input = PasswordInput() + + widget = QWidget() + form_layout = QFormLayout(widget) + + qtbot.addWidget(widget) + password_input.add_form_to_layout(form_layout) + + assert form_layout.itemAt(0, QFormLayout.ItemRole.LabelRole).widget() == password_input._label_password + assert form_layout.itemAt(0, QFormLayout.ItemRole.FieldRole).widget() == password_input.passwordLineEdit + assert form_layout.itemAt(1, QFormLayout.ItemRole.LabelRole).widget() == password_input._label_confirm + assert form_layout.itemAt(1, QFormLayout.ItemRole.FieldRole).widget() == password_input.confirmLineEdit diff --git a/tests/test_repo.py b/tests/test_repo.py index b5b070968..3e13084d7 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -19,33 +19,36 @@ def test_repo_add_failures(qapp, qtbot, mocker, borg_json_output): add_repo_window = main.repoTab._window qtbot.addWidget(add_repo_window) - qtbot.keyClicks(add_repo_window.passwordLineEdit, LONG_PASSWORD) - qtbot.keyClicks(add_repo_window.confirmLineEdit, LONG_PASSWORD) + qtbot.keyClicks(add_repo_window.passwordInput.passwordLineEdit, LONG_PASSWORD) + qtbot.keyClicks(add_repo_window.passwordInput.confirmLineEdit, LONG_PASSWORD) qtbot.keyClicks(add_repo_window.repoURL, 'aaa') qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.MouseButton.LeftButton) assert add_repo_window.errorText.text().startswith('Please enter a valid') - add_repo_window.passwordLineEdit.clear() - add_repo_window.confirmLineEdit.clear() - qtbot.keyClicks(add_repo_window.passwordLineEdit, SHORT_PASSWORD) - qtbot.keyClicks(add_repo_window.confirmLineEdit, SHORT_PASSWORD) + add_repo_window.passwordInput.passwordLineEdit.clear() + add_repo_window.passwordInput.confirmLineEdit.clear() + qtbot.keyClicks(add_repo_window.passwordInput.passwordLineEdit, SHORT_PASSWORD) + qtbot.keyClicks(add_repo_window.passwordInput.confirmLineEdit, SHORT_PASSWORD) qtbot.keyClicks(add_repo_window.repoURL, 'bbb.com:repo') qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.MouseButton.LeftButton) - assert add_repo_window.passwordLabel.text() == 'Passwords must be greater than 8 characters long.' + assert add_repo_window.passwordInput.validation_label.text() == 'Passwords must be atleast 9 characters long.' - add_repo_window.passwordLineEdit.clear() - add_repo_window.confirmLineEdit.clear() - qtbot.keyClicks(add_repo_window.passwordLineEdit, SHORT_PASSWORD + "1") - qtbot.keyClicks(add_repo_window.confirmLineEdit, SHORT_PASSWORD) + add_repo_window.passwordInput.passwordLineEdit.clear() + add_repo_window.passwordInput.confirmLineEdit.clear() + qtbot.keyClicks(add_repo_window.passwordInput.passwordLineEdit, SHORT_PASSWORD + "1") + qtbot.keyClicks(add_repo_window.passwordInput.confirmLineEdit, SHORT_PASSWORD) qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.MouseButton.LeftButton) - assert add_repo_window.passwordLabel.text() == 'Passwords must be identical and greater than 8 characters long.' + assert ( + add_repo_window.passwordInput.validation_label.text() + == 'Passwords must be identical and atleast 9 characters long.' + ) - add_repo_window.passwordLineEdit.clear() - add_repo_window.confirmLineEdit.clear() - qtbot.keyClicks(add_repo_window.passwordLineEdit, LONG_PASSWORD) - qtbot.keyClicks(add_repo_window.confirmLineEdit, SHORT_PASSWORD) + add_repo_window.passwordInput.passwordLineEdit.clear() + add_repo_window.passwordInput.confirmLineEdit.clear() + qtbot.keyClicks(add_repo_window.passwordInput.passwordLineEdit, LONG_PASSWORD) + qtbot.keyClicks(add_repo_window.passwordInput.confirmLineEdit, SHORT_PASSWORD) qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.MouseButton.LeftButton) - assert add_repo_window.passwordLabel.text() == 'Passwords must be identical.' + assert add_repo_window.passwordInput.validation_label.text() == 'Passwords must be identical.' def test_repo_unlink(qapp, qtbot, monkeypatch): @@ -76,7 +79,7 @@ def test_password_autofill(qapp, qtbot): qtbot.keyClicks(add_repo_window.repoURL, test_repo_url) - assert add_repo_window.passwordLineEdit.text() == password + assert add_repo_window.passwordInput.passwordLineEdit.text() == password def test_repo_add_success(qapp, qtbot, mocker, borg_json_output): @@ -89,8 +92,8 @@ def test_repo_add_success(qapp, qtbot, mocker, borg_json_output): qtbot.keyClicks(add_repo_window.repoURL, test_repo_url) qtbot.keyClicks(add_repo_window.repoName, test_repo_name) - qtbot.keyClicks(add_repo_window.passwordLineEdit, LONG_PASSWORD) - qtbot.keyClicks(add_repo_window.confirmLineEdit, LONG_PASSWORD) + qtbot.keyClicks(add_repo_window.passwordInput.passwordLineEdit, LONG_PASSWORD) + qtbot.keyClicks(add_repo_window.passwordInput.confirmLineEdit, LONG_PASSWORD) stdout, stderr = borg_json_output('info') popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)