PinVault/tests/conftest.py
Jarian Cottingham a69c40bc84 Fix startup crashes and harden crypto
- Define missing dict_factory (orphaned fragment left app unable to
  return DB rows; every endpoint crashed on first query)
- Skip backup when PINVAULT_NAS_BACKUP_DIR is empty (os.makedirs('')
  raised in bootstrap and killed gunicorn at import; compose default)
- Generate PINs with secrets.randbelow instead of random.randint
- Per-row random HMAC salts for PINs and recovery codes (single shared
  hardcoded salt defeated the precomputation protection); constant-time
  comparisons; legacy rows keep the historic salt
- Atomic backup restore (temp file + os.replace) to avoid torn DB
- Validate lock_days type, guard backup loop, move re import to module
  level
- Replace weak unittest stubs with 34-test pytest suite (sqlcipher
  shimmed with sqlite3 so tests run without the native lib)
- Add README, LICENSE, pyproject, .env.example; ruff clean
2026-08-20 22:52:28 +00:00

90 lines
2.5 KiB
Python

import os
import sqlite3
import sys
import tempfile
import types
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_TEST_ROOT = tempfile.mkdtemp(prefix="pinvault-test-")
def _install_sqlcipher_shim():
"""Fall back to plain sqlite3 when pysqlcipher3 is not installed.
The SQLCipher-specific `PRAGMA key` statement is swallowed so the app's
full endpoint logic can be exercised without the native library.
"""
try:
import pysqlcipher3.dbapi2 # noqa: F401
return
except ImportError:
pass
class _Connection:
def __init__(self, raw):
self._raw = raw
@property
def row_factory(self):
return self._raw.row_factory
@row_factory.setter
def row_factory(self, value):
self._raw.row_factory = value
def execute(self, sql, *args):
if sql.strip().upper().startswith("PRAGMA KEY"):
return self._raw.execute("SELECT 1", *args)
return self._raw.execute(sql, *args)
def commit(self):
self._raw.commit()
def close(self):
self._raw.close()
def __enter__(self):
return self
def __exit__(self, *exc):
self._raw.close()
def connect(path, *args, **kwargs):
raw = sqlite3.connect(path, *args, **kwargs)
return _Connection(raw)
dbapi = types.ModuleType("pysqlcipher3.dbapi2")
dbapi.connect = connect
pkg = types.ModuleType("pysqlcipher3")
pkg.dbapi2 = dbapi
sys.modules["pysqlcipher3"] = pkg
sys.modules["pysqlcipher3.dbapi2"] = dbapi
_install_sqlcipher_shim()
os.environ.setdefault("PINVAULT_MASTER_HASH", "$2b$12$" + "a" * 53)
os.environ.setdefault("PINVAULT_DB", os.path.join(_TEST_ROOT, "pinvault.db"))
os.environ.setdefault("PINVAULT_LOCAL_BACKUP_DIR", os.path.join(_TEST_ROOT, "backups"))
os.environ.setdefault("PINVAULT_NAS_BACKUP_DIR", "")
os.environ.setdefault("PINVAULT_API_KEY", "test-api-key-0123456789abcdef0123456789abcd")
import app as pinvault # noqa: E402
import pytest # noqa: E402
AUTH = {"Authorization": f"Bearer {os.environ['PINVAULT_API_KEY']}"}
def create_pin(client, label="test", lock_days=30):
resp = client.post("/api/pins", json={"label": label, "lock_days": lock_days}, headers=AUTH)
assert resp.status_code == 201, resp.get_json()
return resp.get_json()
@pytest.fixture()
def client():
pinvault._pin_attempt_locks.clear()
return pinvault.app.test_client()