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>
158 lines
5.7 KiB
Python
158 lines
5.7 KiB
Python
"""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]
|