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:
149
tests/test_storage.py
Normal file
149
tests/test_storage.py
Normal 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()
|
||||
Reference in New Issue
Block a user