Merge pull request #170 from PR0M3TH3AN/codex/update-and-add-unit-tests-for-entries

Update tests for entries schema
This commit is contained in:
thePR0M3TH3AN
2025-07-02 23:10:48 -04:00
committed by GitHub
13 changed files with 75 additions and 29 deletions

View File

@@ -35,7 +35,7 @@ class BackupManager:
timestamped filenames to facilitate easy identification and retrieval. timestamped filenames to facilitate easy identification and retrieval.
""" """
BACKUP_FILENAME_TEMPLATE = "passwords_db_backup_{timestamp}.json.enc" BACKUP_FILENAME_TEMPLATE = "entries_db_backup_{timestamp}.json.enc"
def __init__(self, fingerprint_dir: Path): def __init__(self, fingerprint_dir: Path):
""" """
@@ -47,7 +47,7 @@ class BackupManager:
self.fingerprint_dir = fingerprint_dir self.fingerprint_dir = fingerprint_dir
self.backup_dir = self.fingerprint_dir / "backups" self.backup_dir = self.fingerprint_dir / "backups"
self.backup_dir.mkdir(parents=True, exist_ok=True) self.backup_dir.mkdir(parents=True, exist_ok=True)
self.index_file = self.fingerprint_dir / "seedpass_passwords_db.json.enc" self.index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc"
logger.debug( logger.debug(
f"BackupManager initialized with backup directory at {self.backup_dir}" f"BackupManager initialized with backup directory at {self.backup_dir}"
) )
@@ -79,7 +79,7 @@ class BackupManager:
def restore_latest_backup(self) -> None: def restore_latest_backup(self) -> None:
try: try:
backup_files = sorted( backup_files = sorted(
self.backup_dir.glob("passwords_db_backup_*.json.enc"), self.backup_dir.glob("entries_db_backup_*.json.enc"),
key=lambda x: x.stat().st_mtime, key=lambda x: x.stat().st_mtime,
reverse=True, reverse=True,
) )
@@ -112,7 +112,7 @@ class BackupManager:
def list_backups(self) -> None: def list_backups(self) -> None:
try: try:
backup_files = sorted( backup_files = sorted(
self.backup_dir.glob("passwords_db_backup_*.json.enc"), self.backup_dir.glob("entries_db_backup_*.json.enc"),
key=lambda x: x.stat().st_mtime, key=lambda x: x.stat().st_mtime,
reverse=True, reverse=True,
) )

View File

@@ -246,10 +246,10 @@ class EncryptionManager:
:param data: The JSON data to save. :param data: The JSON data to save.
:param relative_path: The relative path within the fingerprint directory where data will be saved. :param relative_path: The relative path within the fingerprint directory where data will be saved.
Defaults to 'seedpass_passwords_db.json.enc'. Defaults to 'seedpass_entries_db.json.enc'.
""" """
if relative_path is None: if relative_path is None:
relative_path = Path("seedpass_passwords_db.json.enc") relative_path = Path("seedpass_entries_db.json.enc")
try: try:
json_data = json.dumps(data, indent=4).encode("utf-8") json_data = json.dumps(data, indent=4).encode("utf-8")
self.encrypt_and_save_file(json_data, relative_path) self.encrypt_and_save_file(json_data, relative_path)
@@ -273,11 +273,11 @@ class EncryptionManager:
Decrypts and loads JSON data from the specified relative path within the fingerprint directory. Decrypts and loads JSON data from the specified relative path within the fingerprint directory.
:param relative_path: The relative path within the fingerprint directory from which data will be loaded. :param relative_path: The relative path within the fingerprint directory from which data will be loaded.
Defaults to 'seedpass_passwords_db.json.enc'. Defaults to 'seedpass_entries_db.json.enc'.
:return: The decrypted JSON data as a dictionary. :return: The decrypted JSON data as a dictionary.
""" """
if relative_path is None: if relative_path is None:
relative_path = Path("seedpass_passwords_db.json.enc") relative_path = Path("seedpass_entries_db.json.enc")
file_path = self.fingerprint_dir / relative_path file_path = self.fingerprint_dir / relative_path
@@ -320,10 +320,10 @@ class EncryptionManager:
Updates the checksum file for the specified file within the fingerprint directory. Updates the checksum file for the specified file within the fingerprint directory.
:param relative_path: The relative path within the fingerprint directory for which the checksum will be updated. :param relative_path: The relative path within the fingerprint directory for which the checksum will be updated.
Defaults to 'seedpass_passwords_db.json.enc'. Defaults to 'seedpass_entries_db.json.enc'.
""" """
if relative_path is None: if relative_path is None:
relative_path = Path("seedpass_passwords_db.json.enc") relative_path = Path("seedpass_entries_db.json.enc")
try: try:
file_path = self.fingerprint_dir / relative_path file_path = self.fingerprint_dir / relative_path
logger.debug("Calculating checksum of the encrypted file bytes.") logger.debug("Calculating checksum of the encrypted file bytes.")
@@ -368,7 +368,7 @@ class EncryptionManager:
:return: Encrypted data as bytes or None if the index file does not exist. :return: Encrypted data as bytes or None if the index file does not exist.
""" """
try: try:
relative_path = Path("seedpass_passwords_db.json.enc") relative_path = Path("seedpass_entries_db.json.enc")
if not (self.fingerprint_dir / relative_path).exists(): if not (self.fingerprint_dir / relative_path).exists():
logger.error( logger.error(
f"Index file '{relative_path}' does not exist in '{self.fingerprint_dir}'." f"Index file '{relative_path}' does not exist in '{self.fingerprint_dir}'."
@@ -407,10 +407,10 @@ class EncryptionManager:
:param encrypted_data: The encrypted data retrieved from Nostr. :param encrypted_data: The encrypted data retrieved from Nostr.
:param relative_path: The relative path within the fingerprint directory to update. :param relative_path: The relative path within the fingerprint directory to update.
Defaults to 'seedpass_passwords_db.json.enc'. Defaults to 'seedpass_entries_db.json.enc'.
""" """
if relative_path is None: if relative_path is None:
relative_path = Path("seedpass_passwords_db.json.enc") relative_path = Path("seedpass_entries_db.json.enc")
try: try:
decrypted_data = self.decrypt_data(encrypted_data) decrypted_data = self.decrypt_data(encrypted_data)
data = json.loads(decrypted_data.decode("utf-8")) data = json.loads(decrypted_data.decode("utf-8"))

View File

@@ -50,8 +50,8 @@ class EntryManager:
self.fingerprint_dir = fingerprint_dir self.fingerprint_dir = fingerprint_dir
# Use paths relative to the fingerprint directory # Use paths relative to the fingerprint directory
self.index_file = self.fingerprint_dir / "seedpass_passwords_db.json.enc" self.index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc"
self.checksum_file = self.fingerprint_dir / "seedpass_passwords_db_checksum.txt" self.checksum_file = self.fingerprint_dir / "seedpass_entries_db_checksum.txt"
logger.debug(f"EntryManager initialized with index file at {self.index_file}") logger.debug(f"EntryManager initialized with index file at {self.index_file}")
@@ -410,7 +410,7 @@ class EntryManager:
return return
timestamp = int(time.time()) timestamp = int(time.time())
backup_filename = f"passwords_db_backup_{timestamp}.json.enc" backup_filename = f"entries_db_backup_{timestamp}.json.enc"
backup_path = self.fingerprint_dir / backup_filename backup_path = self.fingerprint_dir / backup_filename
with open(index_file_path, "rb") as original_file, open( with open(index_file_path, "rb") as original_file, open(

View File

@@ -769,7 +769,7 @@ class PasswordManager:
def sync_index_from_nostr_if_missing(self) -> None: def sync_index_from_nostr_if_missing(self) -> None:
"""Retrieve the password database from Nostr if it doesn't exist locally.""" """Retrieve the password database from Nostr if it doesn't exist locally."""
index_file = self.fingerprint_dir / "seedpass_passwords_db.json.enc" index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc"
if index_file.exists(): if index_file.exists():
return return
try: try:

View File

@@ -10,7 +10,7 @@ from .encryption import EncryptionManager
class Vault: class Vault:
"""Simple wrapper around :class:`EncryptionManager` for vault storage.""" """Simple wrapper around :class:`EncryptionManager` for vault storage."""
INDEX_FILENAME = "seedpass_passwords_db.json.enc" INDEX_FILENAME = "seedpass_entries_db.json.enc"
CONFIG_FILENAME = "seedpass_config.json.enc" CONFIG_FILENAME = "seedpass_config.json.enc"
def __init__( def __init__(

View File

@@ -17,7 +17,7 @@ def test_backup_restore_workflow(monkeypatch):
vault, enc_mgr = create_vault(fp_dir, TEST_SEED, TEST_PASSWORD) vault, enc_mgr = create_vault(fp_dir, TEST_SEED, TEST_PASSWORD)
backup_mgr = BackupManager(fp_dir) backup_mgr = BackupManager(fp_dir)
index_file = fp_dir / "seedpass_passwords_db.json.enc" index_file = fp_dir / "seedpass_entries_db.json.enc"
data1 = { data1 = {
"schema_version": 2, "schema_version": 2,
@@ -30,7 +30,7 @@ def test_backup_restore_workflow(monkeypatch):
monkeypatch.setattr(time, "time", lambda: 1111) monkeypatch.setattr(time, "time", lambda: 1111)
backup_mgr.create_backup() backup_mgr.create_backup()
backup1 = fp_dir / "backups" / "passwords_db_backup_1111.json.enc" backup1 = fp_dir / "backups" / "entries_db_backup_1111.json.enc"
assert backup1.exists() assert backup1.exists()
assert backup1.stat().st_mode & 0o777 == 0o600 assert backup1.stat().st_mode & 0o777 == 0o600
@@ -45,7 +45,7 @@ def test_backup_restore_workflow(monkeypatch):
monkeypatch.setattr(time, "time", lambda: 2222) monkeypatch.setattr(time, "time", lambda: 2222)
backup_mgr.create_backup() backup_mgr.create_backup()
backup2 = fp_dir / "backups" / "passwords_db_backup_2222.json.enc" backup2 = fp_dir / "backups" / "entries_db_backup_2222.json.enc"
assert backup2.exists() assert backup2.exists()
assert backup2.stat().st_mode & 0o777 == 0o600 assert backup2.stat().st_mode & 0o777 == 0o600

View File

@@ -21,8 +21,8 @@ def test_encryption_checksum_workflow():
manager.save_json_data(data) manager.save_json_data(data)
manager.update_checksum() manager.update_checksum()
enc_file = tmp_path / "seedpass_passwords_db.json.enc" enc_file = tmp_path / "seedpass_entries_db.json.enc"
chk_file = tmp_path / "seedpass_passwords_db.json_checksum.txt" chk_file = tmp_path / "seedpass_entries_db.json_checksum.txt"
checksum = chk_file.read_text().strip() checksum = chk_file.read_text().strip()
assert re.fullmatch(r"[0-9a-f]{64}", checksum) assert re.fullmatch(r"[0-9a-f]{64}", checksum)

View File

@@ -20,7 +20,7 @@ def test_json_save_and_load_round_trip():
loaded = manager.load_json_data() loaded = manager.load_json_data()
assert loaded == data assert loaded == data
file_path = Path(tmpdir) / "seedpass_passwords_db.json.enc" file_path = Path(tmpdir) / "seedpass_entries_db.json.enc"
raw = file_path.read_bytes() raw = file_path.read_bytes()
assert raw != json.dumps(data, indent=4).encode("utf-8") assert raw != json.dumps(data, indent=4).encode("utf-8")

View File

@@ -1,6 +1,7 @@
import sys import sys
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import pytest
from helpers import create_vault, TEST_SEED, TEST_PASSWORD from helpers import create_vault, TEST_SEED, TEST_PASSWORD
sys.path.append(str(Path(__file__).resolve().parents[1])) sys.path.append(str(Path(__file__).resolve().parents[1]))
@@ -30,3 +31,30 @@ def test_add_and_retrieve_entry():
data = enc_mgr.load_json_data(entry_mgr.index_file) data = enc_mgr.load_json_data(entry_mgr.index_file)
assert str(index) in data.get("entries", {}) assert str(index) in data.get("entries", {})
assert data["entries"][str(index)] == entry assert data["entries"][str(index)] == entry
@pytest.mark.parametrize(
"method, expected_type",
[
("add_entry", "password"),
("add_totp", "totp"),
("add_ssh_key", "ssh"),
("add_seed", "seed"),
],
)
def test_round_trip_entry_types(method, expected_type):
with TemporaryDirectory() as tmpdir:
vault, enc_mgr = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD)
entry_mgr = EntryManager(vault, Path(tmpdir))
if method == "add_entry":
index = entry_mgr.add_entry("example.com", 8)
else:
with pytest.raises(NotImplementedError):
getattr(entry_mgr, method)()
index = 0
entry = entry_mgr.retrieve_entry(index)
assert entry["type"] == expected_type
data = enc_mgr.load_json_data(entry_mgr.index_file)
assert data["entries"][str(index)]["type"] == expected_type

View File

@@ -19,7 +19,7 @@ def test_update_checksum_writes_to_expected_path():
vault.save_index({"entries": {}}) vault.save_index({"entries": {}})
entry_mgr.update_checksum() entry_mgr.update_checksum()
expected = tmp_path / "seedpass_passwords_db_checksum.txt" expected = tmp_path / "seedpass_entries_db_checksum.txt"
assert expected.exists() assert expected.exists()
@@ -32,5 +32,5 @@ def test_backup_index_file_creates_backup_in_directory():
vault.save_index({"entries": {}}) vault.save_index({"entries": {}})
entry_mgr.backup_index_file() entry_mgr.backup_index_file()
backups = list(tmp_path.glob("passwords_db_backup_*.json.enc")) backups = list(tmp_path.glob("entries_db_backup_*.json.enc"))
assert len(backups) == 1 assert len(backups) == 1

View File

@@ -64,9 +64,9 @@ def test_manager_workflow(monkeypatch):
pm.handle_add_password() pm.handle_add_password()
assert pm.is_dirty is False assert pm.is_dirty is False
backups = list(tmp_path.glob("passwords_db_backup_*.json.enc")) backups = list(tmp_path.glob("entries_db_backup_*.json.enc"))
assert len(backups) == 1 assert len(backups) == 1
checksum_file = tmp_path / "seedpass_passwords_db_checksum.txt" checksum_file = tmp_path / "seedpass_entries_db_checksum.txt"
assert checksum_file.exists() assert checksum_file.exists()
checksum_after_add = checksum_file.read_text() checksum_after_add = checksum_file.read_text()
first_post = pm.nostr_client.published[-1] first_post = pm.nostr_client.published[-1]
@@ -79,7 +79,7 @@ def test_manager_workflow(monkeypatch):
assert pm.is_dirty is False assert pm.is_dirty is False
pm.backup_manager.create_backup() pm.backup_manager.create_backup()
backup_dir = tmp_path / "backups" backup_dir = tmp_path / "backups"
backups_mod = list(backup_dir.glob("passwords_db_backup_*.json.enc")) backups_mod = list(backup_dir.glob("entries_db_backup_*.json.enc"))
assert backups_mod assert backups_mod
checksum_after_modify = checksum_file.read_text() checksum_after_modify = checksum_file.read_text()
assert checksum_after_modify != checksum_after_add assert checksum_after_modify != checksum_after_add

View File

@@ -23,6 +23,21 @@ def test_migrate_v0_to_v2(tmp_path: Path):
assert data["entries"]["0"] == expected_entry assert data["entries"]["0"] == expected_entry
def test_migrate_v1_to_v2(tmp_path: Path):
enc_mgr, vault = setup(tmp_path)
legacy = {"schema_version": 1, "passwords": {"0": {"website": "b", "length": 10}}}
enc_mgr.save_json_data(legacy)
data = vault.load_index()
assert data["schema_version"] == LATEST_VERSION
expected_entry = {
"website": "b",
"length": 10,
"type": "password",
"notes": "",
}
assert data["entries"]["0"] == expected_entry
def test_error_on_future_version(tmp_path: Path): def test_error_on_future_version(tmp_path: Path):
enc_mgr, vault = setup(tmp_path) enc_mgr, vault = setup(tmp_path)
future = {"schema_version": LATEST_VERSION + 1, "entries": {}} future = {"schema_version": LATEST_VERSION + 1, "entries": {}}

View File

@@ -6,6 +6,7 @@ from hypothesis import given, strategies as st, settings
sys.path.append(str(Path(__file__).resolve().parents[1])) sys.path.append(str(Path(__file__).resolve().parents[1]))
from password_manager.password_generation import PasswordGenerator from password_manager.password_generation import PasswordGenerator
from password_manager.entry_types import EntryType
class DummyEnc: class DummyEnc:
@@ -32,8 +33,10 @@ def make_generator():
@settings(deadline=None) @settings(deadline=None)
def test_password_properties(length, index): def test_password_properties(length, index):
pg = make_generator() pg = make_generator()
entry_type = EntryType.PASSWORD.value
pw1 = pg.generate_password(length=length, index=index) pw1 = pg.generate_password(length=length, index=index)
pw2 = pg.generate_password(length=length, index=index) pw2 = pg.generate_password(length=length, index=index)
assert entry_type == "password"
assert pw1 == pw2 assert pw1 == pw2
assert len(pw1) == length assert len(pw1) == length