mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add CLI subcommands and tests
This commit is contained in:
146
src/main.py
146
src/main.py
@@ -16,10 +16,12 @@ import traceback
|
||||
|
||||
from password_manager.manager import PasswordManager
|
||||
from nostr.client import NostrClient
|
||||
from password_manager.entry_types import EntryType
|
||||
from constants import INACTIVITY_TIMEOUT, initialize_app
|
||||
from utils.password_prompt import PasswordPromptError
|
||||
from utils import timed_input
|
||||
from local_bip85.bip85 import Bip85Error
|
||||
import pyperclip
|
||||
|
||||
|
||||
colorama_init()
|
||||
@@ -233,6 +235,22 @@ def handle_display_stats(password_manager: PasswordManager) -> None:
|
||||
print(colored(f"Error: Failed to display stats: {e}", "red"))
|
||||
|
||||
|
||||
def print_matches(matches: list[tuple[int, str, str | None, str | None, bool]]) -> None:
|
||||
"""Print a list of search matches."""
|
||||
print(colored("\n[+] Matches:\n", "green"))
|
||||
for entry in matches:
|
||||
idx, website, username, url, blacklisted = entry
|
||||
print(colored(f"Index: {idx}", "cyan"))
|
||||
if website:
|
||||
print(colored(f" Website: {website}", "cyan"))
|
||||
if username:
|
||||
print(colored(f" Username: {username}", "cyan"))
|
||||
if url:
|
||||
print(colored(f" URL: {url}", "cyan"))
|
||||
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"))
|
||||
print("-" * 40)
|
||||
|
||||
|
||||
def handle_post_to_nostr(
|
||||
password_manager: PasswordManager, alt_summary: str | None = None
|
||||
):
|
||||
@@ -702,15 +720,14 @@ def display_menu(
|
||||
print(colored("Invalid choice. Please select a valid option.", "red"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Configure logging with both file and console handlers
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""Entry point for the SeedPass CLI."""
|
||||
configure_logging()
|
||||
initialize_app()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("Starting SeedPass Password Manager")
|
||||
|
||||
# Load config from disk and parse command-line arguments
|
||||
cfg = load_global_config()
|
||||
load_global_config()
|
||||
parser = argparse.ArgumentParser()
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
|
||||
@@ -720,48 +737,96 @@ if __name__ == "__main__":
|
||||
imp = sub.add_parser("import")
|
||||
imp.add_argument("--file")
|
||||
|
||||
args = parser.parse_args()
|
||||
search_p = sub.add_parser("search")
|
||||
search_p.add_argument("query")
|
||||
|
||||
get_p = sub.add_parser("get")
|
||||
get_p.add_argument("query")
|
||||
|
||||
totp_p = sub.add_parser("totp")
|
||||
totp_p.add_argument("query")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Initialize PasswordManager and proceed with application logic
|
||||
try:
|
||||
password_manager = PasswordManager()
|
||||
logger.info("PasswordManager initialized successfully.")
|
||||
except (PasswordPromptError, Bip85Error) as e:
|
||||
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
|
||||
sys.exit(1)
|
||||
return 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
|
||||
sys.exit(1)
|
||||
return 1
|
||||
|
||||
if args.command == "export":
|
||||
password_manager.handle_export_database(Path(args.file))
|
||||
sys.exit(0)
|
||||
elif args.command == "import":
|
||||
return 0
|
||||
if args.command == "import":
|
||||
password_manager.handle_import_database(Path(args.file))
|
||||
sys.exit(0)
|
||||
return 0
|
||||
if args.command == "search":
|
||||
matches = password_manager.entry_manager.search_entries(args.query)
|
||||
if matches:
|
||||
print_matches(matches)
|
||||
else:
|
||||
print(colored("No matching entries found.", "yellow"))
|
||||
return 0
|
||||
if args.command == "get":
|
||||
matches = password_manager.entry_manager.search_entries(args.query)
|
||||
if len(matches) != 1:
|
||||
if not matches:
|
||||
print(colored("No matching entries found.", "yellow"))
|
||||
else:
|
||||
print_matches(matches)
|
||||
return 1
|
||||
idx = matches[0][0]
|
||||
entry = password_manager.entry_manager.retrieve_entry(idx)
|
||||
if entry.get("type", EntryType.PASSWORD.value) != EntryType.PASSWORD.value:
|
||||
print(colored("Entry is not a password entry.", "red"))
|
||||
return 1
|
||||
length = int(entry.get("length", 0))
|
||||
pw = password_manager.password_generator.generate_password(length, idx)
|
||||
print(pw)
|
||||
return 0
|
||||
if args.command == "totp":
|
||||
matches = password_manager.entry_manager.search_entries(args.query)
|
||||
if len(matches) != 1:
|
||||
if not matches:
|
||||
print(colored("No matching entries found.", "yellow"))
|
||||
else:
|
||||
print_matches(matches)
|
||||
return 1
|
||||
idx = matches[0][0]
|
||||
entry = password_manager.entry_manager.retrieve_entry(idx)
|
||||
if entry.get("type") != EntryType.TOTP.value:
|
||||
print(colored("Entry is not a TOTP entry.", "red"))
|
||||
return 1
|
||||
code = password_manager.entry_manager.get_totp_code(
|
||||
idx, password_manager.parent_seed
|
||||
)
|
||||
print(code)
|
||||
try:
|
||||
pyperclip.copy(code)
|
||||
except Exception as exc:
|
||||
logging.warning(f"Clipboard copy failed: {exc}")
|
||||
return 0
|
||||
|
||||
# Register signal handlers for graceful shutdown
|
||||
def signal_handler(sig, frame):
|
||||
"""
|
||||
Handles termination signals to gracefully shutdown the NostrClient.
|
||||
"""
|
||||
def signal_handler(sig, _frame):
|
||||
print(colored("\nReceived shutdown signal. Exiting gracefully...", "yellow"))
|
||||
logging.info(f"Received shutdown signal: {sig}. Initiating graceful shutdown.")
|
||||
try:
|
||||
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
|
||||
password_manager.nostr_client.close_client_pool()
|
||||
logging.info("NostrClient closed successfully.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error during shutdown: {e}")
|
||||
print(colored(f"Error during shutdown: {e}", "red"))
|
||||
except Exception as exc:
|
||||
logging.error(f"Error during shutdown: {exc}")
|
||||
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||
sys.exit(0)
|
||||
|
||||
# Register the signal handlers
|
||||
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
|
||||
signal.signal(signal.SIGTERM, signal_handler) # Handle termination signals
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Display the interactive menu to the user
|
||||
try:
|
||||
display_menu(
|
||||
password_manager, inactivity_timeout=password_manager.inactivity_timeout
|
||||
@@ -770,29 +835,34 @@ if __name__ == "__main__":
|
||||
logger.info("Program terminated by user via KeyboardInterrupt.")
|
||||
print(colored("\nProgram terminated by user.", "yellow"))
|
||||
try:
|
||||
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
|
||||
password_manager.nostr_client.close_client_pool()
|
||||
logging.info("NostrClient closed successfully.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error during shutdown: {e}")
|
||||
print(colored(f"Error during shutdown: {e}", "red"))
|
||||
sys.exit(0)
|
||||
except Exception as exc:
|
||||
logging.error(f"Error during shutdown: {exc}")
|
||||
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||
return 0
|
||||
except (PasswordPromptError, Bip85Error) as e:
|
||||
logger.error(f"A user-related error occurred: {e}", exc_info=True)
|
||||
print(colored(f"Error: {e}", "red"))
|
||||
try:
|
||||
password_manager.nostr_client.close_client_pool()
|
||||
logging.info("NostrClient closed successfully.")
|
||||
except Exception as close_error:
|
||||
logging.error(f"Error during shutdown: {close_error}")
|
||||
print(colored(f"Error during shutdown: {close_error}", "red"))
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
logging.error(f"Error during shutdown: {exc}")
|
||||
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||
return 1
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
||||
print(colored(f"Error: An unexpected error occurred: {e}", "red"))
|
||||
try:
|
||||
password_manager.nostr_client.close_client_pool() # Attempt to close the ClientPool
|
||||
password_manager.nostr_client.close_client_pool()
|
||||
logging.info("NostrClient closed successfully.")
|
||||
except Exception as close_error:
|
||||
logging.error(f"Error during shutdown: {close_error}")
|
||||
print(colored(f"Error during shutdown: {close_error}", "red"))
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
logging.error(f"Error during shutdown: {exc}")
|
||||
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
@@ -21,3 +21,4 @@ mutmut==2.4.4
|
||||
pyotp>=2.8.0
|
||||
|
||||
freezegun
|
||||
pyperclip
|
||||
|
66
src/tests/test_cli_subcommands.py
Normal file
66
src/tests/test_cli_subcommands.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
import main
|
||||
from password_manager.entry_types import EntryType
|
||||
|
||||
|
||||
def make_pm(search_results, entry=None, totp_code="123456"):
|
||||
entry_mgr = SimpleNamespace(
|
||||
search_entries=lambda q: search_results,
|
||||
retrieve_entry=lambda idx: entry,
|
||||
get_totp_code=lambda idx, seed: totp_code,
|
||||
)
|
||||
pg = SimpleNamespace(generate_password=lambda l, i: "pw")
|
||||
pm = SimpleNamespace(
|
||||
entry_manager=entry_mgr,
|
||||
password_generator=pg,
|
||||
nostr_client=SimpleNamespace(close_client_pool=lambda: None),
|
||||
parent_seed="seed",
|
||||
inactivity_timeout=1,
|
||||
)
|
||||
return pm
|
||||
|
||||
|
||||
def test_search_command(monkeypatch, capsys):
|
||||
pm = make_pm([(0, "Example", "user", "", False)])
|
||||
monkeypatch.setattr(main, "PasswordManager", lambda: pm)
|
||||
monkeypatch.setattr(main, "configure_logging", lambda: None)
|
||||
monkeypatch.setattr(main, "initialize_app", lambda: None)
|
||||
monkeypatch.setattr(main.signal, "signal", lambda *a, **k: None)
|
||||
rc = main.main(["search", "ex"])
|
||||
assert rc == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "Example" in out
|
||||
|
||||
|
||||
def test_get_command(monkeypatch, capsys):
|
||||
entry = {"type": EntryType.PASSWORD.value, "length": 8}
|
||||
pm = make_pm([(0, "Example", "user", "", False)], entry=entry)
|
||||
monkeypatch.setattr(main, "PasswordManager", lambda: pm)
|
||||
monkeypatch.setattr(main, "configure_logging", lambda: None)
|
||||
monkeypatch.setattr(main, "initialize_app", lambda: None)
|
||||
monkeypatch.setattr(main.signal, "signal", lambda *a, **k: None)
|
||||
rc = main.main(["get", "ex"])
|
||||
assert rc == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "pw" in out
|
||||
|
||||
|
||||
def test_totp_command(monkeypatch, capsys):
|
||||
entry = {"type": EntryType.TOTP.value, "period": 30, "index": 0}
|
||||
pm = make_pm([(0, "Example", None, None, False)], entry=entry)
|
||||
called = {}
|
||||
monkeypatch.setattr(main, "PasswordManager", lambda: pm)
|
||||
monkeypatch.setattr(main, "configure_logging", lambda: None)
|
||||
monkeypatch.setattr(main, "initialize_app", lambda: None)
|
||||
monkeypatch.setattr(main.signal, "signal", lambda *a, **k: None)
|
||||
monkeypatch.setattr(main.pyperclip, "copy", lambda v: called.setdefault("val", v))
|
||||
rc = main.main(["totp", "ex"])
|
||||
assert rc == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "123456" in out
|
||||
assert called.get("val") == "123456"
|
Reference in New Issue
Block a user