mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 15:58:48 +00:00
Implement Typer CLI commands and tests
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import typer
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from password_manager.manager import PasswordManager
|
||||||
|
from password_manager.entry_types import EntryType
|
||||||
|
|
||||||
app = typer.Typer(help="SeedPass command line interface")
|
app = typer.Typer(help="SeedPass command line interface")
|
||||||
|
|
||||||
# Global option shared across all commands
|
# Global option shared across all commands
|
||||||
@@ -27,6 +32,16 @@ app.add_typer(fingerprint_app, name="fingerprint")
|
|||||||
app.add_typer(util_app, name="util")
|
app.add_typer(util_app, name="util")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pm(ctx: typer.Context) -> PasswordManager:
|
||||||
|
"""Return a PasswordManager optionally selecting a fingerprint."""
|
||||||
|
pm = PasswordManager()
|
||||||
|
fp = ctx.obj.get("fingerprint")
|
||||||
|
if fp:
|
||||||
|
# `select_fingerprint` will initialize managers
|
||||||
|
pm.select_fingerprint(fp)
|
||||||
|
return pm
|
||||||
|
|
||||||
|
|
||||||
@app.callback()
|
@app.callback()
|
||||||
def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) -> None:
|
def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) -> None:
|
||||||
"""SeedPass CLI entry point."""
|
"""SeedPass CLI entry point."""
|
||||||
@@ -34,9 +49,77 @@ def main(ctx: typer.Context, fingerprint: Optional[str] = fingerprint_option) ->
|
|||||||
|
|
||||||
|
|
||||||
@entry_app.command("list")
|
@entry_app.command("list")
|
||||||
def entry_list(ctx: typer.Context) -> None:
|
def entry_list(
|
||||||
|
ctx: typer.Context,
|
||||||
|
sort: str = typer.Option(
|
||||||
|
"index", "--sort", help="Sort by 'index', 'label', or 'username'"
|
||||||
|
),
|
||||||
|
kind: Optional[str] = typer.Option(None, "--kind", help="Filter by entry type"),
|
||||||
|
archived: bool = typer.Option(False, "--archived", help="Include archived"),
|
||||||
|
) -> None:
|
||||||
"""List entries in the vault."""
|
"""List entries in the vault."""
|
||||||
typer.echo(f"Listing entries for fingerprint: {ctx.obj.get('fingerprint')}")
|
pm = _get_pm(ctx)
|
||||||
|
entries = pm.entry_manager.list_entries(
|
||||||
|
sort_by=sort, filter_kind=kind, include_archived=archived
|
||||||
|
)
|
||||||
|
for idx, label, username, url, is_archived in entries:
|
||||||
|
line = f"{idx}: {label}"
|
||||||
|
if username:
|
||||||
|
line += f" ({username})"
|
||||||
|
if url:
|
||||||
|
line += f" {url}"
|
||||||
|
if is_archived:
|
||||||
|
line += " [archived]"
|
||||||
|
typer.echo(line)
|
||||||
|
|
||||||
|
|
||||||
|
@entry_app.command("search")
|
||||||
|
def entry_search(ctx: typer.Context, query: str) -> None:
|
||||||
|
"""Search entries."""
|
||||||
|
pm = _get_pm(ctx)
|
||||||
|
results = pm.entry_manager.search_entries(query)
|
||||||
|
if not results:
|
||||||
|
typer.echo("No matching entries found")
|
||||||
|
return
|
||||||
|
for idx, label, username, url, _arch in results:
|
||||||
|
line = f"{idx}: {label}"
|
||||||
|
if username:
|
||||||
|
line += f" ({username})"
|
||||||
|
if url:
|
||||||
|
line += f" {url}"
|
||||||
|
typer.echo(line)
|
||||||
|
|
||||||
|
|
||||||
|
@entry_app.command("get")
|
||||||
|
def entry_get(ctx: typer.Context, query: str) -> None:
|
||||||
|
"""Retrieve a single entry's secret."""
|
||||||
|
pm = _get_pm(ctx)
|
||||||
|
matches = pm.entry_manager.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 in matches:
|
||||||
|
name = f"{idx}: {label}"
|
||||||
|
if username:
|
||||||
|
name += f" ({username})"
|
||||||
|
typer.echo(name)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
index = matches[0][0]
|
||||||
|
entry = pm.entry_manager.retrieve_entry(index)
|
||||||
|
etype = entry.get("type", entry.get("kind"))
|
||||||
|
if etype == EntryType.PASSWORD.value:
|
||||||
|
length = int(entry.get("length", 12))
|
||||||
|
password = pm.password_generator.generate_password(length, index)
|
||||||
|
typer.echo(password)
|
||||||
|
elif etype == EntryType.TOTP.value:
|
||||||
|
code = pm.entry_manager.get_totp_code(index, pm.parent_seed)
|
||||||
|
typer.echo(code)
|
||||||
|
else:
|
||||||
|
typer.echo("Unsupported entry type")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
|
||||||
@vault_app.command("export")
|
@vault_app.command("export")
|
||||||
@@ -44,9 +127,9 @@ def vault_export(
|
|||||||
ctx: typer.Context, file: str = typer.Option(..., help="Output file")
|
ctx: typer.Context, file: str = typer.Option(..., help="Output file")
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Export the vault."""
|
"""Export the vault."""
|
||||||
typer.echo(
|
pm = _get_pm(ctx)
|
||||||
f"Exporting vault for fingerprint {ctx.obj.get('fingerprint')} to {file}"
|
pm.handle_export_database(Path(file))
|
||||||
)
|
typer.echo(str(file))
|
||||||
|
|
||||||
|
|
||||||
@nostr_app.command("sync")
|
@nostr_app.command("sync")
|
||||||
@@ -55,16 +138,31 @@ def nostr_sync(ctx: typer.Context) -> None:
|
|||||||
typer.echo(f"Syncing vault for fingerprint: {ctx.obj.get('fingerprint')}")
|
typer.echo(f"Syncing vault for fingerprint: {ctx.obj.get('fingerprint')}")
|
||||||
|
|
||||||
|
|
||||||
|
@nostr_app.command("get-pubkey")
|
||||||
|
def nostr_get_pubkey(ctx: typer.Context) -> None:
|
||||||
|
"""Display the active profile's npub."""
|
||||||
|
pm = _get_pm(ctx)
|
||||||
|
npub = pm.nostr_client.key_manager.get_npub()
|
||||||
|
typer.echo(npub)
|
||||||
|
|
||||||
|
|
||||||
@config_app.command("get")
|
@config_app.command("get")
|
||||||
def config_get(ctx: typer.Context, key: str) -> None:
|
def config_get(ctx: typer.Context, key: str) -> None:
|
||||||
"""Get a configuration value."""
|
"""Get a configuration value."""
|
||||||
typer.echo(f"Get config '{key}' for fingerprint: {ctx.obj.get('fingerprint')}")
|
pm = _get_pm(ctx)
|
||||||
|
value = pm.config_manager.load_config(require_pin=False).get(key)
|
||||||
|
if value is None:
|
||||||
|
typer.echo("Key not found")
|
||||||
|
else:
|
||||||
|
typer.echo(str(value))
|
||||||
|
|
||||||
|
|
||||||
@fingerprint_app.command("list")
|
@fingerprint_app.command("list")
|
||||||
def fingerprint_list(ctx: typer.Context) -> None:
|
def fingerprint_list(ctx: typer.Context) -> None:
|
||||||
"""List available seed profiles."""
|
"""List available seed profiles."""
|
||||||
typer.echo("Listing seed profiles")
|
pm = _get_pm(ctx)
|
||||||
|
for fp in pm.fingerprint_manager.list_fingerprints():
|
||||||
|
typer.echo(fp)
|
||||||
|
|
||||||
|
|
||||||
@util_app.command("generate-password")
|
@util_app.command("generate-password")
|
||||||
|
115
src/tests/test_typer_cli.py
Normal file
115
src/tests/test_typer_cli.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
from seedpass.cli import app, PasswordManager
|
||||||
|
from seedpass import cli
|
||||||
|
from password_manager.entry_types import EntryType
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
def test_entry_list(monkeypatch):
|
||||||
|
called = {}
|
||||||
|
|
||||||
|
def list_entries(sort_by="index", filter_kind=None, include_archived=False):
|
||||||
|
called["args"] = (sort_by, filter_kind, include_archived)
|
||||||
|
return [(0, "Site", "user", "", False)]
|
||||||
|
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
entry_manager=SimpleNamespace(list_entries=list_entries),
|
||||||
|
select_fingerprint=lambda fp: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
result = runner.invoke(app, ["entry", "list"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Site" in result.stdout
|
||||||
|
assert called["args"] == ("index", None, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_entry_search(monkeypatch):
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
entry_manager=SimpleNamespace(
|
||||||
|
search_entries=lambda q: [(1, "L", None, None, False)]
|
||||||
|
),
|
||||||
|
select_fingerprint=lambda fp: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
result = runner.invoke(app, ["entry", "search", "l"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "1: L" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_entry_get_password(monkeypatch):
|
||||||
|
def search(q):
|
||||||
|
return [(2, "Example", "", "", False)]
|
||||||
|
|
||||||
|
entry = {"type": EntryType.PASSWORD.value, "length": 8}
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
entry_manager=SimpleNamespace(
|
||||||
|
search_entries=search,
|
||||||
|
retrieve_entry=lambda i: entry,
|
||||||
|
get_totp_code=lambda i, s: "",
|
||||||
|
),
|
||||||
|
password_generator=SimpleNamespace(generate_password=lambda l, i: "pw"),
|
||||||
|
parent_seed="seed",
|
||||||
|
select_fingerprint=lambda fp: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
result = runner.invoke(app, ["entry", "get", "ex"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "pw" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_vault_export(monkeypatch, tmp_path):
|
||||||
|
called = {}
|
||||||
|
|
||||||
|
def export_db(path):
|
||||||
|
called["path"] = path
|
||||||
|
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
handle_export_database=export_db, select_fingerprint=lambda fp: None
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
out_path = tmp_path / "out.json"
|
||||||
|
result = runner.invoke(app, ["vault", "export", "--file", str(out_path)])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert called["path"] == out_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_nostr_get_pubkey(monkeypatch):
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
nostr_client=SimpleNamespace(
|
||||||
|
key_manager=SimpleNamespace(get_npub=lambda: "np")
|
||||||
|
),
|
||||||
|
select_fingerprint=lambda fp: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
result = runner.invoke(app, ["nostr", "get-pubkey"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "np" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_fingerprint_list(monkeypatch):
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
fingerprint_manager=SimpleNamespace(list_fingerprints=lambda: ["a", "b"]),
|
||||||
|
select_fingerprint=lambda fp: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
result = runner.invoke(app, ["fingerprint", "list"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "a" in result.stdout and "b" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_get(monkeypatch):
|
||||||
|
pm = SimpleNamespace(
|
||||||
|
config_manager=SimpleNamespace(
|
||||||
|
load_config=lambda require_pin=False: {"x": "1"}
|
||||||
|
),
|
||||||
|
select_fingerprint=lambda fp: None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||||
|
result = runner.invoke(app, ["config", "get", "x"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "1" in result.stdout
|
Reference in New Issue
Block a user