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.
150 lines
5.3 KiB
Python
150 lines
5.3 KiB
Python
"""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()
|