mirror of
https://github.com/borgbase/vorta
synced 2024-12-21 23:33:13 +00:00
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:
parent
157ac373a9
commit
25b4cc0b8b
6 changed files with 474 additions and 228 deletions
|
@ -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">
|
||||
|
|
|
@ -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.
|
||||
|
|
172
src/vorta/views/partials/password_input.py
Normal file
172
src/vorta/views/partials/password_input.py
Normal 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
|
|
@ -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()
|
||||
|
||||
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():
|
||||
|
|
165
tests/test_password_input.py
Normal file
165
tests/test_password_input.py
Normal 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
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue