diff --git a/src/seedpass/cli/__init__.py b/src/seedpass/cli/__init__.py index 3b76f6ba..3babfcd0 100644 --- a/src/seedpass/cli/__init__.py +++ b/src/seedpass/cli/__init__.py @@ -28,14 +28,12 @@ no_clipboard_option = typer.Option( False, "--no-clipboard", help="Disable clipboard support and print secrets instead", - is_flag=True, ) deterministic_totp_option = typer.Option( False, "--deterministic-totp", help="Derive TOTP secrets deterministically", - is_flag=True, ) # Sub command groups diff --git a/src/seedpass/cli/entry.py b/src/seedpass/cli/entry.py index be5062f8..1474cdfb 100644 --- a/src/seedpass/cli/entry.py +++ b/src/seedpass/cli/entry.py @@ -123,7 +123,7 @@ def entry_add( username: Optional[str] = typer.Option(None, "--username"), url: Optional[str] = typer.Option(None, "--url"), no_special: bool = typer.Option( - False, "--no-special", help="Exclude special characters", is_flag=True + False, "--no-special", help="Exclude special characters" ), allowed_special_chars: Optional[str] = typer.Option( None, "--allowed-special-chars", help="Explicit set of special characters" @@ -137,7 +137,6 @@ def entry_add( False, "--exclude-ambiguous", help="Exclude ambiguous characters", - is_flag=True, ), min_uppercase: Optional[int] = typer.Option(None, "--min-uppercase"), min_lowercase: Optional[int] = typer.Option(None, "--min-lowercase"), diff --git a/src/seedpass/cli/util.py b/src/seedpass/cli/util.py index f078d63c..77314944 100644 --- a/src/seedpass/cli/util.py +++ b/src/seedpass/cli/util.py @@ -14,7 +14,7 @@ def generate_password( ctx: typer.Context, length: int = 24, no_special: bool = typer.Option( - False, "--no-special", help="Exclude special characters", is_flag=True + False, "--no-special", help="Exclude special characters" ), allowed_special_chars: Optional[str] = typer.Option( None, "--allowed-special-chars", help="Explicit set of special characters" @@ -28,7 +28,6 @@ def generate_password( False, "--exclude-ambiguous", help="Exclude ambiguous characters", - is_flag=True, ), min_uppercase: Optional[int] = typer.Option(None, "--min-uppercase"), min_lowercase: Optional[int] = typer.Option(None, "--min-lowercase"), diff --git a/src/seedpass/core/entry_management.py b/src/seedpass/core/entry_management.py index c3b003ad..ca4174bd 100644 --- a/src/seedpass/core/entry_management.py +++ b/src/seedpass/core/entry_management.py @@ -1336,7 +1336,7 @@ class EntryManager: atomic_write(checksum_path, lambda f: f.write(checksum)) logger.debug(f"Checksum updated and written to '{checksum_path}'.") - print(colored(f"[+] Checksum updated successfully.", "green")) + print(colored("[+] Checksum updated successfully.", "green")) except Exception as e: logger.error(f"Failed to update checksum: {e}", exc_info=True) diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 9c25101a..d74540c2 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -1671,7 +1671,7 @@ class PasswordManager: self.error_queue.put(exc) try: - loop = asyncio.get_running_loop() + asyncio.get_running_loop() except RuntimeError: thread = threading.Thread( target=lambda: asyncio.run(_worker()), daemon=True @@ -1734,7 +1734,7 @@ class PasswordManager: self.error_queue.put(exc) try: - loop = asyncio.get_running_loop() + asyncio.get_running_loop() except RuntimeError: threading.Thread(target=_worker, daemon=True).start() else: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ae376de3..f46b6f30 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -4,14 +4,30 @@ import sys from pathlib import Path import pytest +from types import SimpleNamespace +from httpx import ASGITransport, AsyncClient +import bcrypt sys.path.append(str(Path(__file__).resolve().parents[1])) -from helpers import create_vault, TEST_PASSWORD, TEST_SEED +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 @pytest.fixture( @@ -94,3 +110,87 @@ def password_manager(vault, 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) diff --git a/src/tests/helpers.py b/src/tests/helpers.py index 8373cd37..f2230c6c 100644 --- a/src/tests/helpers.py +++ b/src/tests/helpers.py @@ -36,7 +36,6 @@ def create_vault( import uuid import asyncio -import pytest from nostr.backup_models import ( KIND_MANIFEST, @@ -239,39 +238,3 @@ class DummyRelayClient: return Result(events) -@pytest.fixture -def dummy_nostr_client(tmp_path, monkeypatch): - """Return a NostrClient wired to a DummyRelayClient.""" - from cryptography.fernet import Fernet - from nostr.client import NostrClient - - 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 = EncryptionManager(Fernet.generate_key(), tmp_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 diff --git a/src/tests/test_api.py b/src/tests/test_api.py index ec0fabc5..64a85cbf 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -1,53 +1,12 @@ -from types import SimpleNamespace from pathlib import Path import sys 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 - - -@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.mark.anyio diff --git a/src/tests/test_api_new_endpoints.py b/src/tests/test_api_new_endpoints.py index c4503411..674737c0 100644 --- a/src/tests/test_api_new_endpoints.py +++ b/src/tests/test_api_new_endpoints.py @@ -5,8 +5,6 @@ import base64 import pytest from seedpass import api -from test_api import client -from helpers import dummy_nostr_client import string from seedpass.core.password_generation import PasswordGenerator, PasswordPolicy from seedpass.core.encryption import EncryptionManager diff --git a/src/tests/test_api_notifications.py b/src/tests/test_api_notifications.py index 57a02a54..1d9069f9 100644 --- a/src/tests/test_api_notifications.py +++ b/src/tests/test_api_notifications.py @@ -1,4 +1,3 @@ -from test_api import client from types import SimpleNamespace import queue import pytest diff --git a/src/tests/test_api_profile_stats.py b/src/tests/test_api_profile_stats.py index 2e172bd9..2b646c9d 100644 --- a/src/tests/test_api_profile_stats.py +++ b/src/tests/test_api_profile_stats.py @@ -1,4 +1,3 @@ -from test_api import client import pytest diff --git a/src/tests/test_full_sync_roundtrip.py b/src/tests/test_full_sync_roundtrip.py index 69a24d3d..c9709f7a 100644 --- a/src/tests/test_full_sync_roundtrip.py +++ b/src/tests/test_full_sync_roundtrip.py @@ -2,7 +2,7 @@ import asyncio from pathlib import Path from tempfile import TemporaryDirectory -from helpers import create_vault, dummy_nostr_client +from helpers import create_vault from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager diff --git a/src/tests/test_full_sync_roundtrip_new.py b/src/tests/test_full_sync_roundtrip_new.py index 69a24d3d..c9709f7a 100644 --- a/src/tests/test_full_sync_roundtrip_new.py +++ b/src/tests/test_full_sync_roundtrip_new.py @@ -2,7 +2,7 @@ import asyncio from pathlib import Path from tempfile import TemporaryDirectory -from helpers import create_vault, dummy_nostr_client +from helpers import create_vault from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager diff --git a/src/tests/test_generate_test_profile_sync.py b/src/tests/test_generate_test_profile_sync.py index 216bb3d6..03808bd5 100644 --- a/src/tests/test_generate_test_profile_sync.py +++ b/src/tests/test_generate_test_profile_sync.py @@ -5,7 +5,6 @@ from tempfile import TemporaryDirectory import asyncio import gzip -from helpers import dummy_nostr_client def load_script(): diff --git a/src/tests/test_manager_add_password.py b/src/tests/test_manager_add_password.py index 92bc12c6..422827b9 100644 --- a/src/tests/test_manager_add_password.py +++ b/src/tests/test_manager_add_password.py @@ -5,7 +5,7 @@ from types import SimpleNamespace import pytest -from helpers import create_vault, TEST_SEED, TEST_PASSWORD, dummy_nostr_client +from helpers import create_vault, TEST_SEED, TEST_PASSWORD sys.path.append(str(Path(__file__).resolve().parents[1])) diff --git a/src/tests/test_manifest_id_privacy.py b/src/tests/test_manifest_id_privacy.py index aa06c1c1..4d4efa04 100644 --- a/src/tests/test_manifest_id_privacy.py +++ b/src/tests/test_manifest_id_privacy.py @@ -1,6 +1,5 @@ import asyncio -from helpers import dummy_nostr_client def test_published_events_no_fingerprint(dummy_nostr_client): diff --git a/src/tests/test_manifest_state_restore.py b/src/tests/test_manifest_state_restore.py index c28fad78..5abb75d2 100644 --- a/src/tests/test_manifest_state_restore.py +++ b/src/tests/test_manifest_state_restore.py @@ -2,7 +2,7 @@ import asyncio from pathlib import Path from tempfile import TemporaryDirectory -from helpers import create_vault, dummy_nostr_client, TEST_SEED +from helpers import create_vault, TEST_SEED from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager @@ -34,8 +34,8 @@ def _init_pm(dir_path: Path, client) -> PasswordManager: return pm -def test_manifest_state_restored(monkeypatch, tmp_path): - client, relay = dummy_nostr_client.__wrapped__(tmp_path / "c1", monkeypatch) +def test_manifest_state_restored(monkeypatch, tmp_path, make_dummy_nostr_client): + client, relay = make_dummy_nostr_client(tmp_path / "c1") with TemporaryDirectory() as tmpdir: fp_dir = Path(tmpdir) pm1 = _init_pm(fp_dir, client) @@ -48,7 +48,7 @@ def test_manifest_state_restored(monkeypatch, tmp_path): assert delta_ts > 0 assert result["manifest_id"] == manifest_id - client2, _ = dummy_nostr_client.__wrapped__(tmp_path / "c2", monkeypatch) + client2, _ = make_dummy_nostr_client(tmp_path / "c2") monkeypatch.setattr( "seedpass.core.manager.NostrClient", lambda *a, **k: client2 ) diff --git a/src/tests/test_multiple_deltas_sync.py b/src/tests/test_multiple_deltas_sync.py index 3b2b2f71..296f6421 100644 --- a/src/tests/test_multiple_deltas_sync.py +++ b/src/tests/test_multiple_deltas_sync.py @@ -2,7 +2,7 @@ import asyncio from pathlib import Path from tempfile import TemporaryDirectory -from helpers import create_vault, dummy_nostr_client +from helpers import create_vault from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager diff --git a/src/tests/test_nostr_dummy_client.py b/src/tests/test_nostr_dummy_client.py index 763e5b0d..5087794f 100644 --- a/src/tests/test_nostr_dummy_client.py +++ b/src/tests/test_nostr_dummy_client.py @@ -3,7 +3,7 @@ import gzip import math import pytest -from helpers import create_vault, dummy_nostr_client, TEST_SEED +from helpers import create_vault, TEST_SEED from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager from seedpass.core.config_manager import ConfigManager diff --git a/src/tests/test_nostr_legacy_key_fallback.py b/src/tests/test_nostr_legacy_key_fallback.py index a551b120..07f42558 100644 --- a/src/tests/test_nostr_legacy_key_fallback.py +++ b/src/tests/test_nostr_legacy_key_fallback.py @@ -3,7 +3,7 @@ import base64 import hashlib import json -from helpers import DummyEvent, DummyFilter, dummy_nostr_client +from helpers import DummyEvent, DummyFilter from nostr.backup_models import KIND_MANIFEST, KIND_SNAPSHOT_CHUNK from nostr_sdk import Keys diff --git a/src/tests/test_nostr_restore_flow.py b/src/tests/test_nostr_restore_flow.py index 759f6f86..1c5c1f86 100644 --- a/src/tests/test_nostr_restore_flow.py +++ b/src/tests/test_nostr_restore_flow.py @@ -1,7 +1,7 @@ from pathlib import Path import main -from helpers import create_vault, dummy_nostr_client, TEST_SEED, TEST_PASSWORD +from helpers import create_vault, TEST_SEED, TEST_PASSWORD from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager from seedpass.core.config_manager import ConfigManager @@ -28,8 +28,8 @@ def _init_pm(dir_path: Path, client) -> PasswordManager: return pm -def test_restore_flow_from_snapshot(monkeypatch, tmp_path): - client, relay = dummy_nostr_client.__wrapped__(tmp_path / "srv", monkeypatch) +def test_restore_flow_from_snapshot(monkeypatch, tmp_path, make_dummy_nostr_client): + client, relay = make_dummy_nostr_client(tmp_path / "srv") dir_a = tmp_path / "A" dir_b = tmp_path / "B" diff --git a/src/tests/test_profiles.py b/src/tests/test_profiles.py index c9efd3ca..a7cfb4f2 100644 --- a/src/tests/test_profiles.py +++ b/src/tests/test_profiles.py @@ -6,7 +6,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1])) from utils.fingerprint_manager import FingerprintManager from seedpass.core.manager import PasswordManager, EncryptionMode -from helpers import create_vault, dummy_nostr_client +from helpers import create_vault import gzip from nostr.backup_models import Manifest, ChunkMeta diff --git a/src/tests/test_restore_from_nostr_setup.py b/src/tests/test_restore_from_nostr_setup.py index 9f304779..1d355feb 100644 --- a/src/tests/test_restore_from_nostr_setup.py +++ b/src/tests/test_restore_from_nostr_setup.py @@ -1,6 +1,6 @@ from pathlib import Path -from helpers import create_vault, dummy_nostr_client, TEST_SEED, TEST_PASSWORD +from helpers import create_vault, TEST_SEED, TEST_PASSWORD from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager @@ -28,8 +28,8 @@ def _init_pm(dir_path: Path, client) -> PasswordManager: return pm -def test_handle_new_seed_setup_restore_from_nostr(monkeypatch, tmp_path, capsys): - client, _relay = dummy_nostr_client.__wrapped__(tmp_path / "srv", monkeypatch) +def test_handle_new_seed_setup_restore_from_nostr(monkeypatch, tmp_path, capsys, make_dummy_nostr_client): + client, _relay = make_dummy_nostr_client(tmp_path / "srv") dir_a = tmp_path / "A" dir_b = tmp_path / "B" @@ -133,8 +133,8 @@ 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) +def test_restore_from_nostr_warns(monkeypatch, tmp_path, capsys, make_dummy_nostr_client): + client, _relay = make_dummy_nostr_client(tmp_path / "srv") monkeypatch.setattr(client, "fetch_latest_snapshot", _no_snapshot) pm = PasswordManager.__new__(PasswordManager) @@ -150,8 +150,8 @@ def test_restore_from_nostr_warns(monkeypatch, tmp_path, capsys): 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) +def test_restore_from_nostr_abort(monkeypatch, tmp_path, capsys, make_dummy_nostr_client): + client, _relay = make_dummy_nostr_client(tmp_path / "srv") monkeypatch.setattr(client, "fetch_latest_snapshot", _no_snapshot) pm = PasswordManager.__new__(PasswordManager) diff --git a/src/tests/test_service_classes.py b/src/tests/test_service_classes.py index 530e60ff..e29ade7d 100644 --- a/src/tests/test_service_classes.py +++ b/src/tests/test_service_classes.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from helpers import create_vault, TEST_SEED, TEST_PASSWORD, dummy_nostr_client +from helpers import create_vault, TEST_SEED, TEST_PASSWORD from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager from seedpass.core.manager import PasswordManager, EncryptionMode diff --git a/src/tests/test_sync_race_conditions.py b/src/tests/test_sync_race_conditions.py index 07866594..b7dbc45a 100644 --- a/src/tests/test_sync_race_conditions.py +++ b/src/tests/test_sync_race_conditions.py @@ -3,7 +3,7 @@ import threading from pathlib import Path -from helpers import create_vault, dummy_nostr_client +from helpers import create_vault from seedpass.core.entry_management import EntryManager from seedpass.core.backup import BackupManager @@ -31,8 +31,8 @@ def _init_pm(dir_path: Path, client) -> PasswordManager: return pm -def test_sync_race_conditions(monkeypatch, tmp_path): - client_a, relay = dummy_nostr_client.__wrapped__(tmp_path / "c1", monkeypatch) +def test_sync_race_conditions(monkeypatch, tmp_path, make_dummy_nostr_client): + client_a, relay = make_dummy_nostr_client(tmp_path / "c1") from cryptography.fernet import Fernet from nostr.client import NostrClient diff --git a/src/tests/test_verbose_timing.py b/src/tests/test_verbose_timing.py index a623b256..ff2517ec 100644 --- a/src/tests/test_verbose_timing.py +++ b/src/tests/test_verbose_timing.py @@ -2,7 +2,6 @@ import asyncio import logging from seedpass.core.manager import PasswordManager -from helpers import dummy_nostr_client def test_unlock_vault_logs_time(monkeypatch, caplog, tmp_path):