import inspect import os import secrets import app as pinvault from conftest import AUTH, create_pin def test_api_key_length(): assert len(pinvault.API_KEY) >= 32 def test_bootstrap_returns_bool(): assert isinstance(pinvault._bootstrap_ok, bool) def test_no_sys_exit_in_bootstrap(): src = inspect.getsource(pinvault.bootstrap) assert "sys.exit" not in src, "bootstrap must not call sys.exit" def test_nas_backup_configurable(): assert pinvault.NAS_BACKUP_DIR == os.environ.get("PINVAULT_NAS_BACKUP_DIR", "") def test_hash_val_per_row_salt(): assert pinvault.hash_val("1234", "salt-a") != pinvault.hash_val("1234", "salt-b") # Legacy rows (salt NULL) keep working with the historic salt. assert pinvault.hash_val("1234") == pinvault.hash_val("1234", pinvault.LEGACY_SALT) def test_recover_pin_roundtrip(): for pin in ("0000", "0042", "1000", "9999"): salt = secrets.token_hex(16) assert pinvault.recover_pin(pinvault.hash_val(pin, salt), salt) == pin def test_new_pins_have_per_row_salt(client): data = create_pin(client, label="salted") with pinvault.get_conn() as conn: row = conn.execute("SELECT salt FROM pins WHERE id = ?", (data["id"],)).fetchone() assert row["salt"], "pin row must carry a salt" assert row["salt"] != pinvault.LEGACY_SALT codes = conn.execute( "SELECT salt FROM recovery_codes WHERE pin_id = ?", (data["id"],) ).fetchall() assert len(codes) == 4 assert all(c["salt"] for c in codes) def test_pin_generation_zero_padded(client): for _ in range(10): data = create_pin(client) assert len(data["pin"]) == 4 assert data["pin"] == f"{int(data['pin']):04d}" def test_rate_limit_blocks_after_max(client): data = create_pin(client, lock_days=30) url = f"/api/pins/{data['id']}/access" for i in range(pinvault._PIN_MAX_ATTEMPTS): resp = client.post(url, json={}, headers=AUTH) assert resp.status_code == 423, f"attempt {i + 1} should be allowed" resp = client.post(url, json={}, headers=AUTH) assert resp.status_code == 429 def test_rate_limit_resets_on_success(client): data = create_pin(client, lock_days=0) url = f"/api/pins/{data['id']}/access" for _ in range(pinvault._PIN_MAX_ATTEMPTS + 3): assert client.post(url, json={}, headers=AUTH).status_code == 200 def test_rate_limit_independent_per_pin(client): a = create_pin(client, label="a", lock_days=30) b = create_pin(client, label="b", lock_days=30) for _ in range(pinvault._PIN_MAX_ATTEMPTS + 1): client.post(f"/api/pins/{a['id']}/access", json={}, headers=AUTH) resp = client.post(f"/api/pins/{b['id']}/access", json={}, headers=AUTH) assert resp.status_code == 423 def test_check_pin_rate_limit_unit(): pinvault._pin_attempt_locks.clear() for i in range(pinvault._PIN_MAX_ATTEMPTS): allowed, _ = pinvault._check_pin_rate_limit(999) assert allowed, f"Attempt {i + 1} should be allowed" allowed, remaining = pinvault._check_pin_rate_limit(999) assert not allowed assert remaining > 0 pinvault._reset_pin_rate_limit(999) allowed, _ = pinvault._check_pin_rate_limit(999) assert allowed pinvault._pin_attempt_locks.clear() def test_backup_dirs_excludes_empty_nas(): if not pinvault.NAS_BACKUP_DIR: assert pinvault._backup_dirs() == [pinvault.LOCAL_BACKUP_DIR]