Files
versioning/tests/test_dbsafety.py
T
retoor 3499f6cda0 Remove encryption; user-data filters; SQLite safety; recovery, retention, scheduler
- No client-side encryption: plaintext content-addressed remote (SHA-256
  names), no backup key, no cryptography dependency (server disk encryption
  is the trust model).
- Filters back user data: images, archives, databases, PDFs accepted; 10 MiB
  cap; binary sniffing removed; temp names hardened (~$, #..#, .temp).
- SQLite zero-error policy: backup-API snapshots + integrity_check, journal
  folding, locked/corrupt loud skips, verified restores.
- Recovery: reindex from manifests, remote adopt, on-demand blob fetch,
  5-day retention + thinning, GC, date-guarded remote purge, metrics.
- Scheduler with WebDAV quota signal and 70% pressure backstop (floor kept).
- 37 tests incl. live-monitor capture safety and DB safety.
2026-10-10 03:41:36 +02:00

127 lines
4.1 KiB
Python

"""Zero-error policy for live databases: only verified snapshots are stored."""
import sqlite3
import pytest
from versiond import dbsafe
from versiond.db import Database
from versiond.filters import Filters
from versiond.ingest import Rejected, Repository
from versiond.store import BlobStore
def _repo(tmp_path):
db = Database(tmp_path / "index.sqlite")
return Repository(db, BlobStore(tmp_path / "spool"), Filters())
def _open_db(path, **kw):
conn = sqlite3.connect(path, **kw)
conn.execute("CREATE TABLE IF NOT EXISTS t(id INTEGER PRIMARY KEY, v TEXT)")
return conn
def _read_snapshot_bytes(repo, path):
data, _ = repo.read_file(path)
return data
def _rows_in_snapshot(data):
import tempfile, os
fd, tmp = tempfile.mkstemp(suffix=".sqlite")
try:
with os.fdopen(fd, "wb") as fh:
fh.write(data)
conn = sqlite3.connect(f"file:{tmp}?mode=ro", uri=True)
try:
assert conn.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
return conn.execute("SELECT v FROM t ORDER BY id").fetchall()
finally:
conn.close()
finally:
os.unlink(tmp)
def test_sqlite_snapshot_roundtrip(tmp_path):
repo = _repo(tmp_path)
db_path = str(tmp_path / "app.db")
conn = _open_db(db_path)
conn.execute("INSERT INTO t(v) VALUES ('a'), ('b')")
conn.commit()
data = _read_snapshot_bytes(repo, db_path)
assert _rows_in_snapshot(data) == [("a",), ("b",)]
result = repo.commit(db_path, data, "test", "edit")
assert result["status"] == "committed"
conn.close()
def test_snapshot_during_uncommitted_write_is_consistent(tmp_path):
"""A writer with an open transaction must not produce a torn version."""
repo = _repo(tmp_path)
db_path = str(tmp_path / "live.db")
writer = _open_db(db_path, isolation_level=None)
writer.execute("INSERT INTO t(v) VALUES ('committed')")
writer.execute("BEGIN IMMEDIATE")
writer.execute("INSERT INTO t(v) VALUES ('uncommitted')")
try:
data = _read_snapshot_bytes(repo, db_path)
finally:
writer.execute("ROLLBACK")
writer.close()
rows = _rows_in_snapshot(data)
assert rows == [("committed",)] # committed state only, verified clean
def test_wal_mode_snapshot(tmp_path):
repo = _repo(tmp_path)
db_path = str(tmp_path / "wal.db")
conn = _open_db(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("INSERT INTO t(v) VALUES ('w1')")
conn.commit()
conn.execute("INSERT INTO t(v) VALUES ('w2')") # may sit in -wal
data = _read_snapshot_bytes(repo, db_path)
conn.close()
rows = _rows_in_snapshot(data)
assert ("w1",) in rows # committed data present, snapshot verified
def test_corrupt_sqlite_magic_rejected(tmp_path):
repo = _repo(tmp_path)
bad = tmp_path / "bad.db"
bad.write_bytes(dbsafe.SQLITE_MAGIC + b"\x00" * 100 + b"garbage")
with pytest.raises(Rejected) as exc:
repo.read_file(str(bad))
assert exc.value.reason in ("database-corrupt", "database-unreadable")
def test_locked_database_rejected_not_stored(tmp_path):
repo = _repo(tmp_path)
db_path = str(tmp_path / "locked.db")
holder = _open_db(db_path, isolation_level=None)
holder.execute("INSERT INTO t(v) VALUES ('x')")
holder.execute("BEGIN EXCLUSIVE")
try:
with pytest.raises(dbsafe.DatabaseUnsafe) as exc:
dbsafe.snapshot_sqlite(db_path, 10_485_760, timeout=0.3)
assert exc.value.reason == "database-locked"
finally:
holder.execute("ROLLBACK")
holder.close()
# After the lock clears, the same file snapshots fine.
assert _rows_in_snapshot(_read_snapshot_bytes(repo, db_path)) == [("x",)]
def test_pushed_sqlite_bytes_verified(tmp_path):
repo = _repo(tmp_path)
db_path = str(tmp_path / "push.db")
with pytest.raises(Rejected):
repo.check_content(dbsafe.SQLITE_MAGIC + b"\x00" * 100 + b"garbage")
# Genuine DB bytes pass.
conn = _open_db(db_path)
conn.execute("INSERT INTO t(v) VALUES ('ok')")
conn.commit()
conn.close()
repo.check_content((tmp_path / "push.db").read_bytes())