diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index a153cd6..cabf9d4 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -1401,6 +1401,8 @@ class PasswordManager: async def sync_index_from_nostr_async(self) -> None: """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() try: if getattr(self, "current_fingerprint", None): @@ -1518,6 +1520,8 @@ class PasswordManager: """Launch a thread to synchronize the vault without blocking the UI.""" if getattr(self, "offline_mode", False): return + if getattr(self, "nostr_client", None) is None: + return if getattr(self, "_sync_task", None) and not getattr( self._sync_task, "done", True ): @@ -1529,7 +1533,7 @@ class PasswordManager: await self.sync_index_from_nostr_async() except Exception as 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) try: @@ -1615,6 +1619,9 @@ class PasswordManager: local index file was written. Returns ``False`` otherwise. The local 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" if index_file.exists(): return True @@ -1680,6 +1687,8 @@ class PasswordManager: asyncio.run(self.sync_index_from_nostr_if_missing_async()) 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() if not success: self.vault.save_index({"schema_version": LATEST_VERSION, "entries": {}}) diff --git a/src/tests/test_background_error_reporting.py b/src/tests/test_background_error_reporting.py index 2f71aad..a6c6ce4 100644 --- a/src/tests/test_background_error_reporting.py +++ b/src/tests/test_background_error_reporting.py @@ -12,6 +12,7 @@ def _make_pm(): pm.notify = lambda msg, level="INFO": pm.notifications.put( manager_module.Notification(msg, level) ) + pm.nostr_client = object() return pm diff --git a/src/tests/test_settings_menu.py b/src/tests/test_settings_menu.py index a0b9fba..734a338 100644 --- a/src/tests/test_settings_menu.py +++ b/src/tests/test_settings_menu.py @@ -1,10 +1,14 @@ import sys import importlib +import queue +import time from pathlib import Path from tempfile import TemporaryDirectory from types import SimpleNamespace from unittest.mock import patch +import pytest + from helpers import create_vault, TEST_SEED, TEST_PASSWORD sys.path.append(str(Path(__file__).resolve().parents[1])) @@ -12,6 +16,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) import main from nostr.client import DEFAULT_RELAYS from seedpass.core.config_manager import ConfigManager +from seedpass.core.manager import Notification, PasswordManager from seedpass.core.vault import Vault from utils.fingerprint_manager import FingerprintManager @@ -137,3 +142,31 @@ def test_settings_menu_change_password_incorrect(monkeypatch, capsys): out = capsys.readouterr().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() diff --git a/src/tests/test_unlock_sync.py b/src/tests/test_unlock_sync.py index 58a62cb..96b9eb7 100644 --- a/src/tests/test_unlock_sync.py +++ b/src/tests/test_unlock_sync.py @@ -17,6 +17,7 @@ def test_unlock_triggers_sync(monkeypatch, tmp_path): pm.setup_encryption_manager = lambda *a, **k: None pm.initialize_bip85 = lambda: None pm.initialize_managers = lambda: None + pm.nostr_client = object() called = {"sync": False} 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): pm = PasswordManager.__new__(PasswordManager) pm.offline_mode = False + pm.nostr_client = object() called = {"init": False, "sync": False} async def fake_attempt(self):