Remove duplicate import in test

This commit is contained in:
thePR0M3TH3AN
2025-08-05 22:18:26 -04:00
parent 1870614d8a
commit 6888fa2431
8 changed files with 138 additions and 53 deletions

View File

@@ -32,6 +32,7 @@ from utils import (
pause,
clear_header_with_notification,
)
from utils.clipboard import ClipboardUnavailableError
from utils.atomic_write import atomic_write
import queue
from local_bip85.bip85 import Bip85Error
@@ -1233,6 +1234,11 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
load_global_config()
parser = argparse.ArgumentParser()
parser.add_argument("--fingerprint")
parser.add_argument(
"--no-clipboard",
action="store_true",
help="Disable clipboard support and print secrets",
)
sub = parser.add_subparsers(dest="command")
exp = sub.add_parser("export")
@@ -1264,6 +1270,9 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
return 1
if args.no_clipboard:
password_manager.secret_mode_enabled = False
if args.command == "export":
password_manager.handle_export_database(Path(args.file))
return 0
@@ -1311,8 +1320,17 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in
idx, password_manager.parent_seed
)
print(code)
if copy_to_clipboard(code, password_manager.clipboard_clear_delay):
print(colored("Code copied to clipboard", "green"))
try:
if copy_to_clipboard(code, password_manager.clipboard_clear_delay):
print(colored("Code copied to clipboard", "green"))
except ClipboardUnavailableError as exc:
print(
colored(
f"Clipboard unavailable: {exc}\n"
"Re-run with '--no-clipboard' to print codes instead.",
"yellow",
)
)
return 0
def signal_handler(sig, _frame):

View File

@@ -23,6 +23,13 @@ fingerprint_option = typer.Option(
help="Specify which seed profile to use",
)
no_clipboard_option = typer.Option(
False,
"--no-clipboard",
help="Disable clipboard support and print secrets instead",
is_flag=True,
)
# Sub command groups
from . import entry, vault, nostr, config, fingerprint, util, api
@@ -44,12 +51,16 @@ def _gui_backend_available() -> bool:
@app.callback(invoke_without_command=True)
def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) -> None:
def main(
ctx: typer.Context,
fingerprint: Optional[str] = fingerprint_option,
no_clipboard: bool = no_clipboard_option,
) -> None:
"""SeedPass CLI entry point.
When called without a subcommand this launches the interactive TUI.
"""
ctx.obj = {"fingerprint": fingerprint}
ctx.obj = {"fingerprint": fingerprint, "no_clipboard": no_clipboard}
if ctx.invoked_subcommand is None:
tui = importlib.import_module("main")
raise typer.Exit(tui.main(fingerprint=fingerprint))

View File

@@ -27,6 +27,8 @@ def _get_pm(ctx: typer.Context) -> PasswordManager:
pm = PasswordManager()
else:
pm = PasswordManager(fingerprint=fp)
if ctx.obj.get("no_clipboard"):
pm.secret_mode_enabled = False
return pm

View File

@@ -8,6 +8,7 @@ from typing import List, Optional
import typer
from .common import _get_entry_service, EntryType
from utils.clipboard import ClipboardUnavailableError
app = typer.Typer(help="Manage individual entries")
@@ -69,31 +70,39 @@ def entry_search(
def entry_get(ctx: typer.Context, query: str) -> None:
"""Retrieve a single entry's secret."""
service = _get_entry_service(ctx)
matches = service.search_entries(query)
if len(matches) == 0:
typer.echo("No matching entries found")
raise typer.Exit(code=1)
if len(matches) > 1:
typer.echo("Matches:")
for idx, label, username, _url, _arch, etype in matches:
name = f"{idx}: {etype.value.replace('_', ' ').title()} - {label}"
if username:
name += f" ({username})"
typer.echo(name)
raise typer.Exit(code=1)
try:
matches = service.search_entries(query)
if len(matches) == 0:
typer.echo("No matching entries found")
raise typer.Exit(code=1)
if len(matches) > 1:
typer.echo("Matches:")
for idx, label, username, _url, _arch, etype in matches:
name = f"{idx}: {etype.value.replace('_', ' ').title()} - {label}"
if username:
name += f" ({username})"
typer.echo(name)
raise typer.Exit(code=1)
index = matches[0][0]
entry = service.retrieve_entry(index)
etype = entry.get("type", entry.get("kind"))
if etype == EntryType.PASSWORD.value:
length = int(entry.get("length", 12))
password = service.generate_password(length, index)
typer.echo(password)
elif etype == EntryType.TOTP.value:
code = service.get_totp_code(index)
typer.echo(code)
else:
typer.echo("Unsupported entry type")
index = matches[0][0]
entry = service.retrieve_entry(index)
etype = entry.get("type", entry.get("kind"))
if etype == EntryType.PASSWORD.value:
length = int(entry.get("length", 12))
password = service.generate_password(length, index)
typer.echo(password)
elif etype == EntryType.TOTP.value:
code = service.get_totp_code(index)
typer.echo(code)
else:
typer.echo("Unsupported entry type")
raise typer.Exit(code=1)
except ClipboardUnavailableError as exc:
typer.echo(
f"Clipboard unavailable: {exc}\n"
"Re-run with '--no-clipboard' to print secrets instead.",
err=True,
)
raise typer.Exit(code=1)

View File

@@ -0,0 +1,44 @@
from typer.testing import CliRunner
from seedpass.cli import app, entry as cli_entry
from seedpass.core.entry_types import EntryType
from utils.clipboard import ClipboardUnavailableError
runner = CliRunner()
def _stub_service(ctx, raise_error=True):
class Service:
def search_entries(self, query, kinds=None):
return [(1, "label", None, None, False, EntryType.PASSWORD)]
def retrieve_entry(self, idx):
return {"type": EntryType.PASSWORD.value, "length": 12}
def generate_password(self, length, index):
if raise_error and not ctx.obj.get("no_clipboard"):
raise ClipboardUnavailableError("missing")
return "pwd"
return Service()
def test_entry_get_handles_missing_clipboard(monkeypatch):
monkeypatch.setattr(
cli_entry, "_get_entry_service", lambda ctx: _stub_service(ctx, True)
)
result = runner.invoke(app, ["entry", "get", "label"], catch_exceptions=False)
assert result.exit_code == 1
assert "no-clipboard" in result.stderr.lower()
def test_entry_get_no_clipboard_flag(monkeypatch):
monkeypatch.setattr(
cli_entry, "_get_entry_service", lambda ctx: _stub_service(ctx, True)
)
result = runner.invoke(
app, ["--no-clipboard", "entry", "get", "label"], catch_exceptions=False
)
assert result.exit_code == 0
assert "pwd" in result.stdout

View File

@@ -7,7 +7,9 @@ import sys
sys.path.append(str(Path(__file__).resolve().parents[1]))
from utils.clipboard import copy_to_clipboard
import pytest
from utils.clipboard import ClipboardUnavailableError, copy_to_clipboard
def test_copy_to_clipboard_clears(monkeypatch):
@@ -69,7 +71,7 @@ def test_copy_to_clipboard_does_not_clear_if_changed(monkeypatch):
assert clipboard["text"] == "other"
def test_copy_to_clipboard_missing_dependency(monkeypatch, capsys):
def test_copy_to_clipboard_missing_dependency(monkeypatch):
def fail_copy(*args, **kwargs):
raise pyperclip.PyperclipException("no copy")
@@ -77,6 +79,5 @@ def test_copy_to_clipboard_missing_dependency(monkeypatch, capsys):
monkeypatch.setattr(pyperclip, "paste", lambda: "")
monkeypatch.setattr(shutil, "which", lambda cmd: None)
copy_to_clipboard("secret", 1)
out = capsys.readouterr().out
assert "install xclip" in out.lower()
with pytest.raises(ClipboardUnavailableError):
copy_to_clipboard("secret", 1)

View File

@@ -38,13 +38,16 @@ from .atomic_write import atomic_write
# Optional clipboard support
try: # pragma: no cover - exercised when dependency missing
from .clipboard import copy_to_clipboard
from .clipboard import ClipboardUnavailableError, copy_to_clipboard
except Exception as exc: # pragma: no cover - executed only if pyperclip missing
class ClipboardUnavailableError(RuntimeError):
"""Stub exception when clipboard support is unavailable."""
def copy_to_clipboard(*_args, **_kwargs):
"""Stub when clipboard support is unavailable."""
logger.warning("Clipboard support unavailable: %s", exc)
return False
raise ClipboardUnavailableError(str(exc))
__all__ = [
@@ -69,6 +72,7 @@ __all__ = [
"timed_input",
"InMemorySecret",
"copy_to_clipboard",
"ClipboardUnavailableError",
"clear_screen",
"clear_and_print_fingerprint",
"clear_header_with_notification",

View File

@@ -1,3 +1,5 @@
"""Clipboard utility helpers."""
import logging
import shutil
import sys
@@ -5,6 +7,11 @@ import threading
import pyperclip
class ClipboardUnavailableError(RuntimeError):
"""Raised when required clipboard utilities are not installed."""
logger = logging.getLogger(__name__)
@@ -15,31 +22,17 @@ def _ensure_clipboard() -> None:
except pyperclip.PyperclipException as exc:
if sys.platform.startswith("linux"):
if shutil.which("xclip") is None and shutil.which("xsel") is None:
raise pyperclip.PyperclipException(
raise ClipboardUnavailableError(
"Clipboard support requires the 'xclip' package. "
"Install it with 'sudo apt install xclip' and restart SeedPass."
"Install it with 'sudo apt install xclip' and restart SeedPass.",
) from exc
raise
def copy_to_clipboard(text: str, timeout: int) -> bool:
"""Copy text to the clipboard and clear after timeout seconds if unchanged.
"""Copy text to the clipboard and clear after ``timeout`` seconds if unchanged."""
Returns True if the text was successfully copied, False otherwise.
"""
try:
_ensure_clipboard()
except pyperclip.PyperclipException as exc:
warning = (
"Clipboard unavailable: "
+ str(exc)
+ "\nSeedPass secret mode requires clipboard support. "
"Install xclip or disable secret mode to view secrets."
)
logger.warning(warning)
print(warning)
return False
_ensure_clipboard()
pyperclip.copy(text)
@@ -51,3 +44,6 @@ def copy_to_clipboard(text: str, timeout: int) -> bool:
timer.daemon = True
timer.start()
return True
__all__ = ["copy_to_clipboard", "ClipboardUnavailableError"]