mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Include entry type in search results
This commit is contained in:
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
27
src/main.py
27
src/main.py
@@ -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:
|
||||||
|
@@ -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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -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: "
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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)
|
||||||
|
]
|
||||||
|
@@ -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)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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)
|
||||||
|
@@ -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"])
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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)]
|
||||||
|
@@ -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)]
|
||||||
|
@@ -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(
|
||||||
|
Reference in New Issue
Block a user