Fix boundary/child count invariant on shelf and book deletion

When deleting a shelf or book, remove the corresponding boundary from
the parent's boundary list so len(boundaries) == len(children) - 1
is maintained. Add API-level tests covering first, middle, and last
child deletion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 19:45:55 +03:00
parent 7095cbaa60
commit ce03046e51
2 changed files with 175 additions and 2 deletions

157
tests/test_api.py Normal file
View File

@@ -0,0 +1,157 @@
"""API-level integration tests.
Uses a minimal FastAPI app (router only, no lifespan) so tests run against
a temporary SQLite database without needing config or plugins.
"""
import json
from collections.abc import Iterator
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
import db
import files
from api import router
# ── Fixtures ──────────────────────────────────────────────────────────────────
_app = FastAPI()
_app.include_router(router)
@pytest.fixture
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]:
"""TestClient backed by a fresh temporary database."""
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()
db.COUNTERS.clear()
with TestClient(_app) as c:
yield c
db.COUNTERS.clear()
def _seed(tmp_path: Path) -> dict[str, str]:
"""Create room/cabinet/shelves/books with boundaries via db helpers; return their IDs."""
with db.transaction() as c:
room = db.create_room(c)
cab = db.create_cabinet(c, room.id)
s_a = db.create_shelf(c, cab.id)
s_b = db.create_shelf(c, cab.id)
s_c = db.create_shelf(c, cab.id)
b_a = db.create_book(c, s_a.id)
b_b = db.create_book(c, s_a.id)
b_c = db.create_book(c, s_a.id)
# 3 shelves → 2 interior boundaries
db.set_cabinet_boundaries(c, cab.id, json.dumps([0.33, 0.66]))
# 3 books in shelf_a → 2 interior boundaries
db.set_shelf_boundaries(c, s_a.id, json.dumps([0.33, 0.66]))
return {
"room": room.id,
"cabinet": cab.id,
"shelf_a": s_a.id,
"shelf_b": s_b.id,
"shelf_c": s_c.id,
"book_a": b_a.id,
"book_b": b_b.id,
"book_c": b_c.id,
}
# ── Shelf deletion / boundary cleanup ─────────────────────────────────────────
def test_delete_first_shelf_removes_first_boundary(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
ids = _seed(tmp_path)
resp = client.delete(f"/api/shelves/{ids['shelf_a']}")
assert resp.status_code == 200
with db.connection() as c:
cab = db.get_cabinet(c, ids["cabinet"])
assert cab is not None
bounds = json.loads(cab.shelf_boundaries or "[]")
assert bounds == [0.66] # first boundary (0.33) removed
def test_delete_middle_shelf_removes_middle_boundary(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
ids = _seed(tmp_path)
resp = client.delete(f"/api/shelves/{ids['shelf_b']}")
assert resp.status_code == 200
with db.connection() as c:
cab = db.get_cabinet(c, ids["cabinet"])
assert cab is not None
bounds = json.loads(cab.shelf_boundaries or "[]")
assert bounds == [0.33] # middle boundary (0.66) removed
def test_delete_last_shelf_removes_last_boundary(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
ids = _seed(tmp_path)
resp = client.delete(f"/api/shelves/{ids['shelf_c']}")
assert resp.status_code == 200
with db.connection() as c:
cab = db.get_cabinet(c, ids["cabinet"])
assert cab is not None
bounds = json.loads(cab.shelf_boundaries or "[]")
assert bounds == [0.33] # last boundary (0.66) removed
def test_delete_only_shelf_leaves_no_boundaries(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Deleting the sole shelf (no boundaries) leaves boundaries unchanged (empty)."""
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
ids = _seed(tmp_path)
# Delete two shelves first to leave only one
client.delete(f"/api/shelves/{ids['shelf_b']}")
client.delete(f"/api/shelves/{ids['shelf_c']}")
resp = client.delete(f"/api/shelves/{ids['shelf_a']}")
assert resp.status_code == 200
with db.connection() as c:
cab = db.get_cabinet(c, ids["cabinet"])
assert cab is not None
bounds = json.loads(cab.shelf_boundaries or "[]")
assert bounds == []
# ── Book deletion / boundary cleanup ──────────────────────────────────────────
def test_delete_first_book_removes_first_boundary(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
ids = _seed(tmp_path)
resp = client.delete(f"/api/books/{ids['book_a']}")
assert resp.status_code == 200
with db.connection() as c:
shelf = db.get_shelf(c, ids["shelf_a"])
assert shelf is not None
bounds = json.loads(shelf.book_boundaries or "[]")
assert bounds == [0.66]
def test_delete_last_book_removes_last_boundary(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
ids = _seed(tmp_path)
resp = client.delete(f"/api/books/{ids['book_c']}")
assert resp.status_code == 200
with db.connection() as c:
shelf = db.get_shelf(c, ids["shelf_a"])
assert shelf is not None
bounds = json.loads(shelf.book_boundaries or "[]")
assert bounds == [0.33]