mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-06 04:42:35 +00:00
feat(tui-v3): implement core functional parity (Phase A/B)
- Implement secure data parity (Reveal/QR/Copy) for SSH, PGP, Nostr, Key-Value, and Documents. - Update derive_pgp_key and API to support PGP Public Key retrieval and copying. - Implement managed account loading/exiting (ml/mx) with reactive profile updates. - Integrate semantic search (keyword/hybrid/semantic) into the entry grid. - Create AddEntryScreen (unified wizard), SeedPlusScreen (BIP-85), and DocumentEditScreen. - Add grid filter presets (secrets, docs, keys, 2fa) and archive view toggle. - Update ActionBar and GridMetrics to be dynamic and context-aware. - Add comprehensive v3 parity tests and update existing core tests for PGP tuple changes.
This commit is contained in:
1 parent
d3920ccc4c
commit
c822eeb602
17 files changed
+855
-89
No files matched your search
@@ -337,12 +337,12 @@ class TestEntryService:
|
||||
assert result == ("priv", "pub")
|
||||
|
||||
def test_get_pgp_key(self, service, mock_manager):
|
||||
mock_manager.entry_manager.get_pgp_key.return_value = ("priv", "fp")
|
||||
mock_manager.entry_manager.get_pgp_key.return_value = ("priv", "pub", "fp")
|
||||
result = service.get_pgp_key(4)
|
||||
mock_manager.entry_manager.get_pgp_key.assert_called_once_with(
|
||||
4, mock_manager.parent_seed
|
||||
)
|
||||
assert result == ("priv", "fp")
|
||||
assert result == ("priv", "pub", "fp")
|
||||
|
||||
def test_get_nostr_key_pair(self, service, mock_manager):
|
||||
mock_manager.entry_manager.get_nostr_key_pair.return_value = ("npub", "nsec")
|
||||
|
||||
@@ -227,7 +227,7 @@ def test_show_pgp_entry_details(monkeypatch, capsys):
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, entry_mgr = _setup_manager(tmp_path)
|
||||
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
|
||||
_k, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
_k, _pub, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
|
||||
called = _detail_common(monkeypatch, pm)
|
||||
|
||||
@@ -335,7 +335,7 @@ def test_show_entry_details_sensitive(monkeypatch, capsys, entry_type):
|
||||
extra = pub
|
||||
elif entry_type == "pgp":
|
||||
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, user_id="test")
|
||||
priv, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
priv, pub, fp = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
expected = priv
|
||||
extra = fp
|
||||
elif entry_type == "nostr":
|
||||
|
||||
@@ -25,8 +25,8 @@ def test_pgp_key_determinism():
|
||||
idx = entry_mgr.add_pgp_key(
|
||||
"pgp", TEST_SEED, key_type="ed25519", user_id="Test"
|
||||
)
|
||||
key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
key1, pub1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
key2, pub2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
|
||||
assert fp1 == fp2
|
||||
assert key1 == key2
|
||||
@@ -55,8 +55,8 @@ def test_pgp_rsa_key_determinism():
|
||||
entry_mgr = EntryManager(vault, backup_mgr)
|
||||
|
||||
idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, key_type="rsa", user_id="Test")
|
||||
key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
key1, pub1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
key2, pub2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED)
|
||||
|
||||
assert fp1 == fp2
|
||||
assert key1 == key2
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from textual.widgets import DataTable
|
||||
from seedpass.tui_v3.app import SeedPassTuiV3
|
||||
|
||||
class MockEntryService:
|
||||
def __init__(self):
|
||||
self.entries = {
|
||||
1: {"id": 1, "label": "test-pwd", "kind": "password", "username": "user1", "length": 16},
|
||||
2: {"id": 2, "label": "test-ssh", "kind": "ssh", "index": 0},
|
||||
3: {"id": 3, "label": "test-pgp", "kind": "pgp", "index": 0},
|
||||
4: {"id": 4, "label": "test-nostr", "kind": "nostr", "index": 0},
|
||||
5: {"id": 5, "label": "test-doc", "kind": "document", "content": "hello world"},
|
||||
6: {"id": 6, "label": "test-seed", "kind": "seed", "index": 0},
|
||||
}
|
||||
def search_entries(self, query="", kinds=None, include_archived=False, archived_only=False):
|
||||
results = []
|
||||
for e in self.entries.values():
|
||||
kind = e["kind"]
|
||||
if kinds and kind not in kinds: continue
|
||||
is_arch = e.get("archived", False)
|
||||
if archived_only and not is_arch: continue
|
||||
if not include_archived and is_arch: continue
|
||||
results.append((e["id"], e["label"], e.get("username"), e.get("url"), is_arch, MagicMock(value=kind)))
|
||||
return results
|
||||
|
||||
def retrieve_entry(self, eid):
|
||||
return self.entries.get(eid)
|
||||
def generate_password(self, length, eid):
|
||||
return "gen-pwd"
|
||||
def get_ssh_key_pair(self, eid):
|
||||
return "priv", "pub"
|
||||
def get_pgp_key(self, eid):
|
||||
return "priv", "pub", "fp"
|
||||
def get_nostr_key_pair(self, eid):
|
||||
return "npub", "nsec"
|
||||
def get_seed_phrase(self, eid, parent):
|
||||
return "word1 word2"
|
||||
def copy_to_clipboard(self, val):
|
||||
return True
|
||||
def archive_entry(self, eid):
|
||||
self.entries[eid]["archived"] = True
|
||||
def restore_entry(self, eid):
|
||||
self.entries[eid]["archived"] = False
|
||||
def load_managed_account(self, eid):
|
||||
pass
|
||||
def exit_managed_account(self):
|
||||
pass
|
||||
def add_entry(self, label, username=None, url=None, length=16, tags=None):
|
||||
eid = max(self.entries.keys()) + 1
|
||||
self.entries[eid] = {"id": eid, "label": label, "kind": "password", "username": username, "tags": tags}
|
||||
return eid
|
||||
def add_totp(self, label, secret, username=None, tags=None):
|
||||
eid = max(self.entries.keys()) + 1
|
||||
self.entries[eid] = {"id": eid, "label": label, "kind": "totp", "username": username, "tags": tags}
|
||||
return eid
|
||||
def add_seed(self, label, index=0, tags=None):
|
||||
eid = max(self.entries.keys()) + 1
|
||||
self.entries[eid] = {"id": eid, "label": label, "kind": "seed", "index": index, "tags": tags}
|
||||
return eid
|
||||
def add_managed_account(self, label, index=0, tags=None):
|
||||
eid = max(self.entries.keys()) + 1
|
||||
self.entries[eid] = {"id": eid, "label": label, "kind": "managed_account", "index": index, "tags": tags}
|
||||
return eid
|
||||
|
||||
class MockVaultService:
|
||||
def __init__(self):
|
||||
self._manager = MagicMock()
|
||||
self._manager.current_fingerprint = "parent-fp"
|
||||
self._manager.profile_stack = []
|
||||
def stats(self):
|
||||
return {"total_entries": len(self._manager.entries) if hasattr(self._manager, "entries") else 6}
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_reveal_copy_parity():
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
async with app.run_test() as pilot:
|
||||
app.selected_entry_id = 1
|
||||
app.action_reveal_selected(confirm=True)
|
||||
payload = app._resolve_sensitive_payload()
|
||||
assert payload[3] == "gen-pwd"
|
||||
app.selected_entry_id = 4
|
||||
payload = app._resolve_sensitive_payload()
|
||||
assert payload[3] == "nsec"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_archive_restore():
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
async with app.run_test() as pilot:
|
||||
app.selected_entry_id = 1
|
||||
app.action_toggle_archive()
|
||||
assert app.services["entry"].entries[1].get("archived") is True
|
||||
app.action_toggle_archive()
|
||||
assert app.services["entry"].entries[1].get("archived") is False
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_add_entry():
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
async with app.run_test() as pilot:
|
||||
app.action_add_entry()
|
||||
await pilot.pause()
|
||||
screen = app.screen
|
||||
from textual.widgets import Input
|
||||
screen.query_one("#entry-label", Input).value = "New Password"
|
||||
screen.query_one("#entry-username", Input).value = "[email protected]"
|
||||
await pilot.press("ctrl+s")
|
||||
await pilot.pause()
|
||||
assert any(e["label"] == "New Password" for e in app.services["entry"].entries.values())
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_v3_filters():
|
||||
app = SeedPassTuiV3(
|
||||
entry_service_factory=lambda: MockEntryService(),
|
||||
vault_service_factory=lambda: MockVaultService(),
|
||||
)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
table = app.screen.query_one("#entry-data-table", DataTable)
|
||||
initial_count = table.row_count
|
||||
app.action_set_kind_filter("2fa")
|
||||
await pilot.pause()
|
||||
assert table.row_count == 0 # No TOTP in mock
|
||||
app.action_set_kind_filter("keys")
|
||||
await pilot.pause()
|
||||
assert table.row_count > 0
|
||||
Reference in new issue
Block a user