mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 07:48:57 +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 password_manager.manager import PasswordManager
|
||||||
from nostr.client import NostrClient
|
from nostr.client import NostrClient
|
||||||
|
from password_manager.entry_types import EntryType
|
||||||
from constants import INACTIVITY_TIMEOUT, initialize_app
|
from constants import INACTIVITY_TIMEOUT, initialize_app
|
||||||
from utils.password_prompt import PasswordPromptError
|
from utils.password_prompt import PasswordPromptError
|
||||||
from utils import timed_input
|
from utils import timed_input
|
||||||
from local_bip85.bip85 import Bip85Error
|
from local_bip85.bip85 import Bip85Error
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
|
|
||||||
colorama_init()
|
colorama_init()
|
||||||
@@ -233,6 +235,22 @@ def handle_display_stats(password_manager: PasswordManager) -> None:
|
|||||||
print(colored(f"Error: Failed to display stats: {e}", "red"))
|
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(
|
def handle_post_to_nostr(
|
||||||
password_manager: PasswordManager, alt_summary: str | None = None
|
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"))
|
print(colored("Invalid choice. Please select a valid option.", "red"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main(argv: list[str] | None = None) -> int:
|
||||||
# Configure logging with both file and console handlers
|
"""Entry point for the SeedPass CLI."""
|
||||||
configure_logging()
|
configure_logging()
|
||||||
initialize_app()
|
initialize_app()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.info("Starting SeedPass Password Manager")
|
logger.info("Starting SeedPass Password Manager")
|
||||||
|
|
||||||
# Load config from disk and parse command-line arguments
|
load_global_config()
|
||||||
cfg = load_global_config()
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
sub = parser.add_subparsers(dest="command")
|
sub = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
@@ -720,48 +737,96 @@ if __name__ == "__main__":
|
|||||||
imp = sub.add_parser("import")
|
imp = sub.add_parser("import")
|
||||||
imp.add_argument("--file")
|
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:
|
try:
|
||||||
password_manager = PasswordManager()
|
password_manager = PasswordManager()
|
||||||
logger.info("PasswordManager initialized successfully.")
|
logger.info("PasswordManager initialized successfully.")
|
||||||
except (PasswordPromptError, Bip85Error) as e:
|
except (PasswordPromptError, Bip85Error) as e:
|
||||||
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
|
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
|
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
|
||||||
sys.exit(1)
|
return 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
|
logger.error(f"Failed to initialize PasswordManager: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
|
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
|
||||||
sys.exit(1)
|
return 1
|
||||||
|
|
||||||
if args.command == "export":
|
if args.command == "export":
|
||||||
password_manager.handle_export_database(Path(args.file))
|
password_manager.handle_export_database(Path(args.file))
|
||||||
sys.exit(0)
|
return 0
|
||||||
elif args.command == "import":
|
if args.command == "import":
|
||||||
password_manager.handle_import_database(Path(args.file))
|
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):
|
||||||
def signal_handler(sig, frame):
|
|
||||||
"""
|
|
||||||
Handles termination signals to gracefully shutdown the NostrClient.
|
|
||||||
"""
|
|
||||||
print(colored("\nReceived shutdown signal. Exiting gracefully...", "yellow"))
|
print(colored("\nReceived shutdown signal. Exiting gracefully...", "yellow"))
|
||||||
logging.info(f"Received shutdown signal: {sig}. Initiating graceful shutdown.")
|
logging.info(f"Received shutdown signal: {sig}. Initiating graceful shutdown.")
|
||||||
try:
|
try:
|
||||||
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
|
password_manager.nostr_client.close_client_pool()
|
||||||
logging.info("NostrClient closed successfully.")
|
logging.info("NostrClient closed successfully.")
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logging.error(f"Error during shutdown: {e}")
|
logging.error(f"Error during shutdown: {exc}")
|
||||||
print(colored(f"Error during shutdown: {e}", "red"))
|
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Register the signal handlers
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler) # Handle termination signals
|
|
||||||
|
|
||||||
# Display the interactive menu to the user
|
|
||||||
try:
|
try:
|
||||||
display_menu(
|
display_menu(
|
||||||
password_manager, inactivity_timeout=password_manager.inactivity_timeout
|
password_manager, inactivity_timeout=password_manager.inactivity_timeout
|
||||||
@@ -770,29 +835,34 @@ if __name__ == "__main__":
|
|||||||
logger.info("Program terminated by user via KeyboardInterrupt.")
|
logger.info("Program terminated by user via KeyboardInterrupt.")
|
||||||
print(colored("\nProgram terminated by user.", "yellow"))
|
print(colored("\nProgram terminated by user.", "yellow"))
|
||||||
try:
|
try:
|
||||||
password_manager.nostr_client.close_client_pool() # Gracefully close the ClientPool
|
password_manager.nostr_client.close_client_pool()
|
||||||
logging.info("NostrClient closed successfully.")
|
logging.info("NostrClient closed successfully.")
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logging.error(f"Error during shutdown: {e}")
|
logging.error(f"Error during shutdown: {exc}")
|
||||||
print(colored(f"Error during shutdown: {e}", "red"))
|
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||||
sys.exit(0)
|
return 0
|
||||||
except (PasswordPromptError, Bip85Error) as e:
|
except (PasswordPromptError, Bip85Error) as e:
|
||||||
logger.error(f"A user-related error occurred: {e}", exc_info=True)
|
logger.error(f"A user-related error occurred: {e}", exc_info=True)
|
||||||
print(colored(f"Error: {e}", "red"))
|
print(colored(f"Error: {e}", "red"))
|
||||||
try:
|
try:
|
||||||
password_manager.nostr_client.close_client_pool()
|
password_manager.nostr_client.close_client_pool()
|
||||||
logging.info("NostrClient closed successfully.")
|
logging.info("NostrClient closed successfully.")
|
||||||
except Exception as close_error:
|
except Exception as exc:
|
||||||
logging.error(f"Error during shutdown: {close_error}")
|
logging.error(f"Error during shutdown: {exc}")
|
||||||
print(colored(f"Error during shutdown: {close_error}", "red"))
|
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||||
sys.exit(1)
|
return 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
||||||
print(colored(f"Error: An unexpected error occurred: {e}", "red"))
|
print(colored(f"Error: An unexpected error occurred: {e}", "red"))
|
||||||
try:
|
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.")
|
logging.info("NostrClient closed successfully.")
|
||||||
except Exception as close_error:
|
except Exception as exc:
|
||||||
logging.error(f"Error during shutdown: {close_error}")
|
logging.error(f"Error during shutdown: {exc}")
|
||||||
print(colored(f"Error during shutdown: {close_error}", "red"))
|
print(colored(f"Error during shutdown: {exc}", "red"))
|
||||||
sys.exit(1)
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
@@ -21,3 +21,4 @@ mutmut==2.4.4
|
|||||||
pyotp>=2.8.0
|
pyotp>=2.8.0
|
||||||
|
|
||||||
freezegun
|
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