Add unit tests for configuration management and settings dialog
- Implement tests for ConfigValidator to ensure proper validation of configuration settings, including valid configurations, required fields, type checks, and error handling. - Create tests for ConfigProfile to verify profile management functionalities such as saving, loading, listing, and deleting profiles. - Add tests for ConfigExporter to validate JSON export and import processes, including error handling for non-existent files and invalid JSON. - Introduce tests for SettingsDialog to confirm proper initialization, tab existence, and configuration data retrieval and application.
This commit is contained in:
parent
5dc988005c
commit
8b0df0e04f
7 changed files with 1556 additions and 4 deletions
434
src/webdrop_bridge/ui/settings_dialog.py
Normal file
434
src/webdrop_bridge/ui/settings_dialog.py
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
"""Settings dialog for configuration management."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from webdrop_bridge.config import Config, ConfigurationError
|
||||
from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""Dialog for managing application settings and configuration.
|
||||
|
||||
Provides tabs for:
|
||||
- Paths: Manage allowed root directories
|
||||
- URLs: Manage allowed web URLs
|
||||
- Logging: Configure logging settings
|
||||
- Window: Manage window size and behavior
|
||||
- Profiles: Save/load/delete configuration profiles
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config, parent=None):
|
||||
"""Initialize the settings dialog.
|
||||
|
||||
Args:
|
||||
config: Current application configuration
|
||||
parent: Parent widget
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.config = config
|
||||
self.profile_manager = ConfigProfile()
|
||||
self.setWindowTitle("Settings")
|
||||
self.setGeometry(100, 100, 600, 500)
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self) -> None:
|
||||
"""Set up the dialog UI with tabs."""
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Create tab widget
|
||||
self.tabs = QTabWidget()
|
||||
|
||||
# Add tabs
|
||||
self.tabs.addTab(self._create_paths_tab(), "Paths")
|
||||
self.tabs.addTab(self._create_urls_tab(), "URLs")
|
||||
self.tabs.addTab(self._create_logging_tab(), "Logging")
|
||||
self.tabs.addTab(self._create_window_tab(), "Window")
|
||||
self.tabs.addTab(self._create_profiles_tab(), "Profiles")
|
||||
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
# Add buttons
|
||||
button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _create_paths_tab(self) -> QWidget:
|
||||
"""Create paths configuration tab."""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
layout.addWidget(QLabel("Allowed root directories for file access:"))
|
||||
|
||||
# List widget for paths
|
||||
self.paths_list = QListWidget()
|
||||
for path in self.config.allowed_roots:
|
||||
self.paths_list.addItem(str(path))
|
||||
layout.addWidget(self.paths_list)
|
||||
|
||||
# Buttons for path management
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
add_path_btn = QPushButton("Add Path")
|
||||
add_path_btn.clicked.connect(self._add_path)
|
||||
button_layout.addWidget(add_path_btn)
|
||||
|
||||
remove_path_btn = QPushButton("Remove Selected")
|
||||
remove_path_btn.clicked.connect(self._remove_path)
|
||||
button_layout.addWidget(remove_path_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
layout.addStretch()
|
||||
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def _create_urls_tab(self) -> QWidget:
|
||||
"""Create URLs configuration tab."""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
layout.addWidget(QLabel("Allowed web URLs (supports wildcards like http://*.example.com):"))
|
||||
|
||||
# List widget for URLs
|
||||
self.urls_list = QListWidget()
|
||||
for url in self.config.allowed_urls:
|
||||
self.urls_list.addItem(url)
|
||||
layout.addWidget(self.urls_list)
|
||||
|
||||
# Buttons for URL management
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
add_url_btn = QPushButton("Add URL")
|
||||
add_url_btn.clicked.connect(self._add_url)
|
||||
button_layout.addWidget(add_url_btn)
|
||||
|
||||
remove_url_btn = QPushButton("Remove Selected")
|
||||
remove_url_btn.clicked.connect(self._remove_url)
|
||||
button_layout.addWidget(remove_url_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
layout.addStretch()
|
||||
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def _create_logging_tab(self) -> QWidget:
|
||||
"""Create logging configuration tab."""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Log level selection
|
||||
layout.addWidget(QLabel("Log Level:"))
|
||||
self.log_level_combo = self._create_log_level_widget()
|
||||
layout.addWidget(self.log_level_combo)
|
||||
|
||||
# Log file path
|
||||
layout.addWidget(QLabel("Log File (optional):"))
|
||||
log_file_layout = QHBoxLayout()
|
||||
|
||||
self.log_file_input = QLineEdit()
|
||||
self.log_file_input.setText(str(self.config.log_file) if self.config.log_file else "")
|
||||
log_file_layout.addWidget(self.log_file_input)
|
||||
|
||||
browse_btn = QPushButton("Browse...")
|
||||
browse_btn.clicked.connect(self._browse_log_file)
|
||||
log_file_layout.addWidget(browse_btn)
|
||||
|
||||
layout.addLayout(log_file_layout)
|
||||
|
||||
layout.addStretch()
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def _create_window_tab(self) -> QWidget:
|
||||
"""Create window settings tab."""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Window width
|
||||
width_layout = QHBoxLayout()
|
||||
width_layout.addWidget(QLabel("Window Width:"))
|
||||
self.width_spin = QSpinBox()
|
||||
self.width_spin.setMinimum(400)
|
||||
self.width_spin.setMaximum(5000)
|
||||
self.width_spin.setValue(self.config.window_width)
|
||||
width_layout.addWidget(self.width_spin)
|
||||
width_layout.addStretch()
|
||||
layout.addLayout(width_layout)
|
||||
|
||||
# Window height
|
||||
height_layout = QHBoxLayout()
|
||||
height_layout.addWidget(QLabel("Window Height:"))
|
||||
self.height_spin = QSpinBox()
|
||||
self.height_spin.setMinimum(300)
|
||||
self.height_spin.setMaximum(5000)
|
||||
self.height_spin.setValue(self.config.window_height)
|
||||
height_layout.addWidget(self.height_spin)
|
||||
height_layout.addStretch()
|
||||
layout.addLayout(height_layout)
|
||||
|
||||
layout.addStretch()
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def _create_profiles_tab(self) -> QWidget:
|
||||
"""Create profiles management tab."""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
layout.addWidget(QLabel("Saved Configuration Profiles:"))
|
||||
|
||||
# List of profiles
|
||||
self.profiles_list = QListWidget()
|
||||
self._refresh_profiles_list()
|
||||
layout.addWidget(self.profiles_list)
|
||||
|
||||
# Profile management buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
save_profile_btn = QPushButton("Save as Profile")
|
||||
save_profile_btn.clicked.connect(self._save_profile)
|
||||
button_layout.addWidget(save_profile_btn)
|
||||
|
||||
load_profile_btn = QPushButton("Load Profile")
|
||||
load_profile_btn.clicked.connect(self._load_profile)
|
||||
button_layout.addWidget(load_profile_btn)
|
||||
|
||||
delete_profile_btn = QPushButton("Delete Profile")
|
||||
delete_profile_btn.clicked.connect(self._delete_profile)
|
||||
button_layout.addWidget(delete_profile_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Export/Import buttons
|
||||
export_layout = QHBoxLayout()
|
||||
|
||||
export_btn = QPushButton("Export Configuration")
|
||||
export_btn.clicked.connect(self._export_config)
|
||||
export_layout.addWidget(export_btn)
|
||||
|
||||
import_btn = QPushButton("Import Configuration")
|
||||
import_btn.clicked.connect(self._import_config)
|
||||
export_layout.addWidget(import_btn)
|
||||
|
||||
layout.addLayout(export_layout)
|
||||
layout.addStretch()
|
||||
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def _create_log_level_widget(self) -> QWidget:
|
||||
"""Create log level selection widget."""
|
||||
from PySide6.QtWidgets import QComboBox
|
||||
|
||||
combo = QComboBox()
|
||||
levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
combo.addItems(levels)
|
||||
combo.setCurrentText(self.config.log_level)
|
||||
return combo
|
||||
|
||||
def _add_path(self) -> None:
|
||||
"""Add a new allowed path."""
|
||||
path = QFileDialog.getExistingDirectory(self, "Select Directory to Allow")
|
||||
if path:
|
||||
self.paths_list.addItem(path)
|
||||
|
||||
def _remove_path(self) -> None:
|
||||
"""Remove selected path."""
|
||||
if self.paths_list.currentItem():
|
||||
self.paths_list.takeItem(self.paths_list.row(self.paths_list.currentItem()))
|
||||
|
||||
def _add_url(self) -> None:
|
||||
"""Add a new allowed URL."""
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
url, ok = QInputDialog.getText(
|
||||
self,
|
||||
"Add URL",
|
||||
"Enter URL pattern (e.g., http://example.com or http://*.example.com):"
|
||||
)
|
||||
if ok and url:
|
||||
self.urls_list.addItem(url)
|
||||
|
||||
def _remove_url(self) -> None:
|
||||
"""Remove selected URL."""
|
||||
if self.urls_list.currentItem():
|
||||
self.urls_list.takeItem(self.urls_list.row(self.urls_list.currentItem()))
|
||||
|
||||
def _browse_log_file(self) -> None:
|
||||
"""Browse for log file location."""
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Select Log File",
|
||||
str(Path.home()),
|
||||
"Log Files (*.log);;All Files (*)"
|
||||
)
|
||||
if file_path:
|
||||
self.log_file_input.setText(file_path)
|
||||
|
||||
def _refresh_profiles_list(self) -> None:
|
||||
"""Refresh the list of available profiles."""
|
||||
self.profiles_list.clear()
|
||||
for profile_name in self.profile_manager.list_profiles():
|
||||
self.profiles_list.addItem(profile_name)
|
||||
|
||||
def _save_profile(self) -> None:
|
||||
"""Save current configuration as a profile."""
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
profile_name, ok = QInputDialog.getText(
|
||||
self,
|
||||
"Save Profile",
|
||||
"Enter profile name (e.g., work, personal):"
|
||||
)
|
||||
|
||||
if ok and profile_name:
|
||||
try:
|
||||
self.profile_manager.save_profile(profile_name, self.config)
|
||||
self._refresh_profiles_list()
|
||||
except ConfigurationError as e:
|
||||
self._show_error(f"Failed to save profile: {e}")
|
||||
|
||||
def _load_profile(self) -> None:
|
||||
"""Load a saved profile."""
|
||||
current_item = self.profiles_list.currentItem()
|
||||
if not current_item:
|
||||
self._show_error("Please select a profile to load")
|
||||
return
|
||||
|
||||
profile_name = current_item.text()
|
||||
try:
|
||||
config_data = self.profile_manager.load_profile(profile_name)
|
||||
self._apply_config_data(config_data)
|
||||
except ConfigurationError as e:
|
||||
self._show_error(f"Failed to load profile: {e}")
|
||||
|
||||
def _delete_profile(self) -> None:
|
||||
"""Delete a saved profile."""
|
||||
current_item = self.profiles_list.currentItem()
|
||||
if not current_item:
|
||||
self._show_error("Please select a profile to delete")
|
||||
return
|
||||
|
||||
profile_name = current_item.text()
|
||||
try:
|
||||
self.profile_manager.delete_profile(profile_name)
|
||||
self._refresh_profiles_list()
|
||||
except ConfigurationError as e:
|
||||
self._show_error(f"Failed to delete profile: {e}")
|
||||
|
||||
def _export_config(self) -> None:
|
||||
"""Export configuration to file."""
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Export Configuration",
|
||||
str(Path.home()),
|
||||
"JSON Files (*.json);;All Files (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
ConfigExporter.export_to_json(self.config, Path(file_path))
|
||||
except ConfigurationError as e:
|
||||
self._show_error(f"Failed to export configuration: {e}")
|
||||
|
||||
def _import_config(self) -> None:
|
||||
"""Import configuration from file."""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Import Configuration",
|
||||
str(Path.home()),
|
||||
"JSON Files (*.json);;All Files (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
config_data = ConfigExporter.import_from_json(Path(file_path))
|
||||
self._apply_config_data(config_data)
|
||||
except ConfigurationError as e:
|
||||
self._show_error(f"Failed to import configuration: {e}")
|
||||
|
||||
def _apply_config_data(self, config_data: dict) -> None:
|
||||
"""Apply imported configuration data to UI.
|
||||
|
||||
Args:
|
||||
config_data: Configuration dictionary
|
||||
"""
|
||||
# Apply paths
|
||||
self.paths_list.clear()
|
||||
for path in config_data.get("allowed_roots", []):
|
||||
self.paths_list.addItem(str(path))
|
||||
|
||||
# Apply URLs
|
||||
self.urls_list.clear()
|
||||
for url in config_data.get("allowed_urls", []):
|
||||
self.urls_list.addItem(url)
|
||||
|
||||
# Apply logging settings
|
||||
self.log_level_combo.setCurrentText(config_data.get("log_level", "INFO"))
|
||||
log_file = config_data.get("log_file")
|
||||
self.log_file_input.setText(str(log_file) if log_file else "")
|
||||
|
||||
# Apply window settings
|
||||
self.width_spin.setValue(config_data.get("window_width", 800))
|
||||
self.height_spin.setValue(config_data.get("window_height", 600))
|
||||
|
||||
def get_config_data(self) -> dict:
|
||||
"""Get updated configuration data from dialog.
|
||||
|
||||
Returns:
|
||||
Configuration dictionary
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If configuration is invalid
|
||||
"""
|
||||
config_data = {
|
||||
"app_name": self.config.app_name,
|
||||
"app_version": self.config.app_version,
|
||||
"log_level": self.log_level_combo.currentText(),
|
||||
"log_file": self.log_file_input.text() or None,
|
||||
"allowed_roots": [self.paths_list.item(i).text() for i in range(self.paths_list.count())],
|
||||
"allowed_urls": [self.urls_list.item(i).text() for i in range(self.urls_list.count())],
|
||||
"webapp_url": self.config.webapp_url,
|
||||
"window_width": self.width_spin.value(),
|
||||
"window_height": self.height_spin.value(),
|
||||
"enable_logging": self.config.enable_logging,
|
||||
}
|
||||
|
||||
# Validate
|
||||
ConfigValidator.validate_or_raise(config_data)
|
||||
|
||||
return config_data
|
||||
|
||||
def _show_error(self, message: str) -> None:
|
||||
"""Show error message to user.
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
"""
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
QMessageBox.critical(self, "Error", message)
|
||||
Loading…
Add table
Add a link
Reference in a new issue