Merge pull request #790 from PR0M3TH3AN/codex/improve-nostr-client-reliability-and-error-handling

Guard missing Nostr client in background sync
This commit is contained in:
thePR0M3TH3AN
2025-08-06 21:40:19 -04:00
committed by GitHub
4 changed files with 46 additions and 1 deletions

View File

@@ -1401,6 +1401,8 @@ class PasswordManager:
async def sync_index_from_nostr_async(self) -> None: async def sync_index_from_nostr_async(self) -> None:
"""Always fetch the latest vault data from Nostr and update the local index.""" """Always fetch the latest vault data from Nostr and update the local index."""
if not getattr(self, "nostr_client", None):
return
start = time.perf_counter() start = time.perf_counter()
try: try:
if getattr(self, "current_fingerprint", None): if getattr(self, "current_fingerprint", None):
@@ -1518,6 +1520,8 @@ class PasswordManager:
"""Launch a thread to synchronize the vault without blocking the UI.""" """Launch a thread to synchronize the vault without blocking the UI."""
if getattr(self, "offline_mode", False): if getattr(self, "offline_mode", False):
return return
if getattr(self, "nostr_client", None) is None:
return
if getattr(self, "_sync_task", None) and not getattr( if getattr(self, "_sync_task", None) and not getattr(
self._sync_task, "done", True self._sync_task, "done", True
): ):
@@ -1529,7 +1533,7 @@ class PasswordManager:
await self.sync_index_from_nostr_async() await self.sync_index_from_nostr_async()
except Exception as exc: except Exception as exc:
logger.warning(f"Background sync failed: {exc}") logger.warning(f"Background sync failed: {exc}")
if hasattr(self, "error_queue"): if hasattr(self, "error_queue") and getattr(self, "nostr_client", None):
self.error_queue.put(exc) self.error_queue.put(exc)
try: try:
@@ -1615,6 +1619,9 @@ class PasswordManager:
local index file was written. Returns ``False`` otherwise. The local local index file was written. Returns ``False`` otherwise. The local
index file is not created on failure. index file is not created on failure.
""" """
if not getattr(self, "nostr_client", None):
return False
index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc" index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc"
if index_file.exists(): if index_file.exists():
return True return True
@@ -1680,6 +1687,8 @@ class PasswordManager:
asyncio.run(self.sync_index_from_nostr_if_missing_async()) asyncio.run(self.sync_index_from_nostr_if_missing_async())
async def sync_index_from_nostr_if_missing_async(self) -> None: async def sync_index_from_nostr_if_missing_async(self) -> None:
if not getattr(self, "nostr_client", None):
return
success = await self.attempt_initial_sync_async() success = await self.attempt_initial_sync_async()
if not success: if not success:
self.vault.save_index({"schema_version": LATEST_VERSION, "entries": {}}) self.vault.save_index({"schema_version": LATEST_VERSION, "entries": {}})

View File

@@ -12,6 +12,7 @@ def _make_pm():
pm.notify = lambda msg, level="INFO": pm.notifications.put( pm.notify = lambda msg, level="INFO": pm.notifications.put(
manager_module.Notification(msg, level) manager_module.Notification(msg, level)
) )
pm.nostr_client = object()
return pm return pm

View File

@@ -1,10 +1,14 @@
import sys import sys
import importlib import importlib
import queue
import time
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
import pytest
from helpers import create_vault, TEST_SEED, TEST_PASSWORD from helpers import create_vault, TEST_SEED, TEST_PASSWORD
sys.path.append(str(Path(__file__).resolve().parents[1])) sys.path.append(str(Path(__file__).resolve().parents[1]))
@@ -12,6 +16,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
import main import main
from nostr.client import DEFAULT_RELAYS from nostr.client import DEFAULT_RELAYS
from seedpass.core.config_manager import ConfigManager from seedpass.core.config_manager import ConfigManager
from seedpass.core.manager import Notification, PasswordManager
from seedpass.core.vault import Vault from seedpass.core.vault import Vault
from utils.fingerprint_manager import FingerprintManager from utils.fingerprint_manager import FingerprintManager
@@ -137,3 +142,31 @@ def test_settings_menu_change_password_incorrect(monkeypatch, capsys):
out = capsys.readouterr().out out = capsys.readouterr().out
assert "Incorrect password" in out assert "Incorrect password" in out
def test_settings_menu_without_nostr_client(monkeypatch):
pm = PasswordManager.__new__(PasswordManager)
pm.offline_mode = False
pm.nostr_client = None
pm.notifications = queue.Queue()
pm.error_queue = queue.Queue()
pm.notify = lambda msg, level="INFO": pm.notifications.put(Notification(msg, level))
pm.is_dirty = False
pm.last_update = time.time()
pm.last_activity = time.time()
pm.update_activity = lambda: None
pm.lock_vault = lambda: None
pm.unlock_vault = lambda: None
pm.start_background_relay_check = lambda: None
pm.poll_background_errors = PasswordManager.poll_background_errors.__get__(pm)
pm.display_stats = lambda: None
inputs = iter(["7", ""])
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
monkeypatch.setattr("builtins.input", lambda *_: "")
with pytest.raises(SystemExit):
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
assert pm.error_queue.empty()
assert pm.notifications.empty()

View File

@@ -17,6 +17,7 @@ def test_unlock_triggers_sync(monkeypatch, tmp_path):
pm.setup_encryption_manager = lambda *a, **k: None pm.setup_encryption_manager = lambda *a, **k: None
pm.initialize_bip85 = lambda: None pm.initialize_bip85 = lambda: None
pm.initialize_managers = lambda: None pm.initialize_managers = lambda: None
pm.nostr_client = object()
called = {"sync": False} called = {"sync": False}
async def fake_sync(self): async def fake_sync(self):
@@ -62,6 +63,7 @@ def test_quick_unlock_background_sync(monkeypatch, tmp_path):
def test_start_background_sync_running_loop(monkeypatch): def test_start_background_sync_running_loop(monkeypatch):
pm = PasswordManager.__new__(PasswordManager) pm = PasswordManager.__new__(PasswordManager)
pm.offline_mode = False pm.offline_mode = False
pm.nostr_client = object()
called = {"init": False, "sync": False} called = {"init": False, "sync": False}
async def fake_attempt(self): async def fake_attempt(self):