mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Remove duplicate import in test
This commit is contained in:
22
src/main.py
22
src/main.py
@@ -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):
|
||||
|
@@ -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))
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
44
src/tests/test_cli_clipboard_flag.py
Normal file
44
src/tests/test_cli_clipboard_flag.py
Normal 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
|
@@ -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)
|
||||
|
@@ -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",
|
||||
|
@@ -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"]
|
||||
|
Reference in New Issue
Block a user