Initial commit

Photo-based book cataloger with AI identification.
Room → Cabinet → Shelf → Book hierarchy; FastAPI + SQLite backend;
vanilla JS SPA; OpenAI-compatible plugin system for boundary
detection, text recognition, and archive search.
This commit is contained in:
night
2026-03-09 14:11:11 +03:00
committed by Petr Polezhaev
commit 5d5f26c8ae
64 changed files with 8605 additions and 0 deletions

149
tests/test_storage.py Normal file
View File

@@ -0,0 +1,149 @@
"""Unit tests for db.py, files.py, and config.py: DB helpers, name/position counters, settings merge."""
import sqlite3
from collections.abc import Iterator
from pathlib import Path
import pytest
import db
import files
from config import deep_merge
@pytest.fixture(autouse=True)
def reset_counters() -> Iterator[None]:
db.COUNTERS.clear()
yield
db.COUNTERS.clear()
@pytest.fixture
def test_db(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[sqlite3.Connection]:
"""Temporary SQLite database with full schema applied."""
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
monkeypatch.setattr(files, "DATA_DIR", tmp_path)
monkeypatch.setattr(files, "IMAGES_DIR", tmp_path / "images")
files.init_dirs()
db.init_db()
connection = db.conn()
yield connection
connection.close()
# ── deep_merge ────────────────────────────────────────────────────────────────
def test_deep_merge_basic() -> None:
result = deep_merge({"a": 1, "b": 2}, {"b": 3, "c": 4})
assert result == {"a": 1, "b": 3, "c": 4}
def test_deep_merge_nested() -> None:
base = {"x": {"a": 1, "b": 2}}
override = {"x": {"b": 99, "c": 3}}
result = deep_merge(base, override)
assert result == {"x": {"a": 1, "b": 99, "c": 3}}
def test_deep_merge_list_replacement() -> None:
base = {"items": [1, 2, 3]}
override = {"items": [4, 5]}
result = deep_merge(base, override)
assert result["items"] == [4, 5]
def test_deep_merge_does_not_mutate_base() -> None:
base = {"a": {"x": 1}}
deep_merge(base, {"a": {"x": 2}})
assert base["a"]["x"] == 1
# ── uid / now ────────────────────────────────────────────────────────────────
def test_uid_unique() -> None:
assert db.uid() != db.uid()
def test_uid_is_string() -> None:
result = db.uid()
assert isinstance(result, str)
assert len(result) == 36 # UUID4 format
def test_now_is_string() -> None:
result = db.now()
assert isinstance(result, str)
assert "T" in result # ISO format
# ── next_name ────────────────────────────────────────────────────────────────
def test_next_name_increments() -> None:
assert db.next_name("Room") == "Room 1"
assert db.next_name("Room") == "Room 2"
assert db.next_name("Room") == "Room 3"
def test_next_name_independent_prefixes() -> None:
assert db.next_name("Room") == "Room 1"
assert db.next_name("Shelf") == "Shelf 1"
assert db.next_name("Room") == "Room 2"
# ── next_pos / next_root_pos ────────────────────────────────────────────────
def test_next_root_pos_empty(test_db: sqlite3.Connection) -> None:
pos = db.next_root_pos(test_db, "rooms")
assert pos == 1
def test_next_root_pos_with_rows(test_db: sqlite3.Connection) -> None:
ts = db.now()
test_db.execute("INSERT INTO rooms VALUES (?,?,?,?)", ["r1", "Room 1", 1, ts])
test_db.execute("INSERT INTO rooms VALUES (?,?,?,?)", ["r2", "Room 2", 2, ts])
test_db.commit()
assert db.next_root_pos(test_db, "rooms") == 3
def test_next_pos_empty(test_db: sqlite3.Connection) -> None:
ts = db.now()
test_db.execute("INSERT INTO rooms VALUES (?,?,?,?)", ["r1", "Room", 1, ts])
test_db.commit()
pos = db.next_pos(test_db, "cabinets", "room_id", "r1")
assert pos == 1
def test_next_pos_with_children(test_db: sqlite3.Connection) -> None:
ts = db.now()
test_db.execute("INSERT INTO rooms VALUES (?,?,?,?)", ["r1", "Room", 1, ts])
test_db.execute("INSERT INTO cabinets VALUES (?,?,?,?,?,?,?,?)", ["c1", "r1", "C1", None, None, None, 1, ts])
test_db.execute("INSERT INTO cabinets VALUES (?,?,?,?,?,?,?,?)", ["c2", "r1", "C2", None, None, None, 2, ts])
test_db.commit()
pos = db.next_pos(test_db, "cabinets", "room_id", "r1")
assert pos == 3
# ── init_db ────────────────────────────────────────────────────────────────────
def test_init_db_creates_tables(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
db.init_db()
connection = sqlite3.connect(tmp_path / "test.db")
tables = {row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
connection.close()
assert {"rooms", "cabinets", "shelves", "books"}.issubset(tables)
# ── init_dirs ─────────────────────────────────────────────────────────────────
def test_init_dirs_creates_images_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(files, "DATA_DIR", tmp_path)
monkeypatch.setattr(files, "IMAGES_DIR", tmp_path / "images")
files.init_dirs()
assert (tmp_path / "images").is_dir()