Include entry type in search results

This commit is contained in:
thePR0M3TH3AN
2025-08-02 16:26:52 -04:00
parent b4f792ad67
commit dcd095d1af
21 changed files with 106 additions and 63 deletions

View File

@@ -59,6 +59,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No
- **Quick Unlock:** Optionally skip the password prompt after verifying once. - **Quick Unlock:** Optionally skip the password prompt after verifying once.
- **Secret Mode:** When enabled, newly generated and retrieved passwords are copied to your clipboard and automatically cleared after a delay. - **Secret Mode:** When enabled, newly generated and retrieved passwords are copied to your clipboard and automatically cleared after a delay.
- **Tagging Support:** Organize entries with optional tags and find them quickly via search. - **Tagging Support:** Organize entries with optional tags and find them quickly via search.
- **Typed Search Results:** Results now display each entry's type for quicker identification.
- **Manual Vault Export/Import:** Create encrypted backups or restore them using the CLI or API. - **Manual Vault Export/Import:** Create encrypted backups or restore them using the CLI or API.
- **Parent Seed Backup:** Securely save an encrypted copy of the master seed. - **Parent Seed Backup:** Securely save an encrypted copy of the master seed.
- **Manual Vault Locking:** Instantly clear keys from memory when needed. - **Manual Vault Locking:** Instantly clear keys from memory when needed.
@@ -235,6 +236,7 @@ seedpass import --file "~/seedpass_backup.json"
seedpass search "github" seedpass search "github"
seedpass search --tags "work,personal" seedpass search --tags "work,personal"
seedpass get "github" seedpass get "github"
# Search results show the entry type, e.g. "1: Password - GitHub"
# Retrieve a TOTP entry # Retrieve a TOTP entry
seedpass entry get "email" seedpass entry get "email"
# The code is printed and copied to your clipboard # The code is printed and copied to your clipboard

View File

@@ -136,7 +136,7 @@ Run or stop the local HTTP API.
### `entry` Commands ### `entry` Commands
- **`seedpass entry list`** List entries in the vault, optionally sorted or filtered. - **`seedpass entry list`** List entries in the vault, optionally sorted or filtered.
- **`seedpass entry search <query>`** Search across labels, usernames, URLs and notes. - **`seedpass entry search <query>`** Search across labels, usernames, URLs and notes. Results show the entry type before each label.
- **`seedpass entry get <query>`** Retrieve the password or TOTP code for one matching entry, depending on the entry's type. - **`seedpass entry get <query>`** Retrieve the password or TOTP code for one matching entry, depending on the entry's type.
- **`seedpass entry add <label>`** Create a new password entry. Use `--length` and flags like `--no-special`, `--special-mode safe`, or `--exclude-ambiguous` to override the global policy. - **`seedpass entry add <label>`** Create a new password entry. Use `--length` and flags like `--no-special`, `--special-mode safe`, or `--exclude-ambiguous` to override the global policy.
- **`seedpass entry add-totp <label>`** Create a TOTP entry. Use `--secret` to import an existing secret or `--index` to derive from the seed. - **`seedpass entry add-totp <label>`** Create a TOTP entry. Use `--secret` to import an existing secret or `--index` to derive from the seed.

View File

@@ -70,6 +70,7 @@ maintainable while enabling a consistent experience on multiple platforms.
- **Quick Unlock:** Optionally skip the password prompt after verifying once. Startup delay is unaffected. - **Quick Unlock:** Optionally skip the password prompt after verifying once. Startup delay is unaffected.
- **Secret Mode:** Copy retrieved passwords directly to your clipboard and automatically clear it after a delay. - **Secret Mode:** Copy retrieved passwords directly to your clipboard and automatically clear it after a delay.
- **Tagging Support:** Organize entries with optional tags and find them quickly via search. - **Tagging Support:** Organize entries with optional tags and find them quickly via search.
- **Typed Search Results:** Searches display each entry's type for easier scanning.
- **Manual Vault Export/Import:** Create encrypted backups or restore them using the CLI or API. - **Manual Vault Export/Import:** Create encrypted backups or restore them using the CLI or API.
- **Parent Seed Backup:** Securely save an encrypted copy of the master seed. - **Parent Seed Backup:** Securely save an encrypted copy of the master seed.
- **Manual Vault Locking:** Instantly clear keys from memory when needed. - **Manual Vault Locking:** Instantly clear keys from memory when needed.
@@ -219,6 +220,7 @@ seedpass vault import --file "~/seedpass_backup.json"
seedpass search "github" seedpass search "github"
seedpass search --tags "work,personal" seedpass search --tags "work,personal"
seedpass get "github" seedpass get "github"
# Search results show the entry type, e.g. "1: Password - GitHub"
# Retrieve a TOTP entry # Retrieve a TOTP entry
seedpass entry get "email" seedpass entry get "email"
# The code is printed and copied to your clipboard # The code is printed and copied to your clipboard

View File

@@ -342,31 +342,28 @@ def handle_display_stats(password_manager: PasswordManager) -> None:
def print_matches( def print_matches(
password_manager: PasswordManager, password_manager: PasswordManager,
matches: list[tuple[int, str, str | None, str | None, bool]], matches: list[tuple[int, str, str | None, str | None, bool, EntryType]],
) -> None: ) -> None:
"""Print a list of search matches.""" """Print a list of search matches."""
print(colored("\n[+] Matches:\n", "green")) print(colored("\n[+] Matches:\n", "green"))
for entry in matches: for entry in matches:
idx, website, username, url, blacklisted = entry idx, website, username, url, blacklisted, etype = entry
data = password_manager.entry_manager.retrieve_entry(idx) data = password_manager.entry_manager.retrieve_entry(idx)
etype = (
data.get("type", data.get("kind", EntryType.PASSWORD.value))
if data
else EntryType.PASSWORD.value
)
print(color_text(f"Index: {idx}", "index")) print(color_text(f"Index: {idx}", "index"))
if etype == EntryType.TOTP.value: if etype == EntryType.TOTP:
print(color_text(f" Label: {data.get('label', website)}", "index")) label = data.get("label", website) if data else website
print(color_text(f" Derivation Index: {data.get('index', idx)}", "index")) deriv = data.get("index", idx) if data else idx
elif etype == EntryType.SEED.value: print(color_text(f" Label: {label}", "index"))
print(color_text(f" Derivation Index: {deriv}", "index"))
elif etype == EntryType.SEED:
print(color_text(" Type: Seed Phrase", "index")) print(color_text(" Type: Seed Phrase", "index"))
elif etype == EntryType.SSH.value: elif etype == EntryType.SSH:
print(color_text(" Type: SSH Key", "index")) print(color_text(" Type: SSH Key", "index"))
elif etype == EntryType.PGP.value: elif etype == EntryType.PGP:
print(color_text(" Type: PGP Key", "index")) print(color_text(" Type: PGP Key", "index"))
elif etype == EntryType.NOSTR.value: elif etype == EntryType.NOSTR:
print(color_text(" Type: Nostr Key", "index")) print(color_text(" Type: Nostr Key", "index"))
elif etype == EntryType.KEY_VALUE.value: elif etype == EntryType.KEY_VALUE:
print(color_text(" Type: Key/Value", "index")) print(color_text(" Type: Key/Value", "index"))
else: else:
if website: if website:

View File

@@ -86,8 +86,9 @@ def search_entry(query: str, authorization: str | None = Header(None)) -> List[A
"username": username, "username": username,
"url": url, "url": url,
"archived": archived, "archived": archived,
"type": etype.value,
} }
for idx, label, username, url, archived in results for idx, label, username, url, archived, etype in results
] ]

View File

@@ -161,8 +161,8 @@ def entry_search(
if not results: if not results:
typer.echo("No matching entries found") typer.echo("No matching entries found")
return return
for idx, label, username, url, _arch in results: for idx, label, username, url, _arch, etype in results:
line = f"{idx}: {label}" line = f"{idx}: {etype.value.replace('_', ' ').title()} - {label}"
if username: if username:
line += f" ({username})" line += f" ({username})"
if url: if url:
@@ -180,8 +180,8 @@ def entry_get(ctx: typer.Context, query: str) -> None:
raise typer.Exit(code=1) raise typer.Exit(code=1)
if len(matches) > 1: if len(matches) > 1:
typer.echo("Matches:") typer.echo("Matches:")
for idx, label, username, _url, _arch in matches: for idx, label, username, _url, _arch, etype in matches:
name = f"{idx}: {label}" name = f"{idx}: {etype.value.replace('_', ' ').title()} - {label}"
if username: if username:
name += f" ({username})" name += f" ({username})"
typer.echo(name) typer.echo(name)

View File

@@ -17,6 +17,7 @@ from pydantic import BaseModel
from .manager import PasswordManager from .manager import PasswordManager
from .pubsub import bus from .pubsub import bus
from .entry_types import EntryType
class VaultExportRequest(BaseModel): class VaultExportRequest(BaseModel):
@@ -274,7 +275,7 @@ class EntryService:
def search_entries( def search_entries(
self, query: str, kinds: list[str] | None = None self, query: str, kinds: list[str] | None = None
) -> list[tuple[int, str, str | None, str | None, bool]]: ) -> list[tuple[int, str, str | None, str | None, bool, EntryType]]:
"""Search entries optionally filtering by ``kinds``. """Search entries optionally filtering by ``kinds``.
Parameters Parameters

View File

@@ -1210,8 +1210,12 @@ class EntryManager:
def search_entries( def search_entries(
self, query: str, kinds: List[str] | None = None self, query: str, kinds: List[str] | None = None
) -> List[Tuple[int, str, Optional[str], Optional[str], bool]]: ) -> List[Tuple[int, str, Optional[str], Optional[str], bool, EntryType]]:
"""Return entries matching ``query`` across whitelisted metadata fields.""" """Return entries matching ``query`` across whitelisted metadata fields.
Each match is represented as ``(index, label, username, url, archived, etype)``
where ``etype`` is the :class:`EntryType` of the entry.
"""
data = self._load_index() data = self._load_index()
entries_data = data.get("entries", {}) entries_data = data.get("entries", {})
@@ -1220,19 +1224,23 @@ class EntryManager:
return [] return []
query_lower = query.lower() query_lower = query.lower()
results: List[Tuple[int, str, Optional[str], Optional[str], bool]] = [] results: List[
Tuple[int, str, Optional[str], Optional[str], bool, EntryType]
] = []
for idx, entry in sorted(entries_data.items(), key=lambda x: int(x[0])): for idx, entry in sorted(entries_data.items(), key=lambda x: int(x[0])):
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) etype = EntryType(
entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
)
if kinds is not None and etype not in kinds: if kinds is not None and etype.value not in kinds:
continue continue
label = entry.get("label", entry.get("website", "")) label = entry.get("label", entry.get("website", ""))
username = ( username = (
entry.get("username", "") if etype == EntryType.PASSWORD.value else None entry.get("username", "") if etype == EntryType.PASSWORD else None
) )
url = entry.get("url", "") if etype == EntryType.PASSWORD.value else None url = entry.get("url", "") if etype == EntryType.PASSWORD else None
tags = entry.get("tags", []) tags = entry.get("tags", [])
archived = entry.get("archived", entry.get("blacklisted", False)) archived = entry.get("archived", entry.get("blacklisted", False))
@@ -1249,6 +1257,7 @@ class EntryManager:
username if username is not None else None, username if username is not None else None,
url if url is not None else None, url if url is not None else None,
archived, archived,
etype,
) )
) )

View File

@@ -3379,11 +3379,12 @@ class PasswordManager:
child_fingerprint=child_fp, child_fingerprint=child_fp,
) )
print(colored("\n[+] Search Results:\n", "green")) print(colored("\n[+] Search Results:\n", "green"))
for idx, label, username, _url, _b in results: for idx, label, username, _url, _b, etype in results:
display_label = label display_label = label
if username: if username:
display_label += f" ({username})" display_label += f" ({username})"
print(colored(f"{idx}. {display_label}", "cyan")) type_name = etype.value.replace("_", " ").title()
print(colored(f"{idx}. {type_name} - {display_label}", "cyan"))
idx_input = input( idx_input = input(
"Enter index to view details or press Enter to go back: " "Enter index to view details or press Enter to go back: "

View File

@@ -351,7 +351,7 @@ class SearchDialog(toga.Window):
query = self.query_input.value or "" query = self.query_input.value or ""
results = self.main.entries.search_entries(query) results = self.main.entries.search_entries(query)
self.main.entry_source.clear() self.main.entry_source.clear()
for idx, label, username, url, _arch in results: for idx, label, username, url, _arch, _etype in results:
self.main.entry_source.append( self.main.entry_source.append(
{ {
"id": idx, "id": idx,

View File

@@ -8,13 +8,16 @@ from fastapi.testclient import TestClient
sys.path.append(str(Path(__file__).resolve().parents[1])) sys.path.append(str(Path(__file__).resolve().parents[1]))
from seedpass import api from seedpass import api
from seedpass.core.entry_types import EntryType
@pytest.fixture @pytest.fixture
def client(monkeypatch): def client(monkeypatch):
dummy = SimpleNamespace( dummy = SimpleNamespace(
entry_manager=SimpleNamespace( entry_manager=SimpleNamespace(
search_entries=lambda q: [(1, "Site", "user", "url", False)], search_entries=lambda q: [
(1, "Site", "user", "url", False, EntryType.PASSWORD)
],
retrieve_entry=lambda i: {"label": "Site"}, retrieve_entry=lambda i: {"label": "Site"},
add_entry=lambda *a, **k: 1, add_entry=lambda *a, **k: 1,
modify_entry=lambda *a, **k: None, modify_entry=lambda *a, **k: None,

View File

@@ -9,6 +9,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
from seedpass.core.entry_management import EntryManager from seedpass.core.entry_management import EntryManager
from seedpass.core.backup import BackupManager from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager from seedpass.core.config_manager import ConfigManager
from seedpass.core.entry_types import EntryType
def setup_entry_mgr(tmp_path: Path) -> EntryManager: def setup_entry_mgr(tmp_path: Path) -> EntryManager:
@@ -26,7 +27,9 @@ def test_archive_nonpassword_list_search():
idx = em.search_entries("Example")[0][0] idx = em.search_entries("Example")[0][0]
assert em.list_entries() == [(idx, "Example", None, None, False)] assert em.list_entries() == [(idx, "Example", None, None, False)]
assert em.search_entries("Example") == [(idx, "Example", None, None, False)] assert em.search_entries("Example") == [
(idx, "Example", None, None, False, EntryType.TOTP)
]
em.archive_entry(idx) em.archive_entry(idx)
assert em.retrieve_entry(idx)["archived"] is True assert em.retrieve_entry(idx)["archived"] is True
@@ -34,9 +37,13 @@ def test_archive_nonpassword_list_search():
assert em.list_entries(include_archived=True) == [ assert em.list_entries(include_archived=True) == [
(idx, "Example", None, None, True) (idx, "Example", None, None, True)
] ]
assert em.search_entries("Example") == [(idx, "Example", None, None, True)] assert em.search_entries("Example") == [
(idx, "Example", None, None, True, EntryType.TOTP)
]
em.restore_entry(idx) em.restore_entry(idx)
assert em.retrieve_entry(idx)["archived"] is False assert em.retrieve_entry(idx)["archived"] is False
assert em.list_entries() == [(idx, "Example", None, None, False)] assert em.list_entries() == [(idx, "Example", None, None, False)]
assert em.search_entries("Example") == [(idx, "Example", None, None, False)] assert em.search_entries("Example") == [
(idx, "Example", None, None, False, EntryType.TOTP)
]

View File

@@ -14,6 +14,7 @@ from seedpass.core.entry_management import EntryManager
from seedpass.core.backup import BackupManager from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager from seedpass.core.config_manager import ConfigManager
from seedpass.core.manager import PasswordManager, EncryptionMode from seedpass.core.manager import PasswordManager, EncryptionMode
from seedpass.core.entry_types import EntryType
def setup_entry_mgr(tmp_path: Path) -> EntryManager: def setup_entry_mgr(tmp_path: Path) -> EntryManager:
@@ -31,7 +32,7 @@ def test_archive_restore_affects_listing_and_search():
assert em.list_entries() == [(idx, "example.com", "alice", "", False)] assert em.list_entries() == [(idx, "example.com", "alice", "", False)]
assert em.search_entries("example") == [ assert em.search_entries("example") == [
(idx, "example.com", "alice", "", False) (idx, "example.com", "alice", "", False, EntryType.PASSWORD)
] ]
em.archive_entry(idx) em.archive_entry(idx)
@@ -40,13 +41,15 @@ def test_archive_restore_affects_listing_and_search():
assert em.list_entries(include_archived=True) == [ assert em.list_entries(include_archived=True) == [
(idx, "example.com", "alice", "", True) (idx, "example.com", "alice", "", True)
] ]
assert em.search_entries("example") == [(idx, "example.com", "alice", "", True)] assert em.search_entries("example") == [
(idx, "example.com", "alice", "", True, EntryType.PASSWORD)
]
em.restore_entry(idx) em.restore_entry(idx)
assert em.retrieve_entry(idx)["archived"] is False assert em.retrieve_entry(idx)["archived"] is False
assert em.list_entries() == [(idx, "example.com", "alice", "", False)] assert em.list_entries() == [(idx, "example.com", "alice", "", False)]
assert em.search_entries("example") == [ assert em.search_entries("example") == [
(idx, "example.com", "alice", "", False) (idx, "example.com", "alice", "", False, EntryType.PASSWORD)
] ]

View File

@@ -5,6 +5,7 @@ from typer.testing import CliRunner
from seedpass import cli from seedpass import cli
from seedpass.cli import app from seedpass.cli import app
from seedpass.core.entry_types import EntryType
runner = CliRunner() runner = CliRunner()
@@ -34,7 +35,7 @@ def test_cli_entry_add_search_sync(monkeypatch):
def search_entries(q, kinds=None): def search_entries(q, kinds=None):
calls["search"] = (q, kinds) calls["search"] = (q, kinds)
return [(1, "Label", None, None, False)] return [(1, "Label", None, None, False, EntryType.PASSWORD)]
def start_background_vault_sync(): def start_background_vault_sync():
calls["sync"] = True calls["sync"] = True

View File

@@ -17,7 +17,9 @@ class DummyPM:
list_entries=lambda sort_by="index", filter_kind=None, include_archived=False: [ list_entries=lambda sort_by="index", filter_kind=None, include_archived=False: [
(1, "Label", "user", "url", False) (1, "Label", "user", "url", False)
], ],
search_entries=lambda q, kinds=None: [(1, "GitHub", "user", "", False)], search_entries=lambda q, kinds=None: [
(1, "GitHub", "user", "", False, EntryType.PASSWORD)
],
retrieve_entry=lambda idx: {"type": EntryType.PASSWORD.value, "length": 8}, retrieve_entry=lambda idx: {"type": EntryType.PASSWORD.value, "length": 8},
get_totp_code=lambda idx, seed: "123456", get_totp_code=lambda idx, seed: "123456",
add_entry=lambda label, length, username, url, **kwargs: 1, add_entry=lambda label, length, username, url, **kwargs: 1,

View File

@@ -27,7 +27,7 @@ def make_pm(search_results, entry=None, totp_code="123456"):
def test_search_command(monkeypatch, capsys): def test_search_command(monkeypatch, capsys):
pm = make_pm([(0, "Example", "user", "", False)]) pm = make_pm([(0, "Example", "user", "", False, EntryType.PASSWORD)])
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
monkeypatch.setattr(main, "initialize_app", lambda: None) monkeypatch.setattr(main, "initialize_app", lambda: None)
@@ -40,7 +40,7 @@ def test_search_command(monkeypatch, capsys):
def test_get_command(monkeypatch, capsys): def test_get_command(monkeypatch, capsys):
entry = {"type": EntryType.PASSWORD.value, "length": 8} entry = {"type": EntryType.PASSWORD.value, "length": 8}
pm = make_pm([(0, "Example", "user", "", False)], entry=entry) pm = make_pm([(0, "Example", "user", "", False, EntryType.PASSWORD)], entry=entry)
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
monkeypatch.setattr(main, "initialize_app", lambda: None) monkeypatch.setattr(main, "initialize_app", lambda: None)
@@ -53,7 +53,7 @@ def test_get_command(monkeypatch, capsys):
def test_totp_command(monkeypatch, capsys): def test_totp_command(monkeypatch, capsys):
entry = {"type": EntryType.TOTP.value, "period": 30, "index": 0} entry = {"type": EntryType.TOTP.value, "period": 30, "index": 0}
pm = make_pm([(0, "Example", None, None, False)], entry=entry) pm = make_pm([(0, "Example", None, None, False, EntryType.TOTP)], entry=entry)
called = {} called = {}
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
@@ -83,7 +83,10 @@ def test_search_command_no_results(monkeypatch, capsys):
def test_get_command_multiple_matches(monkeypatch, capsys): def test_get_command_multiple_matches(monkeypatch, capsys):
matches = [(0, "Example", "user", "", False), (1, "Ex2", "bob", "", False)] matches = [
(0, "Example", "user", "", False, EntryType.PASSWORD),
(1, "Ex2", "bob", "", False, EntryType.PASSWORD),
]
pm = make_pm(matches) pm = make_pm(matches)
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
@@ -97,7 +100,7 @@ def test_get_command_multiple_matches(monkeypatch, capsys):
def test_get_command_wrong_type(monkeypatch, capsys): def test_get_command_wrong_type(monkeypatch, capsys):
entry = {"type": EntryType.TOTP.value} entry = {"type": EntryType.TOTP.value}
pm = make_pm([(0, "Example", "user", "", False)], entry=entry) pm = make_pm([(0, "Example", None, None, False, EntryType.TOTP)], entry=entry)
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
monkeypatch.setattr(main, "initialize_app", lambda: None) monkeypatch.setattr(main, "initialize_app", lambda: None)
@@ -109,7 +112,10 @@ def test_get_command_wrong_type(monkeypatch, capsys):
def test_totp_command_multiple_matches(monkeypatch, capsys): def test_totp_command_multiple_matches(monkeypatch, capsys):
matches = [(0, "GH", None, None, False), (1, "Git", None, None, False)] matches = [
(0, "GH", None, None, False, EntryType.TOTP),
(1, "Git", None, None, False, EntryType.TOTP),
]
pm = make_pm(matches) pm = make_pm(matches)
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
@@ -123,7 +129,7 @@ def test_totp_command_multiple_matches(monkeypatch, capsys):
def test_totp_command_wrong_type(monkeypatch, capsys): def test_totp_command_wrong_type(monkeypatch, capsys):
entry = {"type": EntryType.PASSWORD.value, "length": 8} entry = {"type": EntryType.PASSWORD.value, "length": 8}
pm = make_pm([(0, "Example", "user", "", False)], entry=entry) pm = make_pm([(0, "Example", "user", "", False, EntryType.PASSWORD)], entry=entry)
monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm) monkeypatch.setattr(main, "PasswordManager", lambda *a, **k: pm)
monkeypatch.setattr(main, "configure_logging", lambda: None) monkeypatch.setattr(main, "configure_logging", lambda: None)
monkeypatch.setattr(main, "initialize_app", lambda: None) monkeypatch.setattr(main, "initialize_app", lambda: None)

View File

@@ -2,6 +2,7 @@ import types
from types import SimpleNamespace from types import SimpleNamespace
from seedpass.core.api import VaultService, EntryService, SyncService, UnlockRequest from seedpass.core.api import VaultService, EntryService, SyncService, UnlockRequest
from seedpass.core.entry_types import EntryType
def test_vault_service_unlock(): def test_vault_service_unlock():
@@ -27,7 +28,7 @@ def test_entry_service_add_entry_and_search():
def search_entries(q, kinds=None): def search_entries(q, kinds=None):
called["search"] = (q, kinds) called["search"] = (q, kinds)
return [(5, "Example", username, url, False)] return [(5, "Example", username, url, False, EntryType.PASSWORD)]
def start_background_vault_sync(): def start_background_vault_sync():
called["sync"] = True called["sync"] = True
@@ -47,7 +48,7 @@ def test_entry_service_add_entry_and_search():
assert called.get("sync") is True assert called.get("sync") is True
results = service.search_entries("ex", kinds=["password"]) results = service.search_entries("ex", kinds=["password"])
assert results == [(5, "Example", username, url, False)] assert results == [(5, "Example", username, url, False, EntryType.PASSWORD)]
assert called["search"] == ("ex", ["password"]) assert called["search"] == ("ex", ["password"])

View File

@@ -46,6 +46,6 @@ def test_search_entries_prompt_for_details(monkeypatch, capsys):
pm.handle_search_entries() pm.handle_search_entries()
out = capsys.readouterr().out out = capsys.readouterr().out
assert "0. Example" in out assert "0. Totp - Example" in out
assert "Label: Example" in out assert "Label: Example" in out
assert "Period: 30s" in out assert "Period: 30s" in out

View File

@@ -28,7 +28,7 @@ def test_search_by_website():
entry_mgr.add_entry("Other.com", 8, "bob") entry_mgr.add_entry("Other.com", 8, "bob")
result = entry_mgr.search_entries("example") result = entry_mgr.search_entries("example")
assert result == [(idx0, "Example.com", "alice", "", False)] assert result == [(idx0, "Example.com", "alice", "", False, EntryType.PASSWORD)]
def test_search_by_username(): def test_search_by_username():
@@ -40,7 +40,7 @@ def test_search_by_username():
idx1 = entry_mgr.add_entry("Test.com", 8, "Bob") idx1 = entry_mgr.add_entry("Test.com", 8, "Bob")
result = entry_mgr.search_entries("bob") result = entry_mgr.search_entries("bob")
assert result == [(idx1, "Test.com", "Bob", "", False)] assert result == [(idx1, "Test.com", "Bob", "", False, EntryType.PASSWORD)]
def test_search_by_url(): def test_search_by_url():
@@ -52,7 +52,9 @@ def test_search_by_url():
entry_mgr.add_entry("Other", 8) entry_mgr.add_entry("Other", 8)
result = entry_mgr.search_entries("login") result = entry_mgr.search_entries("login")
assert result == [(idx, "Example", "", "https://ex.com/login", False)] assert result == [
(idx, "Example", "", "https://ex.com/login", False, EntryType.PASSWORD)
]
def test_search_by_notes_and_totp(): def test_search_by_notes_and_totp():
@@ -117,7 +119,7 @@ def test_search_by_tag_password():
idx = entry_mgr.add_entry("TaggedSite", 8, tags=["work"]) idx = entry_mgr.add_entry("TaggedSite", 8, tags=["work"])
result = entry_mgr.search_entries("work") result = entry_mgr.search_entries("work")
assert result == [(idx, "TaggedSite", "", "", False)] assert result == [(idx, "TaggedSite", "", "", False, EntryType.PASSWORD)]
def test_search_by_tag_totp(): def test_search_by_tag_totp():
@@ -129,7 +131,7 @@ def test_search_by_tag_totp():
idx = entry_mgr.search_entries("OTPAccount")[0][0] idx = entry_mgr.search_entries("OTPAccount")[0][0]
result = entry_mgr.search_entries("mfa") result = entry_mgr.search_entries("mfa")
assert result == [(idx, "OTPAccount", None, None, False)] assert result == [(idx, "OTPAccount", None, None, False, EntryType.TOTP)]
def test_search_with_kind_filter(): def test_search_with_kind_filter():
@@ -147,4 +149,4 @@ def test_search_with_kind_filter():
assert {r[0] for r in all_results} == {idx_pw, idx_totp} assert {r[0] for r in all_results} == {idx_pw, idx_totp}
only_pw = entry_mgr.search_entries("", kinds=[EntryType.PASSWORD.value]) only_pw = entry_mgr.search_entries("", kinds=[EntryType.PASSWORD.value])
assert only_pw == [(idx_pw, "Site", "", "", False)] assert only_pw == [(idx_pw, "Site", "", "", False, EntryType.PASSWORD)]

View File

@@ -9,6 +9,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
from seedpass.core.entry_management import EntryManager from seedpass.core.entry_management import EntryManager
from seedpass.core.backup import BackupManager from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager from seedpass.core.config_manager import ConfigManager
from seedpass.core.entry_types import EntryType
def setup_entry_manager(tmp_path: Path) -> EntryManager: def setup_entry_manager(tmp_path: Path) -> EntryManager:
@@ -29,7 +30,7 @@ def test_tags_persist_on_new_entry():
entry_mgr = setup_entry_manager(tmp_path) entry_mgr = setup_entry_manager(tmp_path)
result = entry_mgr.search_entries("work") result = entry_mgr.search_entries("work")
assert result == [(idx, "Site", "", "", False)] assert result == [(idx, "Site", "", "", False, EntryType.PASSWORD)]
def test_tags_persist_after_modify(): def test_tags_persist_after_modify():
@@ -41,9 +42,11 @@ def test_tags_persist_after_modify():
entry_mgr.modify_entry(idx, tags=["personal"]) entry_mgr.modify_entry(idx, tags=["personal"])
# Ensure tag searchable before reload # Ensure tag searchable before reload
assert entry_mgr.search_entries("personal") == [(idx, "Site", "", "", False)] assert entry_mgr.search_entries("personal") == [
(idx, "Site", "", "", False, EntryType.PASSWORD)
]
# Reinitialize to simulate application restart # Reinitialize to simulate application restart
entry_mgr = setup_entry_manager(tmp_path) entry_mgr = setup_entry_manager(tmp_path)
result = entry_mgr.search_entries("personal") result = entry_mgr.search_entries("personal")
assert result == [(idx, "Site", "", "", False)] assert result == [(idx, "Site", "", "", False, EntryType.PASSWORD)]

View File

@@ -34,19 +34,21 @@ def test_entry_list(monkeypatch):
def test_entry_search(monkeypatch): def test_entry_search(monkeypatch):
pm = SimpleNamespace( pm = SimpleNamespace(
entry_manager=SimpleNamespace( entry_manager=SimpleNamespace(
search_entries=lambda q, kinds=None: [(1, "L", None, None, False)] search_entries=lambda q, kinds=None: [
(1, "L", None, None, False, EntryType.PASSWORD)
]
), ),
select_fingerprint=lambda fp: None, select_fingerprint=lambda fp: None,
) )
monkeypatch.setattr(cli, "PasswordManager", lambda: pm) monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
result = runner.invoke(app, ["entry", "search", "l"]) result = runner.invoke(app, ["entry", "search", "l"])
assert result.exit_code == 0 assert result.exit_code == 0
assert "1: L" in result.stdout assert "Password - L" in result.stdout
def test_entry_get_password(monkeypatch): def test_entry_get_password(monkeypatch):
def search(q, kinds=None): def search(q, kinds=None):
return [(2, "Example", "", "", False)] return [(2, "Example", "", "", False, EntryType.PASSWORD)]
entry = {"type": EntryType.PASSWORD.value, "length": 8} entry = {"type": EntryType.PASSWORD.value, "length": 8}
pm = SimpleNamespace( pm = SimpleNamespace(