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 : ^^^
This commit is contained in:
jetchirag 2023-07-28 13:09:10 +05:30 committed by GitHub
parent 157ac373a9
commit 25b4cc0b8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 474 additions and 228 deletions

View File

@ -2,14 +2,6 @@
<ui version="4.0">
<class>AddRepository</class>
<widget class="QDialog" name="AddRepository">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>583</width>
<height>338</height>
</rect>
</property>
<property name="modal">
<bool>true</bool>
</property>
@ -82,7 +74,7 @@
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
<number>20</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="title">
@ -169,47 +161,6 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Borg passphrase:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="passwordLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="confirmLabel">
<property name="text">
<string>Confirm passphrase:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="confirmLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabWidgetPage2">
@ -254,23 +205,6 @@
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="encryptionLabel">
<property name="text">
<string>Encryption:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="encryptionComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">

View File

@ -18,7 +18,6 @@ from PyQt6.QtCore import QFileInfo, QThread, pyqtSignal
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.

View File

@ -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

View File

@ -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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
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()
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(AddRepoWindow):
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)
self.passwordLabel = QLabel(self.tr('Password:'))
self.passwordInput = PasswordLineEdit()
self.repoDataFormLayout.addRow(self.passwordLabel, self.passwordInput)
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"))
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():

View File

@ -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

View File

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