Initial commit
This commit is contained in:
298
backend/tests/unit/test_services/test_bookshelf_service.py
Normal file
298
backend/tests/unit/test_services/test_bookshelf_service.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""Tests for LibraryService"""
|
||||
|
||||
import pytest
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from chitai.services import ShelfService
|
||||
from chitai.database import models as m
|
||||
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from chitai.services.bookshelf import ShelfService
|
||||
from chitai.services import BookService
|
||||
from chitai.database.models.book_list import BookList, BookListLink
|
||||
from chitai.database import models as m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session(bookshelf_service: ShelfService) -> AsyncSession:
|
||||
return bookshelf_service.repository.session
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def test_books(db_session: AsyncSession) -> list[m.Book]:
|
||||
"""Create test books in the database."""
|
||||
|
||||
library = m.Library(
|
||||
name="Default Library",
|
||||
slug="default-library",
|
||||
root_path="./path",
|
||||
path_template="{author}/{title}",
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
db_session.add(library)
|
||||
|
||||
books = [m.Book(title=f"Book {i}", library_id=1) for i in range(1, 8)]
|
||||
db_session.add_all(books)
|
||||
|
||||
user = m.User(email="test_user@example.com", password="password123")
|
||||
db_session.add(user)
|
||||
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
return books
|
||||
|
||||
|
||||
class TestAddBooks:
|
||||
async def test_add_books_to_empty_shelf(
|
||||
self, bookshelf_service: ShelfService, db_session
|
||||
) -> None:
|
||||
"""Successfully add books to an empty shelf."""
|
||||
# Create a shelf
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Add books
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3])
|
||||
|
||||
# Verify books were added
|
||||
links = await db_session.execute(
|
||||
select(BookListLink).where(BookListLink.list_id == shelf.id)
|
||||
)
|
||||
added_links = links.scalars().all()
|
||||
assert len(added_links) == 3
|
||||
assert {link.book_id for link in added_links} == {1, 2, 3}
|
||||
|
||||
async def test_add_books_preserves_positions(
|
||||
self, bookshelf_service: ShelfService, db_session
|
||||
) -> None:
|
||||
"""Verify books are assigned correct positions on the shelf."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink)
|
||||
.where(BookListLink.list_id == shelf.id)
|
||||
.order_by(BookListLink.position)
|
||||
)
|
||||
added_links = links.scalars().all()
|
||||
|
||||
# Verify positions are sequential starting from 0
|
||||
assert [link.position for link in added_links] == [0, 1, 2]
|
||||
|
||||
async def test_add_books_to_shelf_with_existing_books(
|
||||
self, bookshelf_service: ShelfService, db_session
|
||||
) -> None:
|
||||
"""Adding books to a shelf with existing books assigns correct positions."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Add initial books
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2])
|
||||
|
||||
# Add more books
|
||||
await bookshelf_service.add_books(shelf.id, [3, 4])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink)
|
||||
.where(BookListLink.list_id == shelf.id)
|
||||
.order_by(BookListLink.position)
|
||||
)
|
||||
added_links = links.scalars().all()
|
||||
|
||||
# Verify new books continue from position 2
|
||||
assert len(added_links) == 4
|
||||
assert [link.position for link in added_links] == [0, 1, 2, 3]
|
||||
|
||||
async def test_add_duplicate_books_is_idempotent(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Adding books already on shelf should not create duplicates."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Add books
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3])
|
||||
|
||||
# Try to add overlapping books
|
||||
await bookshelf_service.add_books(shelf.id, [2, 3, 4])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink).where(BookListLink.list_id == shelf.id)
|
||||
)
|
||||
added_links = links.scalars().all()
|
||||
|
||||
# Should have 4 books total (1, 2, 3, 4), not 7
|
||||
assert len(added_links) == 4
|
||||
assert {link.book_id for link in added_links} == {1, 2, 3, 4}
|
||||
|
||||
async def test_add_books_raises_on_nonexistent_shelf(
|
||||
self, bookshelf_service: ShelfService
|
||||
) -> None:
|
||||
"""Adding books to nonexistent shelf raises error."""
|
||||
with pytest.raises(Exception): # Could be SQLAlchemyError or specific error
|
||||
await bookshelf_service.add_books(99999, [1, 2, 3])
|
||||
|
||||
async def test_add_nonexistent_books_raises_error(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Adding nonexistent books raises ValueError."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
with pytest.raises(ValueError, match="One or more books not found"):
|
||||
await bookshelf_service.add_books(shelf.id, [99999, 99998])
|
||||
|
||||
async def test_add_partial_nonexistent_books_raises_error(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Adding a mix of existent and nonexistent books raises error."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Book 1 might exist, but 99999 doesn't
|
||||
with pytest.raises(ValueError, match="One or more books not found"):
|
||||
await bookshelf_service.add_books(shelf.id, [1, 99999])
|
||||
|
||||
async def test_add_empty_book_list(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Adding empty book list should return without error."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Should not raise
|
||||
await bookshelf_service.add_books(shelf.id, [])
|
||||
|
||||
|
||||
class TestRemoveBooks:
|
||||
async def test_remove_books_from_shelf(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Successfully remove books from a shelf."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Add books
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3, 4])
|
||||
|
||||
# Remove some books
|
||||
await bookshelf_service.remove_books(shelf.id, [2, 3])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink).where(BookListLink.list_id == shelf.id)
|
||||
)
|
||||
remaining_links = links.scalars().all()
|
||||
|
||||
assert len(remaining_links) == 2
|
||||
assert {link.book_id for link in remaining_links} == {1, 4}
|
||||
|
||||
async def test_remove_all_books_from_shelf(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Removing all books should leave shelf empty."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3])
|
||||
await bookshelf_service.remove_books(shelf.id, [1, 2, 3])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink).where(BookListLink.list_id == shelf.id)
|
||||
)
|
||||
remaining_links = links.scalars().all()
|
||||
|
||||
assert len(remaining_links) == 0
|
||||
|
||||
async def test_remove_nonexistent_book_is_idempotent(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Removing books not on shelf should not raise error."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3])
|
||||
|
||||
# Remove books that don't exist on shelf - should not raise
|
||||
await bookshelf_service.remove_books(shelf.id, [99, 100])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink).where(BookListLink.list_id == shelf.id)
|
||||
)
|
||||
remaining_links = links.scalars().all()
|
||||
|
||||
# Original books should still be there
|
||||
assert len(remaining_links) == 3
|
||||
|
||||
async def test_remove_books_raises_on_nonexistent_shelf(
|
||||
self, bookshelf_service: ShelfService
|
||||
) -> None:
|
||||
"""Removing books from nonexistent shelf raises error."""
|
||||
with pytest.raises(Exception):
|
||||
await bookshelf_service.remove_books(99999, [1, 2, 3])
|
||||
|
||||
async def test_remove_empty_book_list(
|
||||
self, bookshelf_service: ShelfService, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""Removing empty book list should return without error."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3])
|
||||
|
||||
# Should not raise
|
||||
await bookshelf_service.remove_books(shelf.id, [])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink).where(BookListLink.list_id == shelf.id)
|
||||
)
|
||||
remaining_links = links.scalars().all()
|
||||
|
||||
# All books should still be there
|
||||
assert len(remaining_links) == 3
|
||||
|
||||
async def test_add_remove_add_sequence(
|
||||
self, bookshelf_service: ShelfService, db_session
|
||||
) -> None:
|
||||
"""Add books, remove from middle, then add more - positions should be maintained."""
|
||||
shelf = BookList(title="Test Shelf", user_id=1)
|
||||
db_session.add(shelf)
|
||||
await db_session.flush()
|
||||
|
||||
# Add initial books [1, 2, 3, 4, 5]
|
||||
await bookshelf_service.add_books(shelf.id, [1, 2, 3, 4, 5])
|
||||
|
||||
# Remove books from middle [2, 3, 4]
|
||||
await bookshelf_service.remove_books(shelf.id, [2, 3, 4])
|
||||
|
||||
# Add more books [6, 7]
|
||||
await bookshelf_service.add_books(shelf.id, [6, 7])
|
||||
|
||||
links = await db_session.execute(
|
||||
select(BookListLink)
|
||||
.where(BookListLink.list_id == shelf.id)
|
||||
.order_by(BookListLink.position)
|
||||
)
|
||||
final_links = links.scalars().all()
|
||||
|
||||
# Should have [1, 5, 6, 7] with positions [0, 1, 2, 3]
|
||||
assert len(final_links) == 4
|
||||
assert [link.book_id for link in final_links] == [1, 5, 6, 7]
|
||||
assert [link.position for link in final_links] == [0, 1, 2, 3]
|
||||
Reference in New Issue
Block a user