299 lines
10 KiB
Python
299 lines
10 KiB
Python
"""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]
|