diff --git a/src/seedpass_gui/app.py b/src/seedpass_gui/app.py index 88ea909..b3041f5 100644 --- a/src/seedpass_gui/app.py +++ b/src/seedpass_gui/app.py @@ -102,6 +102,7 @@ class MainWindow(toga.Window): search_button = toga.Button("Search", on_press=self.search_entries) relay_button = toga.Button("Relays", on_press=self.manage_relays) totp_button = toga.Button("TOTP", on_press=self.show_totp_codes) + sync_button = toga.Button("Sync", on_press=self.start_vault_sync) button_box = toga.Box(style=Pack(direction=ROW, padding_top=5)) button_box.add(add_button) @@ -109,6 +110,7 @@ class MainWindow(toga.Window): button_box.add(search_button) button_box.add(relay_button) button_box.add(totp_button) + button_box.add(sync_button) self.status = toga.Label("Last sync: never", style=Pack(padding_top=5)) @@ -168,6 +170,14 @@ class MainWindow(toga.Window): win = TotpViewerWindow(self.controller, self.entries) win.show() + def start_vault_sync(self, widget: toga.Widget | None = None) -> None: + """Schedule a background vault synchronization.""" + + async def _runner() -> None: + self.nostr.start_background_vault_sync() + + self.controller.loop.create_task(_runner()) + # --- PubSub callbacks ------------------------------------------------- def sync_started(self, *args: object, **kwargs: object) -> None: self.status.text = "Syncing..." @@ -307,6 +317,8 @@ class EntryDialog(toga.Window): break self.close() + # schedule vault sync after saving + getattr(self.main, "start_vault_sync", lambda *_: None)() class SearchDialog(toga.Window): diff --git a/src/tests/test_gui_sync.py b/src/tests/test_gui_sync.py new file mode 100644 index 0000000..6ef1ed2 --- /dev/null +++ b/src/tests/test_gui_sync.py @@ -0,0 +1,77 @@ +import os +import types +import asyncio +import toga +import pytest + +from seedpass.core.pubsub import bus +from seedpass_gui.app import MainWindow + + +class DummyEntries: + def list_entries(self, sort_by="index", filter_kind=None, include_archived=False): + return [] + + def search_entries(self, q): + return [] + + +class DummyNostr: + def __init__(self): + self.called = False + + def start_background_vault_sync(self): + self.called = True + + def list_relays(self): + return [] + + +class DummyController: + def __init__(self, loop): + self.loop = loop + self.lock_window = types.SimpleNamespace(show=lambda: None) + self.main_window = None + self.vault_service = None + self.entry_service = None + self.nostr_service = None + + +@pytest.fixture(autouse=True) +def set_backend(): + os.environ["TOGA_BACKEND"] = "toga_dummy" + asyncio.set_event_loop(asyncio.new_event_loop()) + + +def test_start_vault_sync_schedules_task(): + toga.App("T", "o") + + tasks = [] + + def create_task(coro): + tasks.append(coro) + + loop = types.SimpleNamespace(create_task=create_task) + ctrl = DummyController(loop) + nostr = DummyNostr() + win = MainWindow(ctrl, None, DummyEntries(), nostr) + + win.start_vault_sync() + assert tasks + asyncio.get_event_loop().run_until_complete(tasks[0]) + assert nostr.called + win.cleanup() + + +def test_status_updates_on_bus_events(): + toga.App("T2", "o2") + loop = types.SimpleNamespace(create_task=lambda c: None) + ctrl = DummyController(loop) + nostr = DummyNostr() + win = MainWindow(ctrl, None, DummyEntries(), nostr) + + bus.publish("sync_started") + assert win.status.text == "Syncing..." + bus.publish("sync_finished") + assert "Last sync:" in win.status.text + win.cleanup()