- 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.
127 lines
4.1 KiB
Python
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())
|