From 836cc60cd74e20333081e7c55632a718f73e339e Mon Sep 17 00:00:00 2001 From: Yifree Date: Thu, 26 Jun 2025 13:48:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=8E=E5=88=86=E6=94=AF=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +- README_zh.md | 20 +- build.py | 76 +++ check_password_settings.py | 33 ++ fix_password_issue.py | 86 ++++ main.py | 53 ++ password_diagnostic.py | 154 ++++++ reset_password_protection.py | 88 ++++ tests/simple_password_test.py | 100 ++++ tests/test_password_feature.py | 253 +++++++++ tests/test_password_integration.py | 231 +++++++++ ui/__pycache__/dialogs.cpython-313.pyc | Bin 100755 -> 103147 bytes .../password_dialogs.cpython-313.pyc | Bin 0 -> 28624 bytes ui/dialogs.py | 127 +++-- ui/password_dialogs.py | 483 ++++++++++++++++++ .../password_manager.cpython-313.pyc | Bin 0 -> 10830 bytes utils/password_manager.py | 278 ++++++++++ ...56\346\224\271\350\256\260\345\275\225.md" | 82 ++- ...37\350\203\275\350\257\264\346\230\216.md" | 170 ++++++ 19 files changed, 2192 insertions(+), 58 deletions(-) create mode 100644 build.py create mode 100644 check_password_settings.py create mode 100644 fix_password_issue.py create mode 100644 password_diagnostic.py create mode 100644 reset_password_protection.py create mode 100644 tests/simple_password_test.py create mode 100644 tests/test_password_feature.py create mode 100644 tests/test_password_integration.py create mode 100644 ui/__pycache__/password_dialogs.cpython-313.pyc create mode 100644 ui/password_dialogs.py create mode 100644 utils/__pycache__/password_manager.cpython-313.pyc create mode 100644 utils/password_manager.py create mode 100644 "\345\257\206\347\240\201\344\277\235\346\212\244\345\212\237\350\203\275\350\257\264\346\230\216.md" diff --git a/README.md b/README.md index 26a63cd..fb570f4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A modern, feature-rich desktop application for managing and organizing your book - 📥 **Import/Export**: Support for Chrome, Firefox, HTML, and JSON formats - 🎲 **Blind Box**: Random bookmark discovery feature - 📚 **History Tracking**: Keep track of accessed bookmarks +- 🔒 **Password Protection**: Secure your bookmarks with password authentication - 🔒 **Edit Lock Mode**: Prevent accidental modifications - 🌍 **Multi-language**: Support for Chinese, English, Japanese, German, French, Korean, and Spanish - 💾 **Auto-backup**: Automatic data backup and restore @@ -60,11 +61,16 @@ A modern, feature-rich desktop application for managing and organizing your book ### Basic Operations -1. Adding Bookmarks : Click the "Add" button or drag URLs from your browser -2. Creating Folders : Right-click in the sidebar to create new folders -3. Searching : Use Ctrl+F or click the search icon -4. Import/Export : Access through the File menu - For detailed instructions, see the User Guide . +1. **Adding Bookmarks**: Click the "Add" button or drag URLs from your browser +2. **Creating Folders**: Right-click in the sidebar to create new folders +3. **Searching**: Use Ctrl+F or click the search icon +4. **Import/Export**: Access through the File menu +5. **Password Protection**: Set up password protection in Settings → Password Settings + - Enable password protection and set your password + - Configure security questions for password recovery + - Password will be required on next startup + +For detailed instructions, see the User Guide. ## 🛠️ Development diff --git a/README_zh.md b/README_zh.md index f0f4583..a352094 100644 --- a/README_zh.md +++ b/README_zh.md @@ -16,6 +16,7 @@ - 📥 **导入/导出**:支持 Chrome、Firefox、HTML 和 JSON 格式 - 🎲 **盲盒功能**:随机书签发现功能 - 📚 **历史记录**:跟踪访问过的书签 +- 🔒 **密码保护**:使用密码验证保护您的书签安全 - 🔒 **编辑锁定模式**:防止意外修改 - 🌍 **多语言支持**:支持中文、英文、日文、德文、法文、韩文和西班牙文 - 💾 **自动备份**:自动数据备份和恢复 @@ -60,12 +61,17 @@ ### 基本操作 -1. 添加书签 :点击"添加"按钮或从浏览器拖拽 URL -2. 创建文件夹 :在侧边栏右键点击创建新文件夹 -3. 搜索 :使用 Ctrl+F 或点击搜索图标 -4. 导入/导出 :通过文件菜单访问 -5. 开魔盒:也叫开盲盒,通过点击右上角的圆形魔盒按钮打开。可直接选择数字(1-5)或输入数字(>5)随机浏览。 - 详细说明请参阅 用户指南 。 +1. **添加书签**:点击"添加"按钮或从浏览器拖拽 URL +2. **创建文件夹**:在侧边栏右键点击创建新文件夹 +3. **搜索**:使用 Ctrl+F 或点击搜索图标 +4. **导入/导出**:通过文件菜单访问 +5. **密码保护**:在设置 → 密码设置中配置密码保护 + - 启用密码保护并设置密码 + - 配置密保问题用于密码恢复 + - 下次启动时将需要输入密码 +6. **开魔盒**:也叫开盲盒,通过点击右上角的圆形魔盒按钮打开。可直接选择数字(1-5)或输入数字(>5)随机浏览。 + +详细说明请参阅 用户指南。 ## 🛠️ 开发 @@ -108,7 +114,7 @@ url-navigator/ ### 下载体验版 -[URL Navigator单EXE体验版](https://github.com/yihufree/URL-Navigator/releases/download/V0.60/URLNav_20250611.zip) +[URL Navigator单EXE体验版](https://github.com/yihufree/URL-Navigator/releases/download/V0.51/URLNav_20250611v0.51.zip) ## 🌐 国际化 diff --git a/build.py b/build.py new file mode 100644 index 0000000..8329a9b --- /dev/null +++ b/build.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import shutil +import subprocess +import platform +import datetime + +def build_exe(): + """构建EXE文件""" + print("开始构建EXE文件...") + + # 清理旧的构建文件 + if os.path.exists("build"): + shutil.rmtree("build") + if os.path.exists("dist"): + shutil.rmtree("dist") + + # 确保资源文件夹存在 + if not os.path.exists("resources"): + print("错误: 资源文件夹不存在!") + return + + # 获取版本日期 + version_date = datetime.datetime.now().strftime("%Y%m%d") + output_filename = f"URLNav_{version_date}" + + # 构建命令 + cmd = [ + "pyinstaller", + f"--name={output_filename}", + "--windowed", # 无控制台窗口 + "--onefile", # 单个EXE文件 + "--clean", # 清理临时文件 + "--icon=resources/icons/app_icon.ico", # 应用图标 + "--add-data=resources;resources", # 添加整个资源目录 + "--add-data=languages;languages", # 添加语言文件目录 + "--noconfirm", # 不显示确认对话框 + "main.py" # 主程序入口 + ] + + # 根据操作系统调整路径分隔符 + if platform.system() != "Windows": + # 对于Unix系统使用冒号分隔符 + for i in range(len(cmd)): + if "--add-data=" in cmd[i]: + cmd[i] = cmd[i].replace(";", ":") + + print(f"运行命令: {' '.join(cmd)}") + + # 执行构建 + try: + subprocess.call(cmd, shell=False) # 明确禁用shell以防止命令注入 + + # 检查是否成功创建EXE文件 + exe_path = os.path.join("dist", f"{output_filename}.exe") + if os.path.exists(exe_path): + print(f"构建成功!EXE文件大小: {os.path.getsize(exe_path) / (1024*1024):.2f} MB") + print(f"EXE文件位置: {os.path.abspath(exe_path)}") + else: + print("构建似乎完成,但找不到EXE文件") + + except Exception as e: + print(f"构建过程中出错: {e}") + return + +def resource_path(relative_path): + """获取资源文件的绝对路径,兼容开发环境和打包后环境""" + if hasattr(sys, '_MEIPASS'): + return os.path.join(sys._MEIPASS, relative_path) + return os.path.join(os.path.abspath("."), relative_path) + +if __name__ == "__main__": + build_exe() \ No newline at end of file diff --git a/check_password_settings.py b/check_password_settings.py new file mode 100644 index 0000000..3491bfa --- /dev/null +++ b/check_password_settings.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检查密码设置状态 +""" + +from PyQt5.QtCore import QSettings + +def main(): + settings = QSettings("URL Navigator", "URL Navigator") + + # 检查各项设置 + password_enabled = settings.value("security/password_enabled", False) + password_hash = settings.value("security/password_hash", "") + password_salt = settings.value("security/password_salt", "") + security_question = settings.value("security/security_question", "") + + print(f"密码保护启用: {password_enabled}") + print(f"密码哈希: {'存在' if password_hash else '不存在'}") + print(f"密码盐值: {'存在' if password_salt else '不存在'}") + print(f"密保问题: {security_question if security_question else '未设置'}") + + # 如果启用了密码保护但没有密码数据,说明有问题 + if password_enabled and (not password_hash or not password_salt): + print("\n问题: 密码保护已启用但密码数据缺失!") + print("建议: 运行 reset_password_protection.py 重置设置") + elif password_enabled: + print("\n状态: 密码保护正常") + else: + print("\n状态: 密码保护未启用") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/fix_password_issue.py b/fix_password_issue.py new file mode 100644 index 0000000..b7cc8d9 --- /dev/null +++ b/fix_password_issue.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +URLNav 密码问题快速修复工具 + +这个工具用于解决首次启动时要求输入密码但不知道密码的问题。 +""" + +import sys +import os +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import QApplication, QMessageBox + +def clear_all_password_settings(): + """清除所有密码相关设置""" + try: + # 尝试不同的组织名和应用名组合 + settings_combinations = [ + ("URL Navigator", "URL Navigator"), + ("URLNav", "URLNav"), + ("", "URLNav"), + ("", "URL Navigator") + ] + + cleared_any = False + + for org, app in settings_combinations: + settings = QSettings(org, app) + + # 检查是否有密码相关设置 + keys_to_check = [ + "security/password_enabled", + "security/password_hash", + "security/password_salt", + "security/security_question", + "security/security_answer_hash", + "security/security_answer_salt" + ] + + has_settings = any(settings.contains(key) for key in keys_to_check) + + if has_settings: + print(f"发现密码设置在: 组织='{org}', 应用='{app}'") + + # 清除所有密码相关设置 + for key in keys_to_check: + if settings.contains(key): + settings.remove(key) + print(f" 已清除: {key}") + + settings.sync() + cleared_any = True + + return cleared_any + + except Exception as e: + print(f"清除设置时出错: {e}") + return False + +def main(): + print("URLNav 密码问题快速修复工具") + print("=" * 40) + + # 创建临时应用(不显示界面) + app = QApplication(sys.argv) if not QApplication.instance() else QApplication.instance() + + print("正在检查和清除密码设置...") + + if clear_all_password_settings(): + print("\n✅ 已成功清除所有密码设置") + print("\n现在您可以正常启动 URLNav 应用程序了。") + print("如果需要重新设置密码保护,请在应用程序的设置中进行配置。") + else: + print("\n❓ 未找到任何密码设置") + print("\n如果仍然遇到密码提示问题,可能的原因:") + print("1. 配置文件位置不同") + print("2. 应用程序代码中有其他问题") + print("\n建议联系开发者获取进一步帮助。") + + print("\n修复工具执行完成。") + + # 不需要显示GUI,直接退出 + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py index 7d02a35..a656d38 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,8 @@ import json from logging.handlers import RotatingFileHandler from threading import Lock +from utils.password_manager import PasswordManager +from ui.password_dialogs import PasswordInputDialog # 配置日志 def setup_logging(): @@ -54,6 +56,52 @@ def setup_logging(): logger = logging.getLogger(__name__) +def verify_password(): + """验证密码""" + try: + password_manager = PasswordManager() + + # 如果未启用密码保护,直接返回True + if not password_manager.is_password_enabled(): + logger.info("密码保护未启用,跳过验证") + return True + + logger.info("密码保护已启用,需要验证密码") + + # 创建临时应用程序实例用于显示对话框 + temp_app = None + if not QApplication.instance(): + temp_app = QApplication(sys.argv) + + # 显示密码输入对话框 + from utils.language_manager import language_manager + dialog = PasswordInputDialog( + parent=None, + language_manager=language_manager, + password_manager=password_manager + ) + + # 执行对话框 + result = dialog.exec_() + + # 清理临时应用程序 + if temp_app: + temp_app.quit() + + if result == QDialog.Accepted: + logger.info("密码验证成功") + return True + else: + logger.info("用户取消密码验证") + return False + + except Exception as e: + logger.error(f"密码验证过程中发生错误: {e}") + import traceback + traceback.print_exc() + # 如果验证过程出错,为了安全起见,拒绝访问 + return False + def main(): try: # 设置日志 @@ -84,6 +132,11 @@ def main(): with open(style_path, "r", encoding="utf-8") as f: app.setStyleSheet(f.read()) + # 密码验证 + if not verify_password(): + logger.info("密码验证失败,程序退出") + sys.exit(0) + # 创建并显示主窗口 window = UrlNavigatorApp() window.show() diff --git a/password_diagnostic.py b/password_diagnostic.py new file mode 100644 index 0000000..f2ce0f9 --- /dev/null +++ b/password_diagnostic.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +密码保护诊断和修复工具 +""" + +import sys +import os +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import QApplication + +def check_all_settings(): + """检查所有相关设置""" + print("=== 完整诊断报告 ===") + + # 检查不同的设置存储位置 + settings_configs = [ + ("URL Navigator", "URL Navigator"), + ("URLNav", "URLNav"), + ("URLNavigator", "URLNavigator"), + ] + + found_settings = False + + for org, app in settings_configs: + settings = QSettings(org, app) + + # 获取所有键 + all_keys = settings.allKeys() + security_keys = [key for key in all_keys if key.startswith('security/')] + + if security_keys: + found_settings = True + print(f"\n在 {org}/{app} 中找到安全设置:") + + for key in security_keys: + value = settings.value(key) + print(f" {key}: {value}") + + # 检查密码保护状态 + password_enabled = settings.value("security/password_enabled", False) + if isinstance(password_enabled, str): + password_enabled = password_enabled.lower() == "true" + + if password_enabled: + print(f" ⚠️ 在 {org}/{app} 中发现启用的密码保护!") + return settings, org, app + + if not found_settings: + print("\n✅ 未找到任何密码保护设置") + return None, None, None + + return None, None, None + +def clear_all_password_settings(): + """清除所有可能的密码设置""" + print("\n=== 清除所有密码设置 ===") + + settings_configs = [ + ("URL Navigator", "URL Navigator"), + ("URLNav", "URLNav"), + ("URLNavigator", "URLNavigator"), + ] + + for org, app in settings_configs: + settings = QSettings(org, app) + + # 清除所有安全相关设置 + security_keys = [ + "security/password_enabled", + "security/password_hash", + "security/password_salt", + "security/security_question", + "security/security_answer_hash", + "security/security_answer_salt" + ] + + cleared_any = False + for key in security_keys: + if settings.contains(key): + settings.remove(key) + cleared_any = True + + if cleared_any: + settings.sync() + print(f"✅ 已清除 {org}/{app} 中的密码设置") + + print("\n所有密码设置已清除") + +def test_password_manager(): + """测试密码管理器""" + print("\n=== 测试密码管理器 ===") + + try: + # 添加项目路径 + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + + from utils.password_manager import PasswordManager + + pm = PasswordManager() + enabled = pm.is_password_enabled() + + print(f"密码管理器报告状态: {enabled}") + + if enabled: + print("⚠️ 密码管理器认为密码保护已启用") + return True + else: + print("✅ 密码管理器认为密码保护未启用") + return False + + except Exception as e: + print(f"❌ 测试密码管理器时出错: {e}") + return False + +def main(): + """主函数""" + print("URLNav 密码保护诊断工具") + print("=" * 50) + + # 1. 检查所有设置 + problem_settings, org, app = check_all_settings() + + # 2. 测试密码管理器 + pm_problem = test_password_manager() + + # 3. 提供解决方案 + if problem_settings or pm_problem: + print("\n🔧 发现问题,提供解决方案:") + print("1. 清除所有密码设置") + print("2. 退出") + + choice = input("\n请选择 (1/2): ").strip() + + if choice == '1': + clear_all_password_settings() + print("\n✅ 修复完成!现在应该可以正常启动应用程序了") + else: + print("操作已取消") + else: + print("\n✅ 未发现密码保护问题") + print("如果仍然遇到密码提示,可能是其他原因") + + print("\n程序结束") + +if __name__ == "__main__": + # 创建临时应用程序实例 + app = QApplication(sys.argv) if not QApplication.instance() else QApplication.instance() + + try: + main() + finally: + if app: + app.quit() \ No newline at end of file diff --git a/reset_password_protection.py b/reset_password_protection.py new file mode 100644 index 0000000..8589d19 --- /dev/null +++ b/reset_password_protection.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +密码保护重置工具 +用于解决密码保护设置问题 +""" + +import sys +import os +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import QApplication, QMessageBox + +def check_password_status(): + """检查当前密码保护状态""" + settings = QSettings("URL Navigator", "URL Navigator") + + print("=== 密码保护状态检查 ===") + + # 检查各项设置 + password_enabled = settings.value("security/password_enabled", False) + password_hash = settings.value("security/password_hash", "") + password_salt = settings.value("security/password_salt", "") + security_question = settings.value("security/security_question", "") + + print(f"密码保护启用状态: {password_enabled}") + print(f"密码哈希存在: {'是' if password_hash else '否'}") + print(f"密码盐值存在: {'是' if password_salt else '否'}") + print(f"密保问题: {security_question if security_question else '未设置'}") + + # 判断问题 + if password_enabled and (not password_hash or not password_salt): + print("\n⚠️ 发现问题: 密码保护已启用但密码数据不完整!") + return True + elif password_enabled: + print("\n✅ 密码保护正常启用") + return False + else: + print("\n✅ 密码保护未启用") + return False + +def reset_password_protection(): + """重置密码保护设置""" + settings = QSettings("URL Navigator", "URL Navigator") + + print("\n=== 重置密码保护设置 ===") + + # 清除所有密码相关设置 + settings.remove("security/password_enabled") + settings.remove("security/password_hash") + settings.remove("security/password_salt") + settings.remove("security/security_question") + settings.remove("security/security_answer_hash") + settings.remove("security/security_answer_salt") + settings.sync() + + print("✅ 密码保护设置已重置") + print("现在可以正常启动应用程序了") + +def main(): + """主函数""" + print("URLNav 密码保护重置工具") + print("=" * 40) + + # 检查状态 + has_problem = check_password_status() + + if has_problem: + print("\n检测到密码保护配置问题") + choice = input("是否要重置密码保护设置?(y/n): ").lower().strip() + + if choice in ['y', 'yes', '是']: + reset_password_protection() + else: + print("操作已取消") + else: + print("\n如果您仍然遇到密码问题,可以选择重置") + choice = input("是否要强制重置密码保护设置?(y/n): ").lower().strip() + + if choice in ['y', 'yes', '是']: + reset_password_protection() + else: + print("操作已取消") + + print("\n程序结束") + input("按回车键退出...") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/simple_password_test.py b/tests/simple_password_test.py new file mode 100644 index 0000000..f1b6ca2 --- /dev/null +++ b/tests/simple_password_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +简单的密码功能测试 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +try: + from utils.password_manager import PasswordManager + print("✓ 成功导入 PasswordManager") +except Exception as e: + print(f"❌ 导入 PasswordManager 失败: {e}") + sys.exit(1) + +def test_basic_functionality(): + """测试基本功能""" + print("\n开始测试密码管理器基本功能...") + + try: + # 创建密码管理器实例 + pm = PasswordManager() + print("✓ 创建密码管理器实例成功") + + # 清除之前的数据 + pm.clear_password() + print("✓ 清除旧数据成功") + + # 检查初始状态 + is_enabled = pm.is_password_enabled() + print(f"✓ 初始密码保护状态: {'已启用' if is_enabled else '未启用'}") + + # 设置密码 + test_password = "test123" + result = pm.set_password(test_password) + print(f"✓ 设置密码结果: {'成功' if result else '失败'}") + + # 检查密码保护是否启用 + is_enabled_after = pm.is_password_enabled() + print(f"✓ 设置密码后状态: {'已启用' if is_enabled_after else '未启用'}") + + # 验证密码 + verify_result = pm.verify_password(test_password) + print(f"✓ 验证正确密码: {'成功' if verify_result else '失败'}") + + # 验证错误密码 + wrong_verify = pm.verify_password("wrong") + print(f"✓ 验证错误密码: {'失败' if not wrong_verify else '成功(这是错误的)'}") + + # 设置密保问题 + question = "您的出生地?" + answer = "北京" + security_result = pm.set_security_question(question, answer) + print(f"✓ 设置密保问题: {'成功' if security_result else '失败'}") + + # 检查密保问题 + has_question = pm.has_security_question() + print(f"✓ 检查密保问题存在: {'是' if has_question else '否'}") + + if has_question: + stored_question = pm.get_security_question() + print(f"✓ 获取密保问题: {stored_question}") + + # 验证密保答案 + answer_verify = pm.verify_security_answer(answer) + print(f"✓ 验证正确密保答案: {'成功' if answer_verify else '失败'}") + + wrong_answer_verify = pm.verify_security_answer("错误答案") + print(f"✓ 验证错误密保答案: {'失败' if not wrong_answer_verify else '成功(这是错误的)'}") + + print("\n✅ 基本功能测试完成!") + return True + + except Exception as e: + print(f"\n❌ 测试过程中发生错误: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + print("密码保护功能简单测试") + print("=" * 40) + + success = test_basic_functionality() + + if success: + print("\n🎉 测试成功!密码保护功能基本正常。") + else: + print("\n❌ 测试失败!请检查问题。") + + return success + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/tests/test_password_feature.py b/tests/test_password_feature.py new file mode 100644 index 0000000..b81281d --- /dev/null +++ b/tests/test_password_feature.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +密码保护功能测试脚本 + +此脚本用于测试密码保护功能的各个组件: +1. 密码管理器基本功能 +2. 密码对话框界面 +3. 设置对话框中的密码选项卡 +""" + +import sys +import os +import logging +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QLabel +from PyQt5.QtCore import Qt + +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from utils.password_manager import PasswordManager +from ui.password_dialogs import PasswordInputDialog, PasswordSettingsWidget +from utils.language_manager import language_manager + +# 设置日志 +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class PasswordTestWindow(QMainWindow): + """密码功能测试窗口""" + + def __init__(self): + super().__init__() + self.password_manager = PasswordManager() + self.init_ui() + + def init_ui(self): + """初始化UI""" + self.setWindowTitle("密码保护功能测试") + self.setGeometry(100, 100, 600, 400) + + # 中央窗口 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 布局 + layout = QVBoxLayout(central_widget) + + # 标题 + title_label = QLabel("密码保护功能测试") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("font-size: 18px; font-weight: bold; margin: 10px;") + layout.addWidget(title_label) + + # 状态显示 + self.status_label = QLabel() + self.update_status() + layout.addWidget(self.status_label) + + # 测试按钮 + self.test_password_manager_btn = QPushButton("测试密码管理器") + self.test_password_manager_btn.clicked.connect(self.test_password_manager) + layout.addWidget(self.test_password_manager_btn) + + self.test_password_input_btn = QPushButton("测试密码输入对话框") + self.test_password_input_btn.clicked.connect(self.test_password_input_dialog) + layout.addWidget(self.test_password_input_btn) + + self.test_password_settings_btn = QPushButton("测试密码设置界面") + self.test_password_settings_btn.clicked.connect(self.test_password_settings) + layout.addWidget(self.test_password_settings_btn) + + self.clear_password_btn = QPushButton("清除所有密码数据") + self.clear_password_btn.clicked.connect(self.clear_password_data) + layout.addWidget(self.clear_password_btn) + + self.refresh_btn = QPushButton("刷新状态") + self.refresh_btn.clicked.connect(self.update_status) + layout.addWidget(self.refresh_btn) + + def update_status(self): + """更新状态显示""" + try: + is_enabled = self.password_manager.is_password_enabled() + has_question = self.password_manager.has_security_question() + question = self.password_manager.get_security_question() + + status_text = f"""当前状态: +密码保护:{'已启用' if is_enabled else '未启用'} +密保问题:{'已设置' if has_question else '未设置'} +密保问题内容:{question if question else '无'}""" + + self.status_label.setText(status_text) + self.status_label.setStyleSheet("background-color: #f0f0f0; padding: 10px; border: 1px solid #ccc;") + + except Exception as e: + self.status_label.setText(f"获取状态失败: {e}") + logger.error(f"更新状态失败: {e}") + + def test_password_manager(self): + """测试密码管理器基本功能""" + try: + logger.info("开始测试密码管理器") + + # 测试设置密码 + test_password = "test123" + if self.password_manager.set_password(test_password): + logger.info("密码设置成功") + + # 测试验证密码 + if self.password_manager.verify_password(test_password): + logger.info("密码验证成功") + else: + logger.error("密码验证失败") + + # 测试错误密码 + if not self.password_manager.verify_password("wrong_password"): + logger.info("错误密码验证正确拒绝") + else: + logger.error("错误密码验证应该失败但成功了") + + # 测试密保问题 + test_question = "您的出生地是?" + test_answer = "北京" + if self.password_manager.set_security_question(test_question, test_answer): + logger.info("密保问题设置成功") + + # 验证密保答案 + if self.password_manager.verify_security_answer(test_answer): + logger.info("密保答案验证成功") + else: + logger.error("密保答案验证失败") + + # 测试通过密保重置密码 + new_password = "new123" + if self.password_manager.reset_password_with_security(new_password, test_answer): + logger.info("通过密保重置密码成功") + + # 验证新密码 + if self.password_manager.verify_password(new_password): + logger.info("新密码验证成功") + else: + logger.error("新密码验证失败") + else: + logger.error("通过密保重置密码失败") + else: + logger.error("密保问题设置失败") + else: + logger.error("密码设置失败") + + self.update_status() + logger.info("密码管理器测试完成") + + except Exception as e: + logger.error(f"测试密码管理器时发生错误: {e}") + + def test_password_input_dialog(self): + """测试密码输入对话框""" + try: + logger.info("测试密码输入对话框") + + dialog = PasswordInputDialog( + parent=self, + language_manager=language_manager, + password_manager=self.password_manager + ) + + result = dialog.exec_() + if result: + password = dialog.get_password() + logger.info(f"用户输入密码: {password}") + else: + logger.info("用户取消了密码输入") + + except Exception as e: + logger.error(f"测试密码输入对话框时发生错误: {e}") + + def test_password_settings(self): + """测试密码设置界面""" + try: + logger.info("测试密码设置界面") + + from PyQt5.QtWidgets import QDialog, QVBoxLayout + + dialog = QDialog(self) + dialog.setWindowTitle("密码设置测试") + dialog.setModal(True) + dialog.resize(500, 600) + + layout = QVBoxLayout(dialog) + + password_widget = PasswordSettingsWidget( + parent=dialog, + language_manager=language_manager, + password_manager=self.password_manager + ) + + layout.addWidget(password_widget) + + dialog.exec_() + self.update_status() + + except Exception as e: + logger.error(f"测试密码设置界面时发生错误: {e}") + + def clear_password_data(self): + """清除所有密码数据""" + try: + from PyQt5.QtWidgets import QMessageBox + + reply = QMessageBox.question( + self, + "确认", + "确定要清除所有密码数据吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + if self.password_manager.clear_password(): + logger.info("密码数据清除成功") + QMessageBox.information(self, "成功", "密码数据已清除") + else: + logger.error("密码数据清除失败") + QMessageBox.warning(self, "错误", "密码数据清除失败") + + self.update_status() + + except Exception as e: + logger.error(f"清除密码数据时发生错误: {e}") + +def main(): + """主函数""" + try: + # 创建应用 + app = QApplication(sys.argv) + app.setApplicationName("密码功能测试") + + # 创建测试窗口 + window = PasswordTestWindow() + window.show() + + # 运行应用 + sys.exit(app.exec_()) + + except Exception as e: + logger.error(f"程序运行时发生错误: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_password_integration.py b/tests/test_password_integration.py new file mode 100644 index 0000000..2d91b99 --- /dev/null +++ b/tests/test_password_integration.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +密码保护功能集成测试 + +此脚本用于测试密码保护功能的完整集成: +1. 测试密码管理器的基本功能 +2. 测试设置对话框中的密码选项卡 +3. 测试主应用程序的密码验证流程 +""" + +import sys +import os +import logging +from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtCore import QSettings + +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from utils.password_manager import PasswordManager +from ui.dialogs import SettingsDialog +from utils.language_manager import language_manager + +# 设置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +def test_password_manager(): + """测试密码管理器基本功能""" + print("\n=== 测试密码管理器基本功能 ===") + + try: + pm = PasswordManager() + + # 清除之前的数据 + pm.clear_password() + print("✓ 清除旧密码数据") + + # 测试初始状态 + assert not pm.is_password_enabled(), "初始状态应该未启用密码保护" + print("✓ 初始状态检查通过") + + # 设置密码 + test_password = "test123456" + assert pm.set_password(test_password), "设置密码应该成功" + print("✓ 密码设置成功") + + # 验证密码保护已启用 + assert pm.is_password_enabled(), "设置密码后应该启用密码保护" + print("✓ 密码保护已启用") + + # 验证正确密码 + assert pm.verify_password(test_password), "正确密码验证应该成功" + print("✓ 正确密码验证成功") + + # 验证错误密码 + assert not pm.verify_password("wrong_password"), "错误密码验证应该失败" + print("✓ 错误密码验证正确拒绝") + + # 设置密保问题 + question = "您的出生地是?" + answer = "北京" + assert pm.set_security_question(question, answer), "设置密保问题应该成功" + print("✓ 密保问题设置成功") + + # 验证密保问题 + assert pm.has_security_question(), "应该有密保问题" + assert pm.get_security_question() == question, "密保问题内容应该匹配" + print("✓ 密保问题验证成功") + + # 验证密保答案 + assert pm.verify_security_answer(answer), "正确密保答案验证应该成功" + assert not pm.verify_security_answer("错误答案"), "错误密保答案验证应该失败" + print("✓ 密保答案验证成功") + + # 通过密保重置密码 + new_password = "new123456" + assert pm.reset_password_with_security(new_password, answer), "通过密保重置密码应该成功" + print("✓ 通过密保重置密码成功") + + # 验证新密码 + assert pm.verify_password(new_password), "新密码验证应该成功" + assert not pm.verify_password(test_password), "旧密码验证应该失败" + print("✓ 新密码验证成功") + + print("\n✅ 密码管理器所有测试通过!") + return True + + except Exception as e: + print(f"\n❌ 密码管理器测试失败: {e}") + import traceback + traceback.print_exc() + return False + +def test_settings_dialog(): + """测试设置对话框中的密码选项卡""" + print("\n=== 测试设置对话框密码选项卡 ===") + + try: + app = QApplication.instance() + if not app: + app = QApplication(sys.argv) + + # 创建设置对话框 + dialog = SettingsDialog() + + # 检查是否有密码选项卡 + tab_widget = None + for child in dialog.findChildren(object): + if hasattr(child, 'tabText'): + tab_widget = child + break + + if tab_widget: + tab_count = tab_widget.count() + tab_names = [tab_widget.tabText(i) for i in range(tab_count)] + print(f"✓ 找到选项卡: {tab_names}") + + # 检查是否有密码相关的选项卡 + password_tab_found = any('密码' in name or 'Password' in name for name in tab_names) + if password_tab_found: + print("✓ 找到密码设置选项卡") + else: + print("⚠ 未找到密码设置选项卡") + else: + print("⚠ 未找到选项卡控件") + + dialog.close() + print("\n✅ 设置对话框测试完成!") + return True + + except Exception as e: + print(f"\n❌ 设置对话框测试失败: {e}") + import traceback + traceback.print_exc() + return False + +def test_password_storage(): + """测试密码存储和持久化""" + print("\n=== 测试密码存储和持久化 ===") + + try: + # 创建第一个密码管理器实例 + pm1 = PasswordManager() + pm1.clear_password() + + # 设置密码和密保 + test_password = "persistent_test_123" + question = "测试问题?" + answer = "测试答案" + + pm1.set_password(test_password) + pm1.set_security_question(question, answer) + print("✓ 第一个实例设置密码和密保") + + # 创建第二个密码管理器实例(模拟重启应用) + pm2 = PasswordManager() + + # 验证数据持久化 + assert pm2.is_password_enabled(), "密码保护状态应该持久化" + assert pm2.verify_password(test_password), "密码应该持久化" + assert pm2.has_security_question(), "密保问题状态应该持久化" + assert pm2.get_security_question() == question, "密保问题内容应该持久化" + assert pm2.verify_security_answer(answer), "密保答案应该持久化" + + print("✓ 密码数据持久化验证成功") + + # 清理测试数据 + pm2.clear_password() + print("✓ 清理测试数据") + + print("\n✅ 密码存储和持久化测试通过!") + return True + + except Exception as e: + print(f"\n❌ 密码存储测试失败: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """主测试函数""" + print("开始密码保护功能集成测试...") + print("=" * 50) + + # 创建应用程序实例 + app = QApplication.instance() + if not app: + app = QApplication(sys.argv) + + # 运行所有测试 + tests = [ + ("密码管理器基本功能", test_password_manager), + ("密码存储和持久化", test_password_storage), + ("设置对话框密码选项卡", test_settings_dialog), + ] + + passed = 0 + total = len(tests) + + for test_name, test_func in tests: + print(f"\n正在运行测试: {test_name}") + if test_func(): + passed += 1 + else: + print(f"测试失败: {test_name}") + + print("\n" + "=" * 50) + print(f"测试结果: {passed}/{total} 通过") + + if passed == total: + print("🎉 所有测试通过!密码保护功能集成成功!") + + # 显示功能说明 + print("\n📋 密码保护功能说明:") + print("1. 在设置对话框中可以启用/禁用密码保护") + print("2. 可以设置密保问题用于密码重置") + print("3. 启用密码保护后,每次启动应用都需要输入密码") + print("4. 忘记密码时可以通过密保问题重置") + print("5. 所有密码数据都经过加密存储") + + else: + print("❌ 部分测试失败,请检查问题") + + return passed == total + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/ui/__pycache__/dialogs.cpython-313.pyc b/ui/__pycache__/dialogs.cpython-313.pyc index cdb81964f79a29941889d81ea60a1b28d41661fa..88f0c2efe296e331d8f60312830929bd75bf104a 100644 GIT binary patch delta 16135 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 MPt123{GX`*FYYJ9FaQ7m delta 14086 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⁣VUNh3#5{`0L+i4)q9B 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 diff --git a/ui/__pycache__/password_dialogs.cpython-313.pyc b/ui/__pycache__/password_dialogs.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94b8835971aca42c74423e5be69e0db1550fec01 GIT binary patch literal 28624 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