From 55df7a3c56c17b762c83348bc4bcde918cf158ce Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:36:05 -0400 Subject: [PATCH] Use notify for warnings --- src/password_manager/manager.py | 105 ++++++++---------- src/tests/test_archive_from_retrieve.py | 3 + src/tests/test_archive_restore.py | 9 +- src/tests/test_manager_checksum_backup.py | 7 +- .../test_manager_warning_notifications.py | 45 ++++++++ src/tests/test_parent_seed_backup.py | 2 + 6 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 src/tests/test_manager_warning_notifications.py diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 771ea12..846b83f 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -723,7 +723,7 @@ class PasswordManager: Handles the setup process when no existing parent seed is found. Asks the user whether to enter an existing BIP-85 seed or generate a new one. """ - print(colored("No existing seed found. Let's set up a new one!", "yellow")) + self.notify("No existing seed found. Let's set up a new one!", level="WARNING") choice = input( "Do you want to (1) Enter an existing BIP-85 seed or (2) Generate a new BIP-85 seed? (1/2): " @@ -827,7 +827,7 @@ class PasswordManager: sys.exit(1) except KeyboardInterrupt: logging.info("Operation cancelled by user.") - print(colored("\nOperation cancelled by user.", "yellow")) + self.notify("Operation cancelled by user.", level="WARNING") sys.exit(0) def generate_new_seed(self) -> Optional[str]: @@ -879,7 +879,7 @@ class PasswordManager: return fingerprint # Return the generated fingerprint else: - print(colored("Seed generation cancelled. Exiting.", "yellow")) + self.notify("Seed generation cancelled. Exiting.", level="WARNING") sys.exit(0) def validate_bip85_seed(self, seed: str) -> bool: @@ -1434,7 +1434,7 @@ class PasswordManager: if not confirm_action( "WARNING: Displaying SSH keys reveals sensitive information. Continue? (Y/N): " ): - print(colored("SSH key display cancelled.", "yellow")) + self.notify("SSH key display cancelled.", level="WARNING") return print(colored(f"\n[+] SSH key entry added with ID {index}.\n", "green")) @@ -1493,7 +1493,7 @@ class PasswordManager: if not confirm_action( "WARNING: Displaying the seed phrase reveals sensitive information. Continue? (Y/N): " ): - print(colored("Seed phrase display cancelled.", "yellow")) + self.notify("Seed phrase display cancelled.", level="WARNING") return print( @@ -1568,7 +1568,7 @@ class PasswordManager: if not confirm_action( "WARNING: Displaying the PGP key reveals sensitive information. Continue? (Y/N): " ): - print(colored("PGP key display cancelled.", "yellow")) + self.notify("PGP key display cancelled.", level="WARNING") return print(colored(f"\n[+] PGP key entry added with ID {index}.\n", "green")) @@ -2003,7 +2003,7 @@ class PasswordManager: entry = self.entry_manager.retrieve_entry(index) or entry return - print(colored("No QR codes available for this entry.", "yellow")) + self.notify("No QR codes available for this entry.", level="WARNING") except Exception as e: # pragma: no cover - best effort logging.error(f"Error displaying QR menu: {e}", exc_info=True) print(colored(f"Error: Failed to display QR codes: {e}", "red")) @@ -2103,7 +2103,7 @@ class PasswordManager: if not confirm_action( "WARNING: Displaying SSH keys reveals sensitive information. Continue? (Y/N): " ): - print(colored("SSH key display cancelled.", "yellow")) + self.notify("SSH key display cancelled.", level="WARNING") return try: priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair( @@ -2142,7 +2142,7 @@ class PasswordManager: if not confirm_action( "WARNING: Displaying the seed phrase reveals sensitive information. Continue? (Y/N): " ): - print(colored("Seed phrase display cancelled.", "yellow")) + self.notify("Seed phrase display cancelled.", level="WARNING") return try: phrase = self.entry_manager.get_seed_phrase(index, self.parent_seed) @@ -2193,7 +2193,7 @@ class PasswordManager: if not confirm_action( "WARNING: Displaying the PGP key reveals sensitive information. Continue? (Y/N): " ): - print(colored("PGP key display cancelled.", "yellow")) + self.notify("PGP key display cancelled.", level="WARNING") return try: priv_key, fingerprint = self.entry_manager.get_pgp_key( @@ -2385,11 +2385,9 @@ class PasswordManager: if url: print(colored(f"URL: {url}", "cyan")) if blacklisted: - print( - colored( - f"Warning: This password is archived and should not be used.", - "yellow", - ) + self.notify( + "Warning: This password is archived and should not be used.", + level="WARNING", ) password = self.password_generator.generate_password(length, index) @@ -2522,8 +2520,9 @@ class PasswordManager: if period_input.isdigit(): new_period = int(period_input) else: - print( - colored("Invalid period value. Keeping current.", "yellow") + self.notify( + "Invalid period value. Keeping current.", + level="WARNING", ) digits_input = input( f"Enter new digit count (current: {digits}): " @@ -2533,11 +2532,9 @@ class PasswordManager: if digits_input.isdigit(): new_digits = int(digits_input) else: - print( - colored( - "Invalid digits value. Keeping current.", - "yellow", - ) + self.notify( + "Invalid digits value. Keeping current.", + level="WARNING", ) blacklist_input = ( input( @@ -2553,11 +2550,9 @@ class PasswordManager: elif blacklist_input == "n": new_blacklisted = False else: - print( - colored( - "Invalid input for archived status. Keeping the current status.", - "yellow", - ) + self.notify( + "Invalid input for archived status. Keeping the current status.", + level="WARNING", ) new_blacklisted = blacklisted @@ -2644,11 +2639,9 @@ class PasswordManager: elif blacklist_input == "n": new_blacklisted = False else: - print( - colored( - "Invalid input for archived status. Keeping the current status.", - "yellow", - ) + self.notify( + "Invalid input for archived status. Keeping the current status.", + level="WARNING", ) new_blacklisted = blacklisted @@ -2749,11 +2742,9 @@ class PasswordManager: elif blacklist_input == "n": new_blacklisted = False else: - print( - colored( - "Invalid input for archived status. Keeping the current status.", - "yellow", - ) + self.notify( + "Invalid input for archived status. Keeping the current status.", + level="WARNING", ) new_blacklisted = blacklisted @@ -2837,13 +2828,13 @@ class PasswordManager: ) query = input("Enter search string: ").strip() if not query: - print(colored("No search string provided.", "yellow")) + self.notify("No search string provided.", level="WARNING") pause() return results = self.entry_manager.search_entries(query) if not results: - print(colored("No matching entries found.", "yellow")) + self.notify("No matching entries found.", level="WARNING") pause() return @@ -3068,7 +3059,7 @@ class PasswordManager: if not confirm_action( f"Are you sure you want to delete entry {index_to_delete}? (Y/N): " ): - print(colored("Deletion cancelled.", "yellow")) + self.notify("Deletion cancelled.", level="WARNING") return self.entry_manager.delete_entry(index_to_delete) @@ -3115,7 +3106,7 @@ class PasswordManager: archived = self.entry_manager.list_entries(include_archived=True) archived = [e for e in archived if e[4]] if not archived: - print(colored("No archived entries found.", "yellow")) + self.notify("No archived entries found.", level="WARNING") pause() return while True: @@ -3196,7 +3187,7 @@ class PasswordManager: totp_list.append((label, int(idx_str), period, imported)) if not totp_list: - print(colored("No 2FA entries found.", "yellow")) + self.notify("No 2FA entries found.", level="WARNING") return totp_list.sort(key=lambda t: t[0].lower()) @@ -3274,11 +3265,9 @@ class PasswordManager: try: verified = verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE) except FileNotFoundError: - print( - colored( - "Checksum file missing. Run scripts/update_checksum.py or choose 'Generate Script Checksum' in Settings.", - "yellow", - ) + self.notify( + "Checksum file missing. Run scripts/update_checksum.py or choose 'Generate Script Checksum' in Settings.", + level="WARNING", ) logging.warning("Checksum file missing during verification.") return @@ -3301,7 +3290,7 @@ class PasswordManager: def handle_update_script_checksum(self) -> None: """Generate a new checksum for the manager script.""" if not confirm_action("Generate new script checksum? (Y/N): "): - print(colored("Operation cancelled.", "yellow")) + self.notify("Operation cancelled.", level="WARNING") return try: fp, parent_fp, child_fp = self.header_fingerprint_args @@ -3500,7 +3489,7 @@ class PasswordManager: ) if not totp_entries: - print(colored("No 2FA codes to export.", "yellow")) + self.notify("No 2FA codes to export.", level="WARNING") return None dest_str = input( @@ -3548,17 +3537,13 @@ class PasswordManager: child_fingerprint=child_fp, ) print(colored("\n=== Backup Parent Seed ===", "yellow")) - print( - colored( - "Warning: Revealing your parent seed is a highly sensitive operation.", - "yellow", - ) + self.notify( + "Warning: Revealing your parent seed is a highly sensitive operation.", + level="WARNING", ) - print( - colored( - "Ensure you're in a secure, private environment and no one is watching your screen.", - "yellow", - ) + self.notify( + "Ensure you're in a secure, private environment and no one is watching your screen.", + level="WARNING", ) # Verify user's identity with secure password verification @@ -3573,7 +3558,7 @@ class PasswordManager: if not confirm_action( "Are you absolutely sure you want to reveal your parent seed? (Y/N): " ): - print(colored("Operation cancelled by user.", "yellow")) + self.notify("Operation cancelled by user.", level="WARNING") return # Reveal the parent seed diff --git a/src/tests/test_archive_from_retrieve.py b/src/tests/test_archive_from_retrieve.py index 779d3f3..bc094da 100644 --- a/src/tests/test_archive_from_retrieve.py +++ b/src/tests/test_archive_from_retrieve.py @@ -2,6 +2,7 @@ import sys from pathlib import Path from tempfile import TemporaryDirectory from types import SimpleNamespace +import queue from helpers import create_vault, TEST_SEED, TEST_PASSWORD @@ -37,6 +38,7 @@ def test_archive_entry_from_retrieve(monkeypatch): pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path pm.secret_mode_enabled = False + pm.notifications = queue.Queue() index = entry_mgr.add_entry("example.com", 8) @@ -68,6 +70,7 @@ def test_restore_entry_from_retrieve(monkeypatch): pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path pm.secret_mode_enabled = False + pm.notifications = queue.Queue() index = entry_mgr.add_entry("example.com", 8) entry_mgr.archive_entry(index) diff --git a/src/tests/test_archive_restore.py b/src/tests/test_archive_restore.py index 00225e1..b182866 100644 --- a/src/tests/test_archive_restore.py +++ b/src/tests/test_archive_restore.py @@ -2,6 +2,7 @@ import sys from pathlib import Path from tempfile import TemporaryDirectory from types import SimpleNamespace +import queue import pytest @@ -67,6 +68,7 @@ def test_view_archived_entries_cli(monkeypatch): pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path pm.is_dirty = False + pm.notifications = queue.Queue() idx = entry_mgr.add_entry("example.com", 8) @@ -98,6 +100,7 @@ def test_view_archived_entries_view_only(monkeypatch, capsys): pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path pm.is_dirty = False + pm.notifications = queue.Queue() idx = entry_mgr.add_entry("example.com", 8) @@ -131,6 +134,7 @@ def test_view_archived_entries_removed_after_restore(monkeypatch, capsys): pm.nostr_client = SimpleNamespace() pm.fingerprint_dir = tmp_path pm.is_dirty = False + pm.notifications = queue.Queue() idx = entry_mgr.add_entry("example.com", 8) @@ -145,5 +149,6 @@ def test_view_archived_entries_removed_after_restore(monkeypatch, capsys): monkeypatch.setattr("builtins.input", lambda *_: "") pm.handle_view_archived_entries() - out = capsys.readouterr().out - assert "No archived entries found." in out + note = pm.notifications.get_nowait() + assert note.level == "WARNING" + assert note.message == "No archived entries found." diff --git a/src/tests/test_manager_checksum_backup.py b/src/tests/test_manager_checksum_backup.py index 4a74ba0..cfba90c 100644 --- a/src/tests/test_manager_checksum_backup.py +++ b/src/tests/test_manager_checksum_backup.py @@ -4,6 +4,7 @@ from pathlib import Path sys.path.append(str(Path(__file__).resolve().parents[1])) from password_manager.manager import PasswordManager, EncryptionMode +import queue class FakeBackupManager: @@ -20,6 +21,7 @@ class FakeBackupManager: def _make_pm(): pm = PasswordManager.__new__(PasswordManager) pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.notifications = queue.Queue() return pm @@ -56,8 +58,9 @@ def test_handle_verify_checksum_missing(monkeypatch, tmp_path, capsys): monkeypatch.setattr("password_manager.manager.verify_checksum", raise_missing) pm.handle_verify_checksum() - out = capsys.readouterr().out.lower() - assert "generate script checksum" in out + note = pm.notifications.get_nowait() + assert note.level == "WARNING" + assert "generate script checksum" in note.message.lower() def test_backup_and_restore_database(monkeypatch, capsys): diff --git a/src/tests/test_manager_warning_notifications.py b/src/tests/test_manager_warning_notifications.py new file mode 100644 index 0000000..55ad85c --- /dev/null +++ b/src/tests/test_manager_warning_notifications.py @@ -0,0 +1,45 @@ +import queue +from types import SimpleNamespace +from pathlib import Path +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from password_manager.manager import PasswordManager, EncryptionMode +from password_manager.entry_management import EntryManager +from password_manager.backup import BackupManager +from helpers import create_vault, TEST_SEED, TEST_PASSWORD +from password_manager.config_manager import ConfigManager + + +def _make_pm(tmp_path: Path) -> PasswordManager: + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = vault + pm.entry_manager = entry_mgr + pm.backup_manager = backup_mgr + pm.parent_seed = TEST_SEED + pm.nostr_client = SimpleNamespace() + pm.fingerprint_dir = tmp_path + pm.notifications = queue.Queue() + return pm + + +def test_handle_search_entries_no_query(monkeypatch, tmp_path): + pm = _make_pm(tmp_path) + monkeypatch.setattr( + "password_manager.manager.clear_header_with_notification", lambda *a, **k: None + ) + monkeypatch.setattr("password_manager.manager.pause", lambda: None) + monkeypatch.setattr("builtins.input", lambda *_: "") + + pm.handle_search_entries() + note = pm.notifications.get_nowait() + assert note.level == "WARNING" + assert note.message == "No search string provided." diff --git a/src/tests/test_parent_seed_backup.py b/src/tests/test_parent_seed_backup.py index 728f8b0..ff379a6 100644 --- a/src/tests/test_parent_seed_backup.py +++ b/src/tests/test_parent_seed_backup.py @@ -2,6 +2,7 @@ import builtins import sys from pathlib import Path from types import SimpleNamespace +import queue sys.path.append(str(Path(__file__).resolve().parents[1])) @@ -16,6 +17,7 @@ def _make_pm(tmp_path: Path) -> PasswordManager: pm.fingerprint_dir = tmp_path pm.encryption_manager = SimpleNamespace(encrypt_and_save_file=lambda *a, **k: None) pm.verify_password = lambda pw: True + pm.notifications = queue.Queue() return pm