import importlib.util import logging import sys from pathlib import Path from types import SimpleNamespace import pytest from httpx import ASGITransport, AsyncClient import bcrypt sys.path.append(str(Path(__file__).resolve().parents[1])) from seedpass import api from seedpass.core.entry_types import EntryType from helpers import ( create_vault, TEST_PASSWORD, TEST_SEED, DummyBuilder, DummyFilter, DummyTag, DummyTimestamp, DummyEventId, DummyRelayClient, ) from seedpass.core.backup import BackupManager from seedpass.core.config_manager import ConfigManager from seedpass.core.entry_management import EntryManager from seedpass.core.manager import EncryptionMode, PasswordManager from seedpass.core.encryption import EncryptionManager as EncMgr from seedpass.core.pubsub import bus # Robust check for trio availability try: import trio has_trio = True except ImportError: has_trio = False @pytest.fixture(params=["asyncio"] + (["trio"] if has_trio else [])) def anyio_backend(request): return request.param @pytest.fixture(autouse=True) def mute_logging(): logging.getLogger().setLevel(logging.WARNING) @pytest.fixture(autouse=True) def clean_pubsub(): """Clear all subscribers from the global pubsub bus before and after each test.""" bus._subscribers.clear() yield bus._subscribers.clear() def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( "--stress", action="store_true", default=False, help="run stress tests", ) parser.addoption( "--desktop", action="store_true", default=False, help="run desktop-only tests", ) parser.addoption( "--max-entries", type=int, default=None, help="maximum entries for nostr index size test", ) parser.addoption( "--determinism-only", action="store_true", default=False, help="run only determinism regression tests", ) def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line("markers", "stress: long running stress tests") config.addinivalue_line("markers", "desktop: desktop only tests") config.addinivalue_line( "markers", "determinism: locks deterministic derivation behavior" ) def pytest_collection_modifyitems( config: pytest.Config, items: list[pytest.Item] ) -> None: if config.getoption("--determinism-only"): skip_non_determinism = pytest.mark.skip(reason="running determinism-only suite") for item in items: if "determinism" not in item.keywords: item.add_marker(skip_non_determinism) return if not config.getoption("--stress"): skip_stress = pytest.mark.skip(reason="need --stress option to run") for item in items: if "stress" in item.keywords: item.add_marker(skip_stress) if not config.getoption("--desktop"): skip_desktop = pytest.mark.skip(reason="need --desktop option to run") for item in items: if "desktop" in item.keywords: item.add_marker(skip_desktop) @pytest.fixture def vault(tmp_path): vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) return vault @pytest.fixture def password_manager(vault, tmp_path): cfg_mgr = ConfigManager(vault, tmp_path) backup_mgr = BackupManager(tmp_path, cfg_mgr) entry_mgr = EntryManager(vault, backup_mgr) pm = PasswordManager.__new__(PasswordManager) pm.encryption_mode = EncryptionMode.SEED_ONLY pm.encryption_manager = vault.encryption_manager pm.vault = vault pm.entry_manager = entry_mgr pm.backup_manager = backup_mgr pm.parent_seed = TEST_SEED pm.nostr_client = None pm.fingerprint_dir = tmp_path pm.is_dirty = False pm.secret_mode_enabled = False return pm @pytest.fixture async def client(monkeypatch): dummy = SimpleNamespace( entry_manager=SimpleNamespace( search_entries=lambda q: [ (1, "Site", "user", "url", False, EntryType.PASSWORD) ], retrieve_entry=lambda i: {"label": "Site"}, add_entry=lambda *a, **k: 1, modify_entry=lambda *a, **k: None, archive_entry=lambda i: None, restore_entry=lambda i: None, ), config_manager=SimpleNamespace( load_config=lambda require_pin=False: {"k": "v"}, set_pin=lambda v: None, set_password_hash=lambda v: None, set_relays=lambda v, require_pin=False: None, set_inactivity_timeout=lambda v: None, set_additional_backup_path=lambda v: None, set_secret_mode_enabled=lambda v: None, set_clipboard_clear_delay=lambda v: None, set_quick_unlock=lambda v: None, ), fingerprint_manager=SimpleNamespace(list_fingerprints=lambda: ["fp"]), nostr_client=SimpleNamespace( key_manager=SimpleNamespace(get_npub=lambda: "np") ), verify_password=lambda pw: True, ) monkeypatch.setattr(api, "PasswordManager", lambda: dummy) monkeypatch.setenv("SEEDPASS_CORS_ORIGINS", "http://example.com") token = api.start_server() transport = ASGITransport(app=api.app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac, token @pytest.fixture def make_dummy_nostr_client(monkeypatch): """Factory to return a NostrClient wired to a DummyRelayClient.""" from cryptography.fernet import Fernet from nostr.client import NostrClient def _make(base_path): relay = DummyRelayClient() monkeypatch.setattr("nostr.client.Client", lambda signer: relay) monkeypatch.setattr("nostr.client.EventBuilder", DummyBuilder) monkeypatch.setattr("nostr.client.Filter", DummyFilter) monkeypatch.setattr("nostr.client.Tag", DummyTag) monkeypatch.setattr("nostr.client.Timestamp", DummyTimestamp) monkeypatch.setattr("nostr.client.EventId", DummyEventId) from nostr.backup_models import KIND_DELTA as KD monkeypatch.setattr("nostr.client.KIND_DELTA", KD, raising=False) monkeypatch.setattr(NostrClient, "initialize_client_pool", lambda self: None) enc_mgr = EncMgr(Fernet.generate_key(), base_path) class DummyKeys: def private_key_hex(self): return "1" * 64 def public_key_hex(self): return "2" * 64 class DummyKeyManager: def __init__(self, *a, **k): self.keys = DummyKeys() with pytest.MonkeyPatch().context() as mp: mp.setattr("nostr.client.KeyManager", DummyKeyManager) mp.setattr(enc_mgr, "decrypt_parent_seed", lambda: TEST_SEED) client = NostrClient(enc_mgr, "fp") return client, relay return _make @pytest.fixture def dummy_nostr_client(make_dummy_nostr_client, tmp_path): return make_dummy_nostr_client(tmp_path)