12 Commits

Author SHA1 Message Date
thePR0M3TH3AN
6928b4ddbf Merge pull request #826 from PR0M3TH3AN/codex/add-tests-for-seed-word-flow
test: add word-by-word seed flow tests
2025-08-18 22:03:41 -04:00
thePR0M3TH3AN
73183d53a5 test: cover invalid word and fingerprint flows 2025-08-18 21:55:39 -04:00
thePR0M3TH3AN
c9ad16f150 Merge pull request #825 from PR0M3TH3AN/codex/extend-test-coverage-for-key/value-and-managed-accounts
test: add key-value and managed account entry tests
2025-08-18 21:18:20 -04:00
thePR0M3TH3AN
bd86bdbb3a test: add key-value and managed account entry tests 2025-08-18 21:12:32 -04:00
thePR0M3TH3AN
8d5374ef5b Merge pull request #824 from PR0M3TH3AN/codex/add-all_entry_types-constant-and-update-filters
Support listing all entry types
2025-08-18 19:22:05 -04:00
thePR0M3TH3AN
468608a369 Support listing all entry types 2025-08-18 19:12:55 -04:00
thePR0M3TH3AN
56e652089a Merge pull request #823 from PR0M3TH3AN/codex/update-documentation-for-installation-requirements
docs: note installer dependency checks
2025-08-18 18:19:33 -04:00
thePR0M3TH3AN
c353c04472 docs: note installer dependency checks 2025-08-18 18:18:07 -04:00
thePR0M3TH3AN
2559920a14 Merge pull request #822 from PR0M3TH3AN/codex/update-readme-and-examples-for-seedpass-commands
docs: update vault import/export commands
2025-08-18 18:11:18 -04:00
thePR0M3TH3AN
57935bdfc1 docs: update vault import/export commands 2025-08-18 18:09:43 -04:00
thePR0M3TH3AN
55fdee522c Merge pull request #821 from PR0M3TH3AN/codex/update-install.sh-argument-parsing
Default GUI install with opt-out flag
2025-08-18 18:00:14 -04:00
thePR0M3TH3AN
af4eb72385 Default to GUI install with opt-out flag 2025-08-18 17:50:18 -04:00
19 changed files with 291 additions and 61 deletions

View File

@@ -146,6 +146,10 @@ The Windows installer will attempt to install Git automatically if it is not alr
**Note:** If this fallback fails, install Python 3.12 manually or install the [Microsoft Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and rerun the installer. **Note:** If this fallback fails, install Python 3.12 manually or install the [Microsoft Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and rerun the installer.
#### Installer Dependency Checks
The installer verifies that core build tooling—C/C++ build tools, Rust, CMake, and the imaging/GTK libraries—are available before completing. Pass `--no-gui` to skip installing GUI packages. On Linux, ensure `xclip` or `wl-clipboard` is installed for clipboard support.
#### Windows Nostr Sync Troubleshooting #### Windows Nostr Sync Troubleshooting
When backing up or restoring from Nostr on Windows, a few issues are common: When backing up or restoring from Nostr on Windows, a few issues are common:
@@ -274,10 +278,10 @@ You can then launch SeedPass and create a backup:
seedpass seedpass
# Export your index # Export your index
seedpass export --file "~/seedpass_backup.json" seedpass vault export --file "~/seedpass_backup.json"
# Later you can restore it # Later you can restore it
seedpass import --file "~/seedpass_backup.json" seedpass vault import --file "~/seedpass_backup.json"
# Quickly find or retrieve entries # Quickly find or retrieve entries
seedpass search "github" seedpass search "github"

View File

@@ -120,6 +120,11 @@ isn't on your PATH. If these tools are unavailable you'll see a link to download
the installer now attempts to download Python 3.12 automatically so you don't have to compile packages from source. the installer now attempts to download Python 3.12 automatically so you don't have to compile packages from source.
**Note:** If this fallback fails, install Python 3.12 manually or install the [Microsoft Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and rerun the installer. **Note:** If this fallback fails, install Python 3.12 manually or install the [Microsoft Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and rerun the installer.
#### Installer Dependency Checks
The installer verifies that core build tooling—C/C++ build tools, Rust, CMake, and the imaging/GTK libraries—are available before completing. Pass `--no-gui` to skip installing GUI packages. On Linux, ensure `xclip` or `wl-clipboard` is installed for clipboard support.
### Uninstall ### Uninstall
Run the matching uninstaller if you need to remove a previous installation or clean up an old `seedpass` command: Run the matching uninstaller if you need to remove a previous installation or clean up an old `seedpass` command:

View File

@@ -17,7 +17,7 @@ VENV_DIR="$INSTALL_DIR/venv"
LAUNCHER_DIR="$HOME/.local/bin" LAUNCHER_DIR="$HOME/.local/bin"
LAUNCHER_PATH="$LAUNCHER_DIR/seedpass" LAUNCHER_PATH="$LAUNCHER_DIR/seedpass"
BRANCH="main" # Default branch BRANCH="main" # Default branch
INSTALL_GUI=false INSTALL_GUI=true
# --- Helper Functions --- # --- Helper Functions ---
print_info() { echo -e "\033[1;34m[INFO]\033[0m" "$1"; } print_info() { echo -e "\033[1;34m[INFO]\033[0m" "$1"; }
@@ -59,9 +59,9 @@ install_dependencies() {
fi fi
} }
usage() { usage() {
echo "Usage: $0 [-b | --branch <branch_name>] [--with-gui] [-h | --help]" echo "Usage: $0 [-b | --branch <branch_name>] [--no-gui] [-h | --help]"
echo " -b, --branch Specify the git branch to install (default: main)" echo " -b, --branch Specify the git branch to install (default: main)"
echo " --with-gui Include graphical interface dependencies" echo " --no-gui Skip graphical interface dependencies (default: include GUI)"
echo " -h, --help Display this help message" echo " -h, --help Display this help message"
exit 0 exit 0
} }
@@ -82,8 +82,8 @@ main() {
-h|--help) -h|--help)
usage usage
;; ;;
--with-gui) --no-gui)
INSTALL_GUI=true INSTALL_GUI=false
shift shift
;; ;;
*) *)

View File

@@ -478,7 +478,7 @@ def get_totp_codes(
_require_password(request, password) _require_password(request, password)
pm = _get_pm(request) pm = _get_pm(request)
entries = pm.entry_manager.list_entries( entries = pm.entry_manager.list_entries(
filter_kind=EntryType.TOTP.value, include_archived=False filter_kinds=[EntryType.TOTP.value], include_archived=False
) )
codes = [] codes = []
for idx, label, _u, _url, _arch in entries: for idx, label, _u, _url, _arch in entries:

View File

@@ -6,8 +6,10 @@ from pathlib import Path
from typing import List, Optional from typing import List, Optional
import typer import typer
import click
from .common import _get_entry_service, EntryType from .common import _get_entry_service, EntryType
from seedpass.core.entry_types import ALL_ENTRY_TYPES
from utils.clipboard import ClipboardUnavailableError from utils.clipboard import ClipboardUnavailableError
@@ -20,13 +22,20 @@ def entry_list(
sort: str = typer.Option( sort: str = typer.Option(
"index", "--sort", help="Sort by 'index', 'label', or 'updated'" "index", "--sort", help="Sort by 'index', 'label', or 'updated'"
), ),
kind: Optional[str] = typer.Option(None, "--kind", help="Filter by entry type"), kind: Optional[str] = typer.Option(
None,
"--kind",
help="Filter by entry type",
click_type=click.Choice(ALL_ENTRY_TYPES),
),
archived: bool = typer.Option(False, "--archived", help="Include archived"), archived: bool = typer.Option(False, "--archived", help="Include archived"),
) -> None: ) -> None:
"""List entries in the vault.""" """List entries in the vault."""
service = _get_entry_service(ctx) service = _get_entry_service(ctx)
entries = service.list_entries( entries = service.list_entries(
sort_by=sort, filter_kind=kind, include_archived=archived sort_by=sort,
filter_kinds=[kind] if kind else None,
include_archived=archived,
) )
for idx, label, username, url, is_archived in entries: for idx, label, username, url, is_archived in entries:
line = f"{idx}: {label}" line = f"{idx}: {label}"
@@ -43,16 +52,17 @@ def entry_list(
def entry_search( def entry_search(
ctx: typer.Context, ctx: typer.Context,
query: str, query: str,
kind: List[str] = typer.Option( kinds: List[str] = typer.Option(
None, None,
"--kind", "--kind",
"-k", "-k",
help="Filter by entry kinds (can be repeated)", help="Filter by entry kinds (can be repeated)",
click_type=click.Choice(ALL_ENTRY_TYPES),
), ),
) -> None: ) -> None:
"""Search entries.""" """Search entries."""
service = _get_entry_service(ctx) service = _get_entry_service(ctx)
kinds = list(kind) if kind else None kinds = list(kinds) if kinds else None
results = service.search_entries(query, kinds=kinds) results = service.search_entries(query, kinds=kinds)
if not results: if not results:
typer.echo("No matching entries found") typer.echo("No matching entries found")

View File

@@ -265,13 +265,13 @@ class EntryService:
def list_entries( def list_entries(
self, self,
sort_by: str = "index", sort_by: str = "index",
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
include_archived: bool = False, include_archived: bool = False,
): ):
with self._lock: with self._lock:
return self._manager.entry_manager.list_entries( return self._manager.entry_manager.list_entries(
sort_by=sort_by, sort_by=sort_by,
filter_kind=filter_kind, filter_kinds=filter_kinds,
include_archived=include_archived, include_archived=include_archived,
) )

View File

@@ -33,7 +33,7 @@ from pathlib import Path
from termcolor import colored from termcolor import colored
from .migrations import LATEST_VERSION from .migrations import LATEST_VERSION
from .entry_types import EntryType from .entry_types import EntryType, ALL_ENTRY_TYPES
from .totp import TotpManager from .totp import TotpManager
from utils.fingerprint import generate_fingerprint from utils.fingerprint import generate_fingerprint
from utils.checksum import canonical_json_dumps from utils.checksum import canonical_json_dumps
@@ -1076,7 +1076,7 @@ class EntryManager:
def list_entries( def list_entries(
self, self,
sort_by: str = "index", sort_by: str = "index",
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
*, *,
include_archived: bool = False, include_archived: bool = False,
verbose: bool = True, verbose: bool = True,
@@ -1088,8 +1088,9 @@ class EntryManager:
sort_by: sort_by:
Field to sort by. Supported values are ``"index"``, ``"label"`` and Field to sort by. Supported values are ``"index"``, ``"label"`` and
``"updated"``. ``"updated"``.
filter_kind: filter_kinds:
Optional entry kind to restrict the results. Optional list of entry kinds to restrict the results. Defaults to
``ALL_ENTRY_TYPES``.
Archived entries are omitted unless ``include_archived`` is ``True``. Archived entries are omitted unless ``include_archived`` is ``True``.
""" """
@@ -1118,12 +1119,14 @@ class EntryManager:
sorted_items = sorted(entries_data.items(), key=sort_key) sorted_items = sorted(entries_data.items(), key=sort_key)
if filter_kinds is None:
filter_kinds = ALL_ENTRY_TYPES
filtered_items: List[Tuple[int, Dict[str, Any]]] = [] filtered_items: List[Tuple[int, Dict[str, Any]]] = []
for idx_str, entry in sorted_items: for idx_str, entry in sorted_items:
if ( if (
filter_kind is not None entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
and entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) not in filter_kinds
!= filter_kind
): ):
continue continue
if not include_archived and entry.get( if not include_archived and entry.get(
@@ -1371,7 +1374,7 @@ class EntryManager:
def list_all_entries( def list_all_entries(
self, self,
sort_by: str = "index", sort_by: str = "index",
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
*, *,
include_archived: bool = False, include_archived: bool = False,
) -> None: ) -> None:
@@ -1379,7 +1382,7 @@ class EntryManager:
try: try:
entries = self.list_entries( entries = self.list_entries(
sort_by=sort_by, sort_by=sort_by,
filter_kind=filter_kind, filter_kinds=filter_kinds,
include_archived=include_archived, include_archived=include_archived,
) )
if not entries: if not entries:
@@ -1403,7 +1406,7 @@ class EntryManager:
def get_entry_summaries( def get_entry_summaries(
self, self,
filter_kind: str | None = None, filter_kinds: list[str] | None = None,
*, *,
include_archived: bool = False, include_archived: bool = False,
) -> list[tuple[int, str, str]]: ) -> list[tuple[int, str, str]]:
@@ -1412,10 +1415,13 @@ class EntryManager:
data = self._load_index() data = self._load_index()
entries_data = data.get("entries", {}) entries_data = data.get("entries", {})
if filter_kinds is None:
filter_kinds = ALL_ENTRY_TYPES
summaries: list[tuple[int, str, str]] = [] summaries: list[tuple[int, str, str]] = []
for idx_str, entry in entries_data.items(): for idx_str, entry in entries_data.items():
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
if filter_kind and etype != filter_kind: if etype not in filter_kinds:
continue continue
if not include_archived and entry.get( if not include_archived and entry.get(
"archived", entry.get("blacklisted", False) "archived", entry.get("blacklisted", False)

View File

@@ -15,3 +15,7 @@ class EntryType(str, Enum):
NOSTR = "nostr" NOSTR = "nostr"
KEY_VALUE = "key_value" KEY_VALUE = "key_value"
MANAGED_ACCOUNT = "managed_account" MANAGED_ACCOUNT = "managed_account"
# List of all entry type values for convenience
ALL_ENTRY_TYPES = [e.value for e in EntryType]

View File

@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
from termcolor import colored from termcolor import colored
from .entry_types import EntryType from .entry_types import EntryType, ALL_ENTRY_TYPES
import seedpass.core.manager as manager_module import seedpass.core.manager as manager_module
from utils.color_scheme import color_text from utils.color_scheme import color_text
from utils.terminal_utils import clear_header_with_notification from utils.terminal_utils import clear_header_with_notification
@@ -36,33 +36,16 @@ class MenuHandler:
) )
print(color_text("\nList Entries:", "menu")) print(color_text("\nList Entries:", "menu"))
print(color_text("1. All", "menu")) print(color_text("1. All", "menu"))
print(color_text("2. Passwords", "menu")) option_map: dict[str, str] = {}
print(color_text("3. 2FA (TOTP)", "menu")) for i, etype in enumerate(ALL_ENTRY_TYPES, start=2):
print(color_text("4. SSH Key", "menu")) label = etype.replace("_", " ").title()
print(color_text("5. Seed Phrase", "menu")) print(color_text(f"{i}. {label}", "menu"))
print(color_text("6. Nostr Key Pair", "menu")) option_map[str(i)] = etype
print(color_text("7. PGP", "menu"))
print(color_text("8. Key/Value", "menu"))
print(color_text("9. Managed Account", "menu"))
choice = input("Select entry type or press Enter to go back: ").strip() choice = input("Select entry type or press Enter to go back: ").strip()
if choice == "1": if choice == "1":
filter_kind = None filter_kinds = None
elif choice == "2": elif choice in option_map:
filter_kind = EntryType.PASSWORD.value filter_kinds = [option_map[choice]]
elif choice == "3":
filter_kind = EntryType.TOTP.value
elif choice == "4":
filter_kind = EntryType.SSH.value
elif choice == "5":
filter_kind = EntryType.SEED.value
elif choice == "6":
filter_kind = EntryType.NOSTR.value
elif choice == "7":
filter_kind = EntryType.PGP.value
elif choice == "8":
filter_kind = EntryType.KEY_VALUE.value
elif choice == "9":
filter_kind = EntryType.MANAGED_ACCOUNT.value
elif not choice: elif not choice:
return return
else: else:
@@ -71,7 +54,7 @@ class MenuHandler:
while True: while True:
summaries = pm.entry_manager.get_entry_summaries( summaries = pm.entry_manager.get_entry_summaries(
filter_kind, include_archived=False filter_kinds, include_archived=False
) )
if not summaries: if not summaries:
break break
@@ -85,7 +68,7 @@ class MenuHandler:
) )
print(colored("\n[+] Entries:\n", "green")) print(colored("\n[+] Entries:\n", "green"))
for idx, etype, label in summaries: for idx, etype, label in summaries:
if filter_kind is None: if filter_kinds is None:
display_type = etype.capitalize() display_type = etype.capitalize()
print(colored(f"{idx}. {display_type} - {label}", "cyan")) print(colored(f"{idx}. {display_type} - {label}", "cyan"))
else: else:

View File

@@ -393,7 +393,7 @@ class TotpViewerWindow(toga.Window):
def refresh_codes(self) -> None: def refresh_codes(self) -> None:
self.table.data = [] self.table.data = []
for idx, label, *_rest in self.entries.list_entries( for idx, label, *_rest in self.entries.list_entries(
filter_kind=EntryType.TOTP.value filter_kinds=[EntryType.TOTP.value]
): ):
entry = self.entries.retrieve_entry(idx) entry = self.entries.retrieve_entry(idx)
code = self.entries.get_totp_code(idx) code = self.entries.get_totp_code(idx)

View File

@@ -16,7 +16,7 @@ from seedpass.core.entry_types import EntryType
class DummyPM: class DummyPM:
def __init__(self): def __init__(self):
self.entry_manager = SimpleNamespace( self.entry_manager = SimpleNamespace(
list_entries=lambda sort_by="index", filter_kind=None, include_archived=False: [ list_entries=lambda sort_by="index", filter_kinds=None, include_archived=False: [
(1, "Label", "user", "url", False) (1, "Label", "user", "url", False)
], ],
search_entries=lambda q, kinds=None: [ search_entries=lambda q, kinds=None: [

View File

@@ -30,8 +30,8 @@ class DummyEntries:
self.data = [(1, "Example", None, None, False)] self.data = [(1, "Example", None, None, False)]
self.code = "111111" self.code = "111111"
def list_entries(self, sort_by="index", filter_kind=None, include_archived=False): def list_entries(self, sort_by="index", filter_kinds=None, include_archived=False):
if filter_kind: if filter_kinds:
return [(idx, label, None, None, False) for idx, label, *_ in self.data] return [(idx, label, None, None, False) for idx, label, *_ in self.data]
return self.data return self.data

View File

@@ -9,7 +9,7 @@ from seedpass_gui.app import MainWindow
class DummyEntries: class DummyEntries:
def list_entries(self, sort_by="index", filter_kind=None, include_archived=False): def list_entries(self, sort_by="index", filter_kinds=None, include_archived=False):
return [] return []
def search_entries(self, q): def search_entries(self, q):

View File

@@ -37,10 +37,30 @@ def test_add_and_modify_key_value():
"tags": [], "tags": [],
} }
# Appears in listing
assert em.list_entries() == [(idx, "API entry", None, None, False)]
# Modify key and value
em.modify_entry(idx, key="api_key2", value="def456") em.modify_entry(idx, key="api_key2", value="def456")
updated = em.retrieve_entry(idx) updated = em.retrieve_entry(idx)
assert updated["key"] == "api_key2" assert updated["key"] == "api_key2"
assert updated["value"] == "def456" assert updated["value"] == "def456"
# Archive and ensure it disappears from the default listing
em.archive_entry(idx)
archived = em.retrieve_entry(idx)
assert archived["archived"] is True
assert em.list_entries() == []
assert em.list_entries(include_archived=True) == [
(idx, "API entry", None, None, True)
]
# Restore and ensure it reappears
em.restore_entry(idx)
restored = em.retrieve_entry(idx)
assert restored["archived"] is False
assert em.list_entries() == [(idx, "API entry", None, None, False)]
# Values are not searchable
results = em.search_entries("def456") results = em.search_entries("def456")
assert results == [] assert results == []

View File

@@ -0,0 +1,85 @@
from __future__ import annotations
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from typer.testing import CliRunner
from seedpass.cli import app as cli_app
from seedpass.cli import entry as entry_cli
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager
from seedpass.core.entry_management import EntryManager
from seedpass.core.manager import PasswordManager, EncryptionMode
def _setup_manager(tmp_path: Path) -> tuple[PasswordManager, EntryManager]:
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
pm = PasswordManager.__new__(PasswordManager)
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.encryption_manager = enc_mgr
pm.vault = vault
pm.entry_manager = entry_mgr
pm.backup_manager = backup_mgr
pm.parent_seed = TEST_SEED
pm.nostr_client = SimpleNamespace()
pm.fingerprint_dir = tmp_path
pm.secret_mode_enabled = False
return pm, entry_mgr
def _create_all_entries(em: EntryManager) -> None:
em.add_entry("pw", 8)
em.add_totp("totp", TEST_SEED)
em.add_ssh_key("ssh", TEST_SEED)
em.add_seed("seed", TEST_SEED, words_num=12)
em.add_nostr_key("nostr", TEST_SEED)
em.add_pgp_key("pgp", TEST_SEED)
em.add_key_value("kv", "k", "v")
em.add_managed_account("acct", TEST_SEED)
def test_cli_list_all_types(monkeypatch):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, em = _setup_manager(tmp_path)
_create_all_entries(em)
def fake_get_entry_service(_ctx):
return SimpleNamespace(
list_entries=lambda sort_by, filter_kinds, include_archived: pm.entry_manager.list_entries(
sort_by=sort_by,
filter_kinds=filter_kinds,
include_archived=include_archived,
)
)
monkeypatch.setattr(entry_cli, "_get_entry_service", fake_get_entry_service)
runner = CliRunner()
result = runner.invoke(cli_app, ["entry", "list"])
assert result.exit_code == 0
out = result.stdout
for label in ["pw", "totp", "ssh", "seed", "nostr", "pgp", "kv", "acct"]:
assert label in out
def test_menu_list_all_types(monkeypatch, capsys):
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
pm, em = _setup_manager(tmp_path)
_create_all_entries(em)
inputs = iter(["1", "", ""]) # choose All then exit
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
pm.handle_list_entries()
out = capsys.readouterr().out
for label in ["pw", "totp", "ssh", "seed", "nostr", "pgp", "kv", "acct"]:
assert label in out

View File

@@ -57,5 +57,5 @@ def test_filter_by_type():
em = setup_entry_manager(tmp_path) em = setup_entry_manager(tmp_path)
em.add_entry("site", 8, "user") em.add_entry("site", 8, "user")
em.add_totp("Example", TEST_SEED) em.add_totp("Example", TEST_SEED)
result = em.list_entries(filter_kind=EntryType.TOTP.value) result = em.list_entries(filter_kinds=[EntryType.TOTP.value])
assert result == [(1, "Example", None, None, False)] assert result == [(1, "Example", None, None, False)]

View File

@@ -41,6 +41,9 @@ def test_add_and_get_managed_account_seed():
assert fp assert fp
assert (tmp_path / "accounts" / fp).exists() assert (tmp_path / "accounts" / fp).exists()
# Appears in listing
assert mgr.list_entries() == [(idx, "acct", None, None, False)]
phrase_a = mgr.get_managed_account_seed(idx, TEST_SEED) phrase_a = mgr.get_managed_account_seed(idx, TEST_SEED)
phrase_b = mgr.get_managed_account_seed(idx, TEST_SEED) phrase_b = mgr.get_managed_account_seed(idx, TEST_SEED)
assert phrase_a == phrase_b assert phrase_a == phrase_b
@@ -51,6 +54,23 @@ def test_add_and_get_managed_account_seed():
assert phrase_a == expected assert phrase_a == expected
assert generate_fingerprint(phrase_a) == fp assert generate_fingerprint(phrase_a) == fp
# Archive and ensure it disappears from default listing
mgr.archive_entry(idx)
archived = mgr.retrieve_entry(idx)
assert archived["archived"] is True
assert mgr.list_entries() == []
assert mgr.list_entries(include_archived=True) == [
(idx, "acct", None, None, True)
]
# Restore and ensure deterministic derivation is unchanged
mgr.restore_entry(idx)
restored = mgr.retrieve_entry(idx)
assert restored["archived"] is False
assert mgr.list_entries() == [(idx, "acct", None, None, False)]
phrase_c = mgr.get_managed_account_seed(idx, TEST_SEED)
assert phrase_c == expected
def test_load_and_exit_managed_account(monkeypatch): def test_load_and_exit_managed_account(monkeypatch):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:

View File

@@ -0,0 +1,93 @@
import builtins
from types import SimpleNamespace
import pytest
import seedpass.core.manager as manager_module
from helpers import TEST_SEED
from utils import seed_prompt
def test_prompt_seed_words_confirmation_loop(monkeypatch):
phrase = TEST_SEED
words = phrase.split()
inputs = iter(words + [words[2]])
confirmations = iter(["y", "y", "n", "y"] + ["y"] * (len(words) - 3))
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs))
monkeypatch.setattr(seed_prompt, "_apply_backoff", lambda *_a, **_k: None)
monkeypatch.setattr(seed_prompt, "clear_screen", lambda *_a, **_k: None)
monkeypatch.setattr(builtins, "input", lambda *_: next(confirmations))
result = seed_prompt.prompt_seed_words(len(words))
assert result == phrase
def test_prompt_seed_words_invalid_word(monkeypatch):
phrase = TEST_SEED
words = phrase.split()
inputs = iter(["invalid"] + words)
confirmations = iter(["y"] * len(words))
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(inputs))
monkeypatch.setattr(seed_prompt, "_apply_backoff", lambda *_a, **_k: None)
monkeypatch.setattr(seed_prompt, "clear_screen", lambda *_a, **_k: None)
monkeypatch.setattr(builtins, "input", lambda *_: next(confirmations))
result = seed_prompt.prompt_seed_words(len(words))
assert result == phrase
def test_add_new_fingerprint_words_flow_success(monkeypatch):
pm = manager_module.PasswordManager.__new__(manager_module.PasswordManager)
pm.fingerprint_manager = SimpleNamespace(current_fingerprint=None)
pm.initialize_managers = lambda: None
phrase = TEST_SEED
words = phrase.split()
word_iter = iter(words)
inputs = iter(["2"] + ["y"] * len(words))
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(word_iter))
monkeypatch.setattr(seed_prompt, "_apply_backoff", lambda *_a, **_k: None)
monkeypatch.setattr(seed_prompt, "clear_screen", lambda *_a, **_k: None)
monkeypatch.setattr(builtins, "input", lambda *_: next(inputs))
captured = {}
def finalize(self, seed, password=None):
captured["seed"] = seed
self.parent_seed = seed
return "fp"
monkeypatch.setattr(
manager_module.PasswordManager, "_finalize_existing_seed", finalize
)
result = pm.add_new_fingerprint()
assert result == "fp"
assert pm.fingerprint_manager.current_fingerprint == "fp"
assert captured["seed"] == phrase
assert pm.parent_seed == phrase
def test_add_new_fingerprint_words_flow_invalid_phrase(monkeypatch):
pm = manager_module.PasswordManager.__new__(manager_module.PasswordManager)
pm.fingerprint_manager = SimpleNamespace(current_fingerprint=None)
pm.initialize_managers = lambda: None
words = ["abandon"] * 12
word_iter = iter(words)
inputs = iter(["2"] + ["y"] * len(words))
monkeypatch.setattr(seed_prompt, "masked_input", lambda *_: next(word_iter))
monkeypatch.setattr(seed_prompt, "_apply_backoff", lambda *_a, **_k: None)
monkeypatch.setattr(seed_prompt, "clear_screen", lambda *_a, **_k: None)
monkeypatch.setattr(builtins, "input", lambda *_: next(inputs))
with pytest.raises(SystemExit):
pm.add_new_fingerprint()
assert pm.fingerprint_manager.current_fingerprint is None
assert not hasattr(pm, "parent_seed")

View File

@@ -18,8 +18,8 @@ runner = CliRunner()
def test_entry_list(monkeypatch): def test_entry_list(monkeypatch):
called = {} called = {}
def list_entries(sort_by="index", filter_kind=None, include_archived=False): def list_entries(sort_by="index", filter_kinds=None, include_archived=False):
called["args"] = (sort_by, filter_kind, include_archived) called["args"] = (sort_by, filter_kinds, include_archived)
return [(0, "Site", "user", "", False)] return [(0, "Site", "user", "", False)]
pm = SimpleNamespace( pm = SimpleNamespace(