"""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]