From 89574ef249f840e2bca38815415c2c13b4a01881 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:56:28 -0400 Subject: [PATCH] Add search option to menu --- src/main.py | 20 +++++++++------ src/password_manager/manager.py | 29 ++++++++++++++++++++++ src/tests/test_auto_sync.py | 2 +- src/tests/test_cli_invalid_input.py | 8 +++--- src/tests/test_inactivity_lock.py | 4 +-- src/tests/test_menu_search.py | 38 +++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 src/tests/test_menu_search.py diff --git a/src/main.py b/src/main.py index b6bdd48..597e2c6 100644 --- a/src/main.py +++ b/src/main.py @@ -615,10 +615,11 @@ def display_menu( Select an option: 1. Add Entry 2. Retrieve Entry - 3. Modify an Existing Entry - 4. 2FA Codes - 5. Settings - 6. Exit + 3. Search Entries + 4. Modify an Existing Entry + 5. 2FA Codes + 6. Settings + 7. Exit """ display_fn = getattr(password_manager, "display_stats", None) if callable(display_fn): @@ -643,7 +644,7 @@ def display_menu( print(colored(menu, "cyan")) try: choice = timed_input( - "Enter your choice (1-6): ", inactivity_timeout + "Enter your choice (1-7): ", inactivity_timeout ).strip() except TimeoutError: print(colored("Session timed out. Vault locked.", "yellow")) @@ -654,7 +655,7 @@ def display_menu( if not choice: print( colored( - "No input detected. Please enter a number between 1 and 6.", + "No input detected. Please enter a number between 1 and 7.", "yellow", ) ) @@ -682,14 +683,17 @@ def display_menu( password_manager.handle_retrieve_entry() elif choice == "3": password_manager.update_activity() - password_manager.handle_modify_entry() + password_manager.handle_search_entries() elif choice == "4": password_manager.update_activity() - password_manager.handle_display_totp_codes() + password_manager.handle_modify_entry() elif choice == "5": password_manager.update_activity() handle_settings(password_manager) elif choice == "6": + password_manager.update_activity() + password_manager.handle_display_totp_codes() + elif choice == "7": logging.info("Exiting the program.") print(colored("Exiting the program.", "green")) password_manager.nostr_client.close_client_pool() diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 2d603f0..0f0274c 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -1303,6 +1303,35 @@ class PasswordManager: logging.error(f"Error during modifying entry: {e}", exc_info=True) print(colored(f"Error: Failed to modify entry: {e}", "red")) + def handle_search_entries(self) -> None: + """Prompt for a query and display matching entries.""" + try: + query = input("Enter search string: ").strip() + if not query: + print(colored("No search string provided.", "yellow")) + return + + results = self.entry_manager.search_entries(query) + if not results: + print(colored("No matching entries found.", "yellow")) + return + + print(colored("\n[+] Search Results:\n", "green")) + for entry in results: + index, website, username, url, blacklisted = entry + print(colored(f"Index: {index}", "cyan")) + print(colored(f" Website: {website}", "cyan")) + print(colored(f" Username: {username or 'N/A'}", "cyan")) + print(colored(f" URL: {url or 'N/A'}", "cyan")) + print( + colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan") + ) + print("-" * 40) + + except Exception as e: + logging.error(f"Failed to search entries: {e}", exc_info=True) + print(colored(f"Error: Failed to search entries: {e}", "red")) + def delete_entry(self) -> None: """Deletes an entry from the password index.""" try: diff --git a/src/tests/test_auto_sync.py b/src/tests/test_auto_sync.py index e968c8d..bdd9f28 100644 --- a/src/tests/test_auto_sync.py +++ b/src/tests/test_auto_sync.py @@ -31,7 +31,7 @@ def test_auto_sync_triggers_post(monkeypatch): called = True monkeypatch.setattr(main, "handle_post_to_nostr", fake_post) - monkeypatch.setattr(main, "timed_input", lambda *_: "6") + monkeypatch.setattr(main, "timed_input", lambda *_: "7") with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=0.1) diff --git a/src/tests/test_cli_invalid_input.py b/src/tests/test_cli_invalid_input.py index 50760a2..df64494 100644 --- a/src/tests/test_cli_invalid_input.py +++ b/src/tests/test_cli_invalid_input.py @@ -52,7 +52,7 @@ def _make_pm(called, locked=None): def test_empty_and_non_numeric_choice(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["", "abc", "6"]) + inputs = iter(["", "abc", "7"]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) @@ -65,7 +65,7 @@ def test_empty_and_non_numeric_choice(monkeypatch, capsys): def test_out_of_range_menu(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["9", "6"]) + inputs = iter(["9", "7"]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) @@ -77,7 +77,7 @@ def test_out_of_range_menu(monkeypatch, capsys): def test_invalid_add_entry_submenu(monkeypatch, capsys): called = {"add": False, "retrieve": False, "modify": False} pm, _ = _make_pm(called) - inputs = iter(["1", "4", "3", "6"]) + inputs = iter(["1", "4", "3", "7"]) monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) monkeypatch.setattr("builtins.input", lambda *_: next(inputs)) with pytest.raises(SystemExit): @@ -92,7 +92,7 @@ def test_inactivity_timeout_loop(monkeypatch, capsys): pm, locked = _make_pm(called) pm.last_activity = 0 monkeypatch.setattr(time, "time", lambda: 100.0) - monkeypatch.setattr(main, "timed_input", lambda *_: "6") + monkeypatch.setattr(main, "timed_input", lambda *_: "7") with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1) out = capsys.readouterr().out diff --git a/src/tests/test_inactivity_lock.py b/src/tests/test_inactivity_lock.py index 500bcd6..c8a4ed7 100644 --- a/src/tests/test_inactivity_lock.py +++ b/src/tests/test_inactivity_lock.py @@ -36,7 +36,7 @@ def test_inactivity_triggers_lock(monkeypatch): unlock_vault=unlock_vault, ) - monkeypatch.setattr(main, "timed_input", lambda *_: "6") + monkeypatch.setattr(main, "timed_input", lambda *_: "7") with pytest.raises(SystemExit): main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1) @@ -72,7 +72,7 @@ def test_input_timeout_triggers_lock(monkeypatch): unlock_vault=unlock_vault, ) - responses = iter([TimeoutError(), "6"]) + responses = iter([TimeoutError(), "7"]) def fake_input(*_args, **_kwargs): val = next(responses) diff --git a/src/tests/test_menu_search.py b/src/tests/test_menu_search.py new file mode 100644 index 0000000..65dc386 --- /dev/null +++ b/src/tests/test_menu_search.py @@ -0,0 +1,38 @@ +import time +from types import SimpleNamespace +from pathlib import Path +import sys +import pytest + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +import main + + +def _make_pm(called): + pm = SimpleNamespace( + is_dirty=False, + last_update=time.time(), + last_activity=time.time(), + nostr_client=SimpleNamespace(close_client_pool=lambda: None), + handle_add_password=lambda: None, + handle_add_totp=lambda: None, + handle_retrieve_entry=lambda: None, + handle_search_entries=lambda: called.append("search"), + handle_modify_entry=lambda: None, + update_activity=lambda: None, + lock_vault=lambda: None, + unlock_vault=lambda: None, + ) + return pm + + +def test_menu_search_option(monkeypatch): + called = [] + pm = _make_pm(called) + inputs = iter(["3", "7"]) + monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs)) + monkeypatch.setattr("builtins.input", lambda *_: "query") + with pytest.raises(SystemExit): + main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000) + assert called == ["search"]