Add Nostr restore guidance and tests

This commit is contained in:
thePR0M3TH3AN
2025-07-31 18:28:48 -04:00
parent f58ed03e6f
commit e3bd669668
2 changed files with 84 additions and 7 deletions

View File

@@ -105,6 +105,12 @@ from .stats_manager import StatsManager
logger = logging.getLogger(__name__)
def calculate_profile_id(seed: str) -> str:
"""Return the fingerprint identifier for ``seed``."""
fp = generate_fingerprint(seed)
return fp or ""
@dataclass
class Notification:
"""Simple message container for UI notifications."""
@@ -828,13 +834,8 @@ class PasswordManager:
elif choice == "3":
self.generate_new_seed()
elif choice == "4":
fp = self.setup_existing_seed()
if fp:
success = self.attempt_initial_sync()
if success:
print(colored("Vault restored from Nostr.", "green"))
else:
print(colored("Failed to download vault from Nostr.", "red"))
seed_phrase = masked_input("Enter your 12-word BIP-85 seed: ").strip()
self.restore_from_nostr_with_guidance(seed_phrase)
return
else:
print(colored("Invalid choice. Exiting.", "red"))
@@ -1412,6 +1413,37 @@ class PasswordManager:
except Exception as exc: # pragma: no cover - best effort
logger.warning(f"Unable to publish fresh database: {exc}")
def check_nostr_backup_exists(self, profile_id: str) -> bool:
"""Return ``True`` if a snapshot exists on Nostr for ``profile_id``."""
if not self.nostr_client or getattr(self, "offline_mode", False):
return False
previous = self.nostr_client.fingerprint
self.nostr_client.fingerprint = profile_id
try:
result = asyncio.run(self.nostr_client.fetch_latest_snapshot())
return result is not None
finally:
self.nostr_client.fingerprint = previous
def restore_from_nostr_with_guidance(self, seed_phrase: str) -> None:
"""Restore a profile from Nostr, warning if no backup exists."""
profile_id = calculate_profile_id(seed_phrase)
have_backup = self.check_nostr_backup_exists(profile_id)
if not have_backup:
print(colored("No Nostr backup found for this seed profile.", "yellow"))
if not confirm_action("Continue with an empty database? (Y/N): "):
return
fp = self._finalize_existing_seed(seed_phrase)
if not fp:
return
success = self.attempt_initial_sync()
if success:
print(colored("Vault restored from Nostr.", "green"))
elif have_backup:
print(colored("Failed to download vault from Nostr.", "red"))
def handle_add_password(self) -> None:
try:
fp, parent_fp, child_fp = self.header_fingerprint_args

View File

@@ -72,3 +72,48 @@ def test_handle_new_seed_setup_restore_from_nostr(monkeypatch, tmp_path, capsys)
assert "Vault restored from Nostr" in out
labels = [e[1] for e in pm_new.entry_manager.list_entries()]
assert labels == ["site1"]
async def _no_snapshot():
return None
def test_restore_from_nostr_warns(monkeypatch, tmp_path, capsys):
client, _relay = dummy_nostr_client.__wrapped__(tmp_path / "srv", monkeypatch)
monkeypatch.setattr(client, "fetch_latest_snapshot", _no_snapshot)
pm = PasswordManager.__new__(PasswordManager)
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.nostr_client = client
monkeypatch.setattr("seedpass.core.manager.confirm_action", lambda *_: True)
monkeypatch.setattr(pm, "_finalize_existing_seed", lambda *_a, **_k: "fp")
monkeypatch.setattr(pm, "attempt_initial_sync", lambda: False)
pm.restore_from_nostr_with_guidance(TEST_SEED)
out = capsys.readouterr().out
assert "No Nostr backup" in out
def test_restore_from_nostr_abort(monkeypatch, tmp_path, capsys):
client, _relay = dummy_nostr_client.__wrapped__(tmp_path / "srv", monkeypatch)
monkeypatch.setattr(client, "fetch_latest_snapshot", _no_snapshot)
pm = PasswordManager.__new__(PasswordManager)
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.nostr_client = client
pm.vault = None
called = {"finalize": 0}
def finalize(*_a, **_k):
called["finalize"] += 1
monkeypatch.setattr("seedpass.core.manager.confirm_action", lambda *_: False)
monkeypatch.setattr(pm, "_finalize_existing_seed", finalize)
pm.restore_from_nostr_with_guidance(TEST_SEED)
out = capsys.readouterr().out
assert "No Nostr backup" in out
assert called["finalize"] == 0
assert pm.vault is None