965 lines
32 KiB
Python
965 lines
32 KiB
Python
"""
|
|
CLI command tests for HXBooks.
|
|
|
|
Tests all CLI commands for correct behavior, database integration, and output formatting.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from datetime import date
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
from flask import Flask
|
|
|
|
from hxbooks.cli import cli
|
|
from hxbooks.db import db
|
|
from hxbooks.models import Author, Book, Genre, Reading, User
|
|
|
|
|
|
class TestBookAddCommand:
|
|
"""Test the 'hxbooks book add' command."""
|
|
|
|
def test_book_add_basic(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test basic book addition with title and owner."""
|
|
# Run the CLI command
|
|
result = cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"The Hobbit",
|
|
"--owner",
|
|
"frodo",
|
|
"--authors",
|
|
"J.R.R. Tolkien",
|
|
"--genres",
|
|
"Fantasy,Adventure",
|
|
"--isbn",
|
|
"9780547928227",
|
|
"--publisher",
|
|
"Houghton Mifflin Harcourt",
|
|
"--place",
|
|
"home",
|
|
"--bookshelf",
|
|
"living room",
|
|
"--shelf",
|
|
"2",
|
|
"--description",
|
|
"A classic fantasy tale",
|
|
"--notes",
|
|
"First edition",
|
|
],
|
|
)
|
|
|
|
# Verify CLI command succeeded
|
|
assert result.exit_code == 0, f"CLI command failed with output: {result.output}"
|
|
|
|
# Verify success message format
|
|
assert "Added book: The Hobbit (ID:" in result.output
|
|
assert "Created user: frodo" in result.output
|
|
|
|
# Verify database state
|
|
with app.app_context():
|
|
# Check user was created
|
|
users = db.session.execute(db.select(User)).scalars().all()
|
|
assert len(users) == 1
|
|
user = users[0]
|
|
assert user.username == "frodo"
|
|
|
|
# Check book was created with correct fields
|
|
books = (
|
|
db.session
|
|
.execute(db.select(Book).join(Book.authors).join(Book.genres))
|
|
.unique()
|
|
.scalars()
|
|
.all()
|
|
)
|
|
assert len(books) == 1
|
|
book = books[0]
|
|
|
|
assert book.title == "The Hobbit"
|
|
assert book.owner_id == user.id
|
|
assert book.isbn == "9780547928227"
|
|
assert book.publisher == "Houghton Mifflin Harcourt"
|
|
assert book.location_place == "home"
|
|
assert book.location_bookshelf == "living room"
|
|
assert book.location_shelf == 2
|
|
assert book.description == "A classic fantasy tale"
|
|
assert book.notes == "First edition"
|
|
|
|
# Check authors were created and linked
|
|
authors = db.session.execute(db.select(Author)).scalars().all()
|
|
assert len(authors) == 1
|
|
author = authors[0]
|
|
assert author.name == "J.R.R. Tolkien"
|
|
assert book in author.books
|
|
assert author in book.authors
|
|
|
|
# Check genres were created and linked
|
|
genres = db.session.execute(db.select(Genre)).scalars().all()
|
|
assert len(genres) == 2
|
|
genre_names = {genre.name for genre in genres}
|
|
assert genre_names == {"Fantasy", "Adventure"}
|
|
|
|
for genre in genres:
|
|
assert book in genre.books
|
|
assert genre in book.genres
|
|
|
|
def test_book_add_minimal_fields(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test book addition with only required fields."""
|
|
result = cli_runner.invoke(
|
|
cli, ["book", "add", "Minimal Book", "--owner", "alice"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Added book: Minimal Book (ID:" in result.output
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
assert book.title == "Minimal Book"
|
|
assert book.isbn == "" # Default empty string
|
|
assert book.publisher == ""
|
|
assert book.location_shelf is None # Default None
|
|
assert len(book.authors) == 0 # No authors provided
|
|
assert len(book.genres) == 0 # No genres provided
|
|
|
|
|
|
class TestBookListCommand:
|
|
"""Test the 'hxbooks book list' command."""
|
|
|
|
def test_book_list_empty(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing books when database is empty."""
|
|
result = cli_runner.invoke(cli, ["book", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No books found." in result.output
|
|
|
|
def test_book_list_with_books(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing books in table format."""
|
|
# Add test data
|
|
cli_runner.invoke(
|
|
cli,
|
|
["book", "add", "Book One", "--owner", "alice", "--authors", "Author A"],
|
|
)
|
|
cli_runner.invoke(
|
|
cli, ["book", "add", "Book Two", "--owner", "bob", "--authors", "Author B"]
|
|
)
|
|
|
|
result = cli_runner.invoke(cli, ["book", "list"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Book One" in result.output
|
|
assert "Book Two" in result.output
|
|
assert "Author A" in result.output
|
|
assert "Author B" in result.output
|
|
assert "alice" in result.output
|
|
assert "bob" in result.output
|
|
|
|
def test_book_list_json_format(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing books in JSON format."""
|
|
# Add test data
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Test Book",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Test Author",
|
|
"--isbn",
|
|
"1234567890",
|
|
],
|
|
)
|
|
|
|
result = cli_runner.invoke(cli, ["book", "list", "--format", "json"])
|
|
|
|
assert result.exit_code == 0
|
|
books_data = json.loads(result.output)
|
|
assert len(books_data) == 1
|
|
book = books_data[0]
|
|
assert book["title"] == "Test Book"
|
|
assert book["authors"] == ["Test Author"]
|
|
assert book["owner"] == "alice"
|
|
assert book["isbn"] == "1234567890"
|
|
|
|
def test_book_list_filter_by_owner(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test filtering books by owner."""
|
|
# Add books for different owners
|
|
cli_runner.invoke(cli, ["book", "add", "Alice Book", "--owner", "alice"])
|
|
cli_runner.invoke(cli, ["book", "add", "Bob Book", "--owner", "bob"])
|
|
|
|
result = cli_runner.invoke(cli, ["book", "list", "--owner", "alice"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Alice Book" in result.output
|
|
assert "Bob Book" not in result.output
|
|
|
|
def test_book_list_filter_by_location(
|
|
self, app: Flask, cli_runner: CliRunner
|
|
) -> None:
|
|
"""Test filtering books by location."""
|
|
# Add books in different locations
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Home Book",
|
|
"--owner",
|
|
"alice",
|
|
"--place",
|
|
"home",
|
|
"--bookshelf",
|
|
"living",
|
|
"--shelf",
|
|
"1",
|
|
],
|
|
)
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Office Book",
|
|
"--owner",
|
|
"alice",
|
|
"--place",
|
|
"office",
|
|
"--bookshelf",
|
|
"work",
|
|
"--shelf",
|
|
"2",
|
|
],
|
|
)
|
|
|
|
result = cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"list",
|
|
"--place",
|
|
"home",
|
|
"--bookshelf",
|
|
"living",
|
|
"--shelf",
|
|
"1",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Home Book" in result.output
|
|
assert "Office Book" not in result.output
|
|
|
|
|
|
class TestBookSearchCommand:
|
|
"""Test the 'hxbooks book search' command."""
|
|
|
|
def test_book_search_basic(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test basic book search functionality."""
|
|
# Add test books
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"The Hobbit",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Tolkien",
|
|
"--genres",
|
|
"Fantasy",
|
|
],
|
|
)
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Dune",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Herbert",
|
|
"--genres",
|
|
"Sci-Fi",
|
|
],
|
|
)
|
|
|
|
result = cli_runner.invoke(cli, ["book", "search", "Hobbit"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "The Hobbit" in result.output
|
|
assert "Dune" not in result.output
|
|
|
|
def test_book_search_no_results(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test search with no matching results."""
|
|
result = cli_runner.invoke(cli, ["book", "search", "nonexistent"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No books found." in result.output
|
|
|
|
def test_book_search_json_format(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test book search with JSON output."""
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Test Book",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Test Author",
|
|
],
|
|
)
|
|
|
|
result = cli_runner.invoke(cli, ["book", "search", "Test", "--format", "json"])
|
|
|
|
assert result.exit_code == 0
|
|
search_results = json.loads(result.output)
|
|
assert len(search_results) >= 1
|
|
# Results format depends on BookService.search_books_advanced implementation
|
|
|
|
@pytest.mark.parametrize(
|
|
"query,username,expected_titles",
|
|
[
|
|
# String field filters
|
|
("title:Hobbit", "", ["The Hobbit"]),
|
|
("author:Tolkien", "", ["The Fellowship", "The Hobbit"]),
|
|
("genre:Fantasy", "", ["The Fellowship", "The Hobbit"]),
|
|
("owner:alice", "", ["Dune", "The Fellowship", "The Hobbit"]),
|
|
("place:home", "", ["Programming Book", "The Hobbit"]),
|
|
("bookshelf:fantasy", "", ["The Fellowship", "The Hobbit"]),
|
|
# Numeric field filters
|
|
("rating>=4", "", ["Programming Book", "The Hobbit"]),
|
|
("rating=3", "", ["Dune"]),
|
|
("shelf>1", "", ["Programming Book", "The Fellowship"]),
|
|
("year>=1954", "", ["Programming Book", "Dune", "The Fellowship"]),
|
|
# Date field filters
|
|
(
|
|
"added>=2026-03-15",
|
|
"",
|
|
["Programming Book", "Dune", "The Fellowship", "The Hobbit"],
|
|
),
|
|
("bought<2026-01-01", "", ["Programming Book"]),
|
|
# Negation
|
|
("-genre:Fantasy", "", ["Programming Book", "Dune"]),
|
|
("-owner:bob", "", ["Dune", "The Fellowship", "The Hobbit"]),
|
|
# Complex query with multiple filters
|
|
("-genre:Fantasy owner:alice", "", ["Dune"]),
|
|
# User-specific queries
|
|
("rating>=4", "alice", ["The Hobbit"]),
|
|
("is:reading", "alice", ["The Fellowship"]),
|
|
("is:read", "alice", ["Dune", "The Hobbit"]),
|
|
("is:wished", "alice", ["Programming Book"]),
|
|
# Sorting
|
|
(
|
|
"sort:added-desc",
|
|
"",
|
|
["Programming Book", "Dune", "The Fellowship", "The Hobbit"],
|
|
),
|
|
(
|
|
"sort:added-asc",
|
|
"",
|
|
["The Hobbit", "The Fellowship", "Dune", "Programming Book"],
|
|
),
|
|
(
|
|
"sort:read-desc",
|
|
"alice",
|
|
["Dune", "The Hobbit", "Programming Book", "The Fellowship"],
|
|
),
|
|
(
|
|
"sort:owner-asc",
|
|
"",
|
|
["Dune", "The Fellowship", "The Hobbit", "Programming Book"],
|
|
),
|
|
(
|
|
"sort:rating-desc",
|
|
"alice",
|
|
["The Hobbit", "Dune", "Programming Book", "The Fellowship"],
|
|
),
|
|
],
|
|
)
|
|
def test_book_search_advanced_queries(
|
|
self,
|
|
app: Flask,
|
|
cli_runner: CliRunner,
|
|
query: str,
|
|
username: str,
|
|
expected_titles: list[str],
|
|
) -> None:
|
|
"""Test advanced search queries with various field filters."""
|
|
# Set up comprehensive test data
|
|
self._setup_search_test_data(app, cli_runner)
|
|
|
|
# Execute the search query
|
|
result = cli_runner.invoke(
|
|
cli,
|
|
["book", "search", "--format", "json", "--username", username, "--", query],
|
|
)
|
|
|
|
assert result.exit_code == 0, f"Search query '{query}' failed: {result.output}"
|
|
|
|
# Parse results and extract titles
|
|
search_results = json.loads(result.output)
|
|
actual_titles = [book["title"] for book in search_results]
|
|
|
|
# Verify expected titles are present (order doesn't matter)
|
|
assert expected_titles == actual_titles, (
|
|
f"Query '{query}' expected {expected_titles}, got {actual_titles}"
|
|
)
|
|
|
|
def _setup_search_test_data(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Set up comprehensive test data for advanced search testing."""
|
|
# Book 1: The Hobbit - Fantasy, high rating, shelf 1, home
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"The Hobbit",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"J.R.R. Tolkien",
|
|
"--genres",
|
|
"Fantasy,Adventure",
|
|
"--place",
|
|
"home",
|
|
"--bookshelf",
|
|
"fantasy",
|
|
"--shelf",
|
|
"1",
|
|
"--publisher",
|
|
"Allen & Unwin",
|
|
],
|
|
)
|
|
|
|
# Book 2: The Fellowship - Fantasy, high rating, shelf 2, office
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"The Fellowship",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"J.R.R. Tolkien",
|
|
"--genres",
|
|
"Fantasy,Epic",
|
|
"--place",
|
|
"office",
|
|
"--bookshelf",
|
|
"fantasy",
|
|
"--shelf",
|
|
"2",
|
|
"--publisher",
|
|
"Allen & Unwin",
|
|
],
|
|
)
|
|
|
|
# Book 3: Dune - Sci-Fi, medium rating, shelf 1, office
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Dune",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Frank Herbert",
|
|
"--genres",
|
|
"Science Fiction",
|
|
"--place",
|
|
"office",
|
|
"--bookshelf",
|
|
"scifi",
|
|
"--shelf",
|
|
"1",
|
|
"--publisher",
|
|
"Chilton Books",
|
|
],
|
|
)
|
|
|
|
# Book 4: Programming Book - Tech, high rating, shelf 2, home, different owner
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Programming Book",
|
|
"--owner",
|
|
"bob",
|
|
"--authors",
|
|
"Tech Author",
|
|
"--genres",
|
|
"Technology,Programming",
|
|
"--place",
|
|
"home",
|
|
"--bookshelf",
|
|
"tech",
|
|
"--shelf",
|
|
"2",
|
|
"--publisher",
|
|
"Tech Press",
|
|
],
|
|
)
|
|
|
|
# Add some readings and ratings to test rating filters
|
|
with app.app_context():
|
|
# Get book IDs
|
|
books = (
|
|
db.session.execute(db.select(Book).order_by(Book.id)).scalars().all()
|
|
)
|
|
hobbit_id = next(b.id for b in books if b.title == "The Hobbit")
|
|
fellowship_id = next(b.id for b in books if b.title == "The Fellowship")
|
|
dune_id = next(b.id for b in books if b.title == "Dune")
|
|
prog_id = next(b.id for b in books if b.title == "Programming Book")
|
|
|
|
# Start and finish reading sessions with ratings
|
|
cli_runner.invoke(cli, ["reading", "start", str(hobbit_id), "--owner", "alice"])
|
|
cli_runner.invoke(cli, ["reading", "start", str(dune_id), "--owner", "alice"])
|
|
cli_runner.invoke(cli, ["reading", "start", str(prog_id), "--owner", "bob"])
|
|
cli_runner.invoke(
|
|
cli, ["reading", "start", str(fellowship_id), "--owner", "alice"]
|
|
)
|
|
|
|
with app.app_context():
|
|
# Get reading session IDs
|
|
readings = (
|
|
db.session
|
|
.execute(db.select(Reading).order_by(Reading.id))
|
|
.scalars()
|
|
.all()
|
|
)
|
|
hobbit_reading = next(r for r in readings if r.book_id == hobbit_id)
|
|
dune_reading = next(r for r in readings if r.book_id == dune_id)
|
|
prog_reading = next(r for r in readings if r.book_id == prog_id)
|
|
|
|
# Finish with different ratings
|
|
cli_runner.invoke(
|
|
cli, ["reading", "finish", str(hobbit_reading.id), "--rating", "5"]
|
|
)
|
|
cli_runner.invoke(
|
|
cli, ["reading", "finish", str(dune_reading.id), "--rating", "3"]
|
|
)
|
|
cli_runner.invoke(
|
|
cli, ["reading", "finish", str(prog_reading.id), "--rating", "4"]
|
|
)
|
|
|
|
# Update one book with bought_date for date filter testing
|
|
with app.app_context():
|
|
prog_book = db.session.get(Book, prog_id)
|
|
assert prog_book is not None
|
|
prog_book.bought_date = date(2025, 12, 1) # Before 2026-01-01
|
|
prog_book.first_published = 2000
|
|
|
|
hobbit_book = db.session.get(Book, hobbit_id)
|
|
assert hobbit_book is not None
|
|
hobbit_book.first_published = 1937
|
|
|
|
fellowship_book = db.session.get(Book, fellowship_id)
|
|
assert fellowship_book is not None
|
|
fellowship_book.first_published = 1954
|
|
|
|
dune_book = db.session.get(Book, dune_id)
|
|
assert dune_book is not None
|
|
dune_book.first_published = 1965
|
|
|
|
db.session.commit()
|
|
|
|
# Add a book to wishlist
|
|
cli_runner.invoke(
|
|
cli,
|
|
["wishlist", "add", str(prog_id), "--owner", "alice"],
|
|
)
|
|
|
|
|
|
class TestReadingCommands:
|
|
"""Test reading-related CLI commands."""
|
|
|
|
def test_reading_start_basic(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test starting a reading session."""
|
|
# Add a book first
|
|
result = cli_runner.invoke(
|
|
cli, ["book", "add", "Test Book", "--owner", "alice"]
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
# Extract book ID from output
|
|
|
|
book_id_match = re.search(r"ID: (\d+)", result.output)
|
|
assert book_id_match
|
|
book_id = book_id_match.group(1)
|
|
|
|
# Start reading session
|
|
result = cli_runner.invoke(
|
|
cli, ["reading", "start", book_id, "--owner", "alice"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Started reading session" in result.output
|
|
assert f"for book {book_id}" in result.output
|
|
|
|
def test_reading_finish_with_rating(
|
|
self, app: Flask, cli_runner: CliRunner
|
|
) -> None:
|
|
"""Test finishing a reading session with rating."""
|
|
# Add book and start reading
|
|
cli_runner.invoke(cli, ["book", "add", "Test Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
# Get the book ID from database
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["reading", "start", str(book_id), "--owner", "alice"]
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
# Extract reading session ID
|
|
|
|
reading_id_match = re.search(r"Started reading session (\d+)", result.output)
|
|
assert reading_id_match
|
|
reading_id = reading_id_match.group(1)
|
|
|
|
# Finish reading with rating
|
|
result = cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"reading",
|
|
"finish",
|
|
reading_id,
|
|
"--rating",
|
|
"4",
|
|
"--comments",
|
|
"Great book!",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Finished reading: Test Book" in result.output
|
|
assert "Rating: 4/5" in result.output
|
|
|
|
def test_reading_drop(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test dropping a reading session."""
|
|
# Add book and start reading
|
|
cli_runner.invoke(cli, ["book", "add", "Boring Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["reading", "start", str(book_id), "--owner", "alice"]
|
|
)
|
|
|
|
reading_id_match = re.search(r"Started reading session (\d+)", result.output)
|
|
assert reading_id_match is not None
|
|
reading_id = reading_id_match.group(1)
|
|
|
|
# Drop the reading
|
|
result = cli_runner.invoke(
|
|
cli, ["reading", "drop", reading_id, "--comments", "Too boring"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Dropped reading: Boring Book" in result.output
|
|
|
|
def test_reading_list_current(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing current (unfinished) readings."""
|
|
# Add book and start reading
|
|
cli_runner.invoke(cli, ["book", "add", "Current Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
cli_runner.invoke(cli, ["reading", "start", str(book_id), "--owner", "alice"])
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["reading", "list", "--owner", "alice", "--current"]
|
|
)
|
|
|
|
assert result.exit_code == 0, f"CLI command failed with output: {result.output}"
|
|
assert "Current Book" in result.output
|
|
assert "Reading" in result.output
|
|
|
|
def test_reading_list_json_format(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing readings in JSON format."""
|
|
# Add book and start reading
|
|
cli_runner.invoke(cli, ["book", "add", "JSON Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
cli_runner.invoke(cli, ["reading", "start", str(book_id), "--owner", "alice"])
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["reading", "list", "--owner", "alice", "--format", "json"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
readings_data = json.loads(result.output)
|
|
assert len(readings_data) == 1
|
|
reading = readings_data[0]
|
|
assert reading["book_title"] == "JSON Book"
|
|
assert reading["finished"] is False
|
|
|
|
|
|
class TestWishlistCommands:
|
|
"""Test wishlist-related CLI commands."""
|
|
|
|
def test_wishlist_add(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test adding a book to wishlist."""
|
|
# Add a book first
|
|
cli_runner.invoke(cli, ["book", "add", "Desired Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["wishlist", "add", str(book_id), "--owner", "alice"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert "Added 'Desired Book' to wishlist" in result.output
|
|
|
|
def test_wishlist_remove(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test removing a book from wishlist."""
|
|
# Add book and add to wishlist
|
|
cli_runner.invoke(cli, ["book", "add", "Unwanted Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
cli_runner.invoke(cli, ["wishlist", "add", str(book_id), "--owner", "alice"])
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["wishlist", "remove", str(book_id), "--owner", "alice"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert f"Removed book {book_id} from wishlist" in result.output
|
|
|
|
def test_wishlist_remove_not_in_list(
|
|
self, app: Flask, cli_runner: CliRunner
|
|
) -> None:
|
|
"""Test removing a book that's not in wishlist."""
|
|
# Add book but don't add to wishlist
|
|
cli_runner.invoke(cli, ["book", "add", "Not Wished Book", "--owner", "alice"])
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["wishlist", "remove", str(book_id), "--owner", "alice"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert f"Book {book_id} was not in wishlist" in result.output
|
|
|
|
def test_wishlist_list_empty(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing empty wishlist."""
|
|
result = cli_runner.invoke(cli, ["wishlist", "list", "--owner", "alice"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Wishlist is empty." in result.output
|
|
|
|
def test_wishlist_list_with_items(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing wishlist with items."""
|
|
# Add books and add to wishlist
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Wished Book 1",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Author One",
|
|
],
|
|
)
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Wished Book 2",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Author Two",
|
|
],
|
|
)
|
|
|
|
with app.app_context():
|
|
books = db.session.execute(db.select(Book)).scalars().all()
|
|
for book in books:
|
|
result = cli_runner.invoke(
|
|
cli, ["wishlist", "add", str(book.id), "--owner", "alice"]
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
result = cli_runner.invoke(cli, ["wishlist", "list", "--owner", "alice"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Wished Book 1" in result.output
|
|
assert "Wished Book 2" in result.output
|
|
assert "Author One" in result.output
|
|
assert "Author Two" in result.output
|
|
|
|
def test_wishlist_list_json_format(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test listing wishlist in JSON format."""
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"JSON Wished Book",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"JSON Author",
|
|
],
|
|
)
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
cli_runner.invoke(cli, ["wishlist", "add", str(book_id), "--owner", "alice"])
|
|
|
|
result = cli_runner.invoke(
|
|
cli, ["wishlist", "list", "--owner", "alice", "--format", "json"]
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
wishlist_data = json.loads(result.output)
|
|
assert len(wishlist_data) == 1
|
|
item = wishlist_data[0]
|
|
assert item["title"] == "JSON Wished Book"
|
|
assert item["authors"] == ["JSON Author"]
|
|
|
|
|
|
class TestDatabaseCommands:
|
|
"""Test database management CLI commands."""
|
|
|
|
def test_db_init(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test database initialization."""
|
|
result = cli_runner.invoke(cli, ["db", "init"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Database initialized." in result.output
|
|
|
|
def test_db_seed(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test database seeding with sample data."""
|
|
result = cli_runner.invoke(cli, ["db", "seed", "--owner", "test_owner"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Created: The Hobbit" in result.output
|
|
assert "Created: Dune" in result.output
|
|
assert "Created: The Pragmatic Programmer" in result.output
|
|
assert "Created 3 sample books for user 'test_owner'" in result.output
|
|
|
|
# Verify books were actually created
|
|
with app.app_context():
|
|
books = db.session.execute(db.select(Book)).scalars().all()
|
|
assert len(books) == 3
|
|
titles = {book.title for book in books}
|
|
assert "The Hobbit" in titles
|
|
assert "Dune" in titles
|
|
assert "The Pragmatic Programmer" in titles
|
|
|
|
def test_db_status_empty(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test database status with empty database."""
|
|
result = cli_runner.invoke(cli, ["db", "status"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Database Statistics:" in result.output
|
|
assert "Books: 0" in result.output
|
|
assert "Authors: 0" in result.output
|
|
assert "Genres: 0" in result.output
|
|
assert "Users: 0" in result.output
|
|
assert "Reading sessions: 0" in result.output
|
|
assert "Wishlist items: 0" in result.output
|
|
|
|
def test_db_status_with_data(self, app: Flask, cli_runner: CliRunner) -> None:
|
|
"""Test database status with sample data."""
|
|
# Add some test data
|
|
cli_runner.invoke(
|
|
cli,
|
|
[
|
|
"book",
|
|
"add",
|
|
"Status Book",
|
|
"--owner",
|
|
"alice",
|
|
"--authors",
|
|
"Status Author",
|
|
"--genres",
|
|
"Status Genre",
|
|
],
|
|
)
|
|
|
|
with app.app_context():
|
|
book = db.session.execute(db.select(Book)).scalar_one()
|
|
book_id = book.id
|
|
|
|
# Add reading and wishlist entries
|
|
cli_runner.invoke(cli, ["reading", "start", str(book_id), "--owner", "alice"])
|
|
cli_runner.invoke(cli, ["wishlist", "add", str(book_id), "--owner", "bob"])
|
|
|
|
result = cli_runner.invoke(cli, ["db", "status"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Books: 1" in result.output
|
|
assert "Authors: 1" in result.output
|
|
assert "Genres: 1" in result.output
|
|
assert "Users: 2" in result.output # alice and bob
|
|
assert "Reading sessions: 1" in result.output
|
|
assert "Wishlist items: 1" in result.output
|
|
|
|
|
|
class TestErrorScenarios:
|
|
"""Test error handling and edge cases."""
|
|
|
|
def test_reading_start_invalid_book_id(
|
|
self, app: Flask, cli_runner: CliRunner
|
|
) -> None:
|
|
"""Test starting reading with non-existent book ID."""
|
|
result = cli_runner.invoke(cli, ["reading", "start", "999", "--owner", "alice"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "Error starting reading:" in result.output
|
|
|
|
def test_wishlist_add_invalid_book_id(
|
|
self, app: Flask, cli_runner: CliRunner
|
|
) -> None:
|
|
"""Test adding non-existent book to wishlist."""
|
|
result = cli_runner.invoke(cli, ["wishlist", "add", "999", "--owner", "alice"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "Error adding to wishlist:" in result.output
|
|
|
|
def test_reading_finish_invalid_reading_id(
|
|
self, app: Flask, cli_runner: CliRunner
|
|
) -> None:
|
|
"""Test finishing non-existent reading session."""
|
|
result = cli_runner.invoke(cli, ["reading", "finish", "999"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "Error finishing reading:" in result.output
|