Linted and formatted everything new

This commit is contained in:
2026-03-16 02:42:23 +01:00
parent 40ca08359f
commit d427cec8d5
18 changed files with 1410 additions and 1209 deletions

View File

@@ -6,22 +6,21 @@ Tests all CLI commands for correct behavior, database integration, and output fo
import json
import re
import tempfile
from datetime import date
from pathlib import Path
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, Wishlist
from hxbooks.models import Author, Book, Genre, Reading, User
class TestBookAddCommand:
"""Test the 'hxbooks book add' command."""
def test_book_add_basic(self, app, cli_runner):
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(
@@ -70,7 +69,8 @@ class TestBookAddCommand:
# Check book was created with correct fields
books = (
db.session.execute(db.select(Book).join(Book.authors).join(Book.genres))
db.session
.execute(db.select(Book).join(Book.authors).join(Book.genres))
.unique()
.scalars()
.all()
@@ -106,7 +106,7 @@ class TestBookAddCommand:
assert book in genre.books
assert genre in book.genres
def test_book_add_minimal_fields(self, app, cli_runner):
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"]
@@ -124,7 +124,9 @@ class TestBookAddCommand:
assert len(book.authors) == 0 # No authors provided
assert len(book.genres) == 0 # No genres provided
def test_book_add_missing_owner_fails(self, app, cli_runner):
def test_book_add_missing_owner_fails(
self, app: Flask, cli_runner: CliRunner
) -> None:
"""Test that book addition fails when owner is not provided."""
result = cli_runner.invoke(
cli,
@@ -144,14 +146,14 @@ class TestBookAddCommand:
class TestBookListCommand:
"""Test the 'hxbooks book list' command."""
def test_book_list_empty(self, app, cli_runner):
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, cli_runner):
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(
@@ -172,7 +174,7 @@ class TestBookListCommand:
assert "alice" in result.output
assert "bob" in result.output
def test_book_list_json_format(self, app, cli_runner):
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(
@@ -201,7 +203,7 @@ class TestBookListCommand:
assert book["owner"] == "alice"
assert book["isbn"] == "1234567890"
def test_book_list_filter_by_owner(self, app, cli_runner):
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"])
@@ -213,7 +215,9 @@ class TestBookListCommand:
assert "Alice Book" in result.output
assert "Bob Book" not in result.output
def test_book_list_filter_by_location(self, app, cli_runner):
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(
@@ -271,7 +275,7 @@ class TestBookListCommand:
class TestBookSearchCommand:
"""Test the 'hxbooks book search' command."""
def test_book_search_basic(self, app, cli_runner):
def test_book_search_basic(self, app: Flask, cli_runner: CliRunner) -> None:
"""Test basic book search functionality."""
# Add test books
cli_runner.invoke(
@@ -309,14 +313,14 @@ class TestBookSearchCommand:
assert "The Hobbit" in result.output
assert "Dune" not in result.output
def test_book_search_no_results(self, app, cli_runner):
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, cli_runner):
def test_book_search_json_format(self, app: Flask, cli_runner: CliRunner) -> None:
"""Test book search with JSON output."""
cli_runner.invoke(
cli,
@@ -378,8 +382,8 @@ class TestBookSearchCommand:
],
)
def test_book_search_advanced_queries(
self, app, cli_runner, query, expected_titles
):
self, app: Flask, cli_runner: CliRunner, query: 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)
@@ -400,7 +404,7 @@ class TestBookSearchCommand:
f"Query '{query}' expected {expected_titles}, got {actual_titles}"
)
def _setup_search_test_data(self, app, cli_runner):
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(
@@ -517,7 +521,8 @@ class TestBookSearchCommand:
with app.app_context():
# Get reading session IDs
readings = (
db.session.execute(db.select(Reading).order_by(Reading.id))
db.session
.execute(db.select(Reading).order_by(Reading.id))
.scalars()
.all()
)
@@ -539,16 +544,20 @@ class TestBookSearchCommand:
# 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()
@@ -557,7 +566,7 @@ class TestBookSearchCommand:
class TestReadingCommands:
"""Test reading-related CLI commands."""
def test_reading_start_basic(self, app, cli_runner):
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(
@@ -566,7 +575,6 @@ class TestReadingCommands:
assert result.exit_code == 0
# Extract book ID from output
import re
book_id_match = re.search(r"ID: (\d+)", result.output)
assert book_id_match
@@ -578,10 +586,12 @@ class TestReadingCommands:
)
assert result.exit_code == 0
assert f"Started reading session" in result.output
assert "Started reading session" in result.output
assert f"for book {book_id}" in result.output
def test_reading_finish_with_rating(self, app, cli_runner):
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"])
@@ -597,7 +607,6 @@ class TestReadingCommands:
assert result.exit_code == 0
# Extract reading session ID
import re
reading_id_match = re.search(r"Started reading session (\d+)", result.output)
assert reading_id_match
@@ -621,7 +630,7 @@ class TestReadingCommands:
assert "Finished reading: Test Book" in result.output
assert "Rating: 4/5" in result.output
def test_reading_drop(self, app, cli_runner):
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"])
@@ -634,9 +643,8 @@ class TestReadingCommands:
cli, ["reading", "start", str(book_id), "--owner", "alice"]
)
import re
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
@@ -647,7 +655,7 @@ class TestReadingCommands:
assert result.exit_code == 0
assert "Dropped reading: Boring Book" in result.output
def test_reading_list_current(self, app, cli_runner):
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"])
@@ -666,7 +674,7 @@ class TestReadingCommands:
assert "Current Book" in result.output
assert "Reading" in result.output
def test_reading_list_json_format(self, app, cli_runner):
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"])
@@ -692,7 +700,7 @@ class TestReadingCommands:
class TestWishlistCommands:
"""Test wishlist-related CLI commands."""
def test_wishlist_add(self, app, cli_runner):
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"])
@@ -708,7 +716,7 @@ class TestWishlistCommands:
assert result.exit_code == 0
assert "Added 'Desired Book' to wishlist" in result.output
def test_wishlist_remove(self, app, cli_runner):
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"])
@@ -726,7 +734,9 @@ class TestWishlistCommands:
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, cli_runner):
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"])
@@ -742,14 +752,14 @@ class TestWishlistCommands:
assert result.exit_code == 0
assert f"Book {book_id} was not in wishlist" in result.output
def test_wishlist_list_empty(self, app, cli_runner):
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, cli_runner):
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(
@@ -793,7 +803,7 @@ class TestWishlistCommands:
assert "Author One" in result.output
assert "Author Two" in result.output
def test_wishlist_list_json_format(self, app, cli_runner):
def test_wishlist_list_json_format(self, app: Flask, cli_runner: CliRunner) -> None:
"""Test listing wishlist in JSON format."""
cli_runner.invoke(
cli,
@@ -829,14 +839,14 @@ class TestWishlistCommands:
class TestDatabaseCommands:
"""Test database management CLI commands."""
def test_db_init(self, app, cli_runner):
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, cli_runner):
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"])
@@ -855,7 +865,7 @@ class TestDatabaseCommands:
assert "Dune" in titles
assert "The Pragmatic Programmer" in titles
def test_db_status_empty(self, app, cli_runner):
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"])
@@ -868,7 +878,7 @@ class TestDatabaseCommands:
assert "Reading sessions: 0" in result.output
assert "Wishlist items: 0" in result.output
def test_db_status_with_data(self, app, cli_runner):
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(
@@ -908,21 +918,27 @@ class TestDatabaseCommands:
class TestErrorScenarios:
"""Test error handling and edge cases."""
def test_reading_start_invalid_book_id(self, app, cli_runner):
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, cli_runner):
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, cli_runner):
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"])