Files
seedPass/src/tests/test_config_manager.py
2025-08-05 23:05:11 -04:00

239 lines
8.5 KiB
Python

import bcrypt
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
import sys
sys.path.append(str(Path(__file__).resolve().parents[1]))
from seedpass.core.config_manager import ConfigManager
from seedpass.core.vault import Vault
from nostr.client import DEFAULT_RELAYS
from constants import INACTIVITY_TIMEOUT
def test_config_defaults_and_round_trip():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["relays"] == list(DEFAULT_RELAYS)
assert cfg["pin_hash"] == ""
assert cfg["password_hash"] == ""
assert cfg["additional_backup_path"] == ""
assert cfg["quick_unlock_enabled"] is False
assert cfg["kdf_iterations"] == 50_000
cfg_mgr.set_pin("1234")
cfg_mgr.set_relays(["wss://example.com"], require_pin=False)
cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["relays"] == ["wss://example.com"]
assert bcrypt.checkpw(b"1234", cfg2["pin_hash"].encode())
def test_pin_verification_and_change():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg_mgr.set_pin("1234")
assert cfg_mgr.verify_pin("1234")
assert not cfg_mgr.verify_pin("0000")
assert cfg_mgr.change_pin("1234", "5678")
assert cfg_mgr.verify_pin("5678")
import json
def test_config_file_encrypted_after_save():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
data = {"relays": ["wss://r"], "pin_hash": ""}
cfg_mgr.save_config(data)
file_path = Path(tmpdir) / cfg_mgr.CONFIG_FILENAME
raw = file_path.read_bytes()
assert raw != json.dumps(data).encode()
loaded = cfg_mgr.load_config(require_pin=False)
assert loaded["relays"] == data["relays"]
assert loaded["pin_hash"] == data["pin_hash"]
assert loaded["password_hash"] == ""
def test_set_relays_persists_changes():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg_mgr.set_relays(["wss://custom"], require_pin=False)
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["relays"] == ["wss://custom"]
def test_set_relays_requires_at_least_one():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
with pytest.raises(ValueError):
cfg_mgr.set_relays([], require_pin=False)
def test_inactivity_timeout_round_trip():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["inactivity_timeout"] == INACTIVITY_TIMEOUT
cfg_mgr.set_inactivity_timeout(123)
cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["inactivity_timeout"] == 123
def test_password_hash_migrates_from_file(tmp_path):
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
# save legacy config without password_hash
legacy_cfg = {"relays": ["wss://r"], "pin_hash": ""}
cfg_mgr.save_config(legacy_cfg)
hashed = bcrypt.hashpw(b"pw", bcrypt.gensalt())
(tmp_path / "hashed_password.enc").write_bytes(hashed)
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["password_hash"] == hashed.decode()
# subsequent loads should read from config
(tmp_path / "hashed_password.enc").unlink()
cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["password_hash"] == hashed.decode()
def test_additional_backup_path_round_trip():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
# default should be empty string
assert cfg_mgr.load_config(require_pin=False)["additional_backup_path"] == ""
cfg_mgr.set_additional_backup_path("/tmp/my_backups")
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["additional_backup_path"] == "/tmp/my_backups"
assert cfg_mgr.get_additional_backup_path() == "/tmp/my_backups"
cfg_mgr.set_additional_backup_path(None)
cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["additional_backup_path"] == ""
def test_secret_mode_round_trip():
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["secret_mode_enabled"] is False
assert cfg["clipboard_clear_delay"] == 45
cfg_mgr.set_secret_mode_enabled(True)
cfg_mgr.set_clipboard_clear_delay(99)
cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["secret_mode_enabled"] is True
assert cfg2["clipboard_clear_delay"] == 99
def test_kdf_iterations_round_trip():
with TemporaryDirectory() as tmpdir:
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
assert cfg_mgr.get_kdf_iterations() == 50_000
cfg_mgr.set_kdf_iterations(200_000)
assert cfg_mgr.get_kdf_iterations() == 200_000
def test_backup_interval_round_trip():
with TemporaryDirectory() as tmpdir:
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
assert cfg_mgr.get_backup_interval() == 0
cfg_mgr.set_backup_interval(15)
assert cfg_mgr.get_backup_interval() == 15
def test_quick_unlock_round_trip():
with TemporaryDirectory() as tmpdir:
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
assert cfg_mgr.get_quick_unlock() is False
cfg_mgr.set_quick_unlock(True)
assert cfg_mgr.get_quick_unlock() is True
def test_nostr_retry_settings_round_trip():
with TemporaryDirectory() as tmpdir:
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["nostr_max_retries"] == 2
assert cfg["nostr_retry_delay"] == 1.0
cfg_mgr.set_nostr_max_retries(5)
cfg_mgr.set_nostr_retry_delay(3.5)
assert cfg_mgr.get_nostr_max_retries() == 5
assert cfg_mgr.get_nostr_retry_delay() == 3.5
def test_special_char_settings_round_trip():
with TemporaryDirectory() as tmpdir:
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg = cfg_mgr.load_config(require_pin=False)
assert cfg["include_special_chars"] is True
assert cfg["allowed_special_chars"] == ""
assert cfg["special_mode"] == "standard"
assert cfg["exclude_ambiguous"] is False
cfg_mgr.set_include_special_chars(False)
cfg_mgr.set_allowed_special_chars("@$")
cfg_mgr.set_special_mode("safe")
cfg_mgr.set_exclude_ambiguous(True)
cfg2 = cfg_mgr.load_config(require_pin=False)
assert cfg2["include_special_chars"] is False
assert cfg2["allowed_special_chars"] == "@$"
assert cfg2["special_mode"] == "safe"
assert cfg2["exclude_ambiguous"] is True
def test_password_policy_extended_fields():
with TemporaryDirectory() as tmpdir:
vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, Path(tmpdir))
cfg_mgr.set_include_special_chars(False)
cfg_mgr.set_allowed_special_chars("()")
cfg_mgr.set_special_mode("safe")
cfg_mgr.set_exclude_ambiguous(True)
policy = cfg_mgr.get_password_policy()
assert policy.include_special_chars is False
assert policy.allowed_special_chars == "()"
assert policy.special_mode == "safe"
assert policy.exclude_ambiguous is True