Extended search functionality
This commit is contained in:
@@ -124,24 +124,6 @@ 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: Flask, cli_runner: CliRunner
|
||||
) -> None:
|
||||
"""Test that book addition fails when owner is not provided."""
|
||||
result = cli_runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"book",
|
||||
"add",
|
||||
"Test Book",
|
||||
# Missing --owner parameter
|
||||
],
|
||||
)
|
||||
|
||||
# Should fail with exit code 2 (Click validation error)
|
||||
assert result.exit_code == 2
|
||||
assert "Missing option '--owner'" in result.output
|
||||
|
||||
|
||||
class TestBookListCommand:
|
||||
"""Test the 'hxbooks book list' command."""
|
||||
@@ -343,46 +325,46 @@ class TestBookSearchCommand:
|
||||
# Results format depends on BookService.search_books_advanced implementation
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query,expected_titles",
|
||||
"query,username,expected_titles",
|
||||
[
|
||||
# String field filters
|
||||
("title:Hobbit", ["The Hobbit"]),
|
||||
("author:Tolkien", ["The Hobbit", "The Fellowship"]),
|
||||
("genre:Fantasy", ["The Hobbit", "The Fellowship"]),
|
||||
("owner:alice", ["The Hobbit", "The Fellowship", "Dune"]),
|
||||
("place:home", ["The Hobbit", "Programming Book"]),
|
||||
("bookshelf:fantasy", ["The Hobbit", "The Fellowship"]),
|
||||
("title:Hobbit", "", ["The Hobbit"]),
|
||||
("author:Tolkien", "", ["The Hobbit", "The Fellowship"]),
|
||||
("genre:Fantasy", "", ["The Hobbit", "The Fellowship"]),
|
||||
("owner:alice", "", ["The Hobbit", "The Fellowship", "Dune"]),
|
||||
("place:home", "", ["The Hobbit", "Programming Book"]),
|
||||
("bookshelf:fantasy", "", ["The Hobbit", "The Fellowship"]),
|
||||
# Numeric field filters
|
||||
pytest.param(
|
||||
"rating>=4",
|
||||
["The Hobbit", "Programming Book"],
|
||||
marks=pytest.mark.xfail(reason="Rating filter not implemented yet"),
|
||||
),
|
||||
pytest.param(
|
||||
"rating=3",
|
||||
["Dune"],
|
||||
marks=pytest.mark.xfail(reason="Rating filter not implemented yet"),
|
||||
),
|
||||
("shelf>1", ["The Fellowship", "Programming Book"]),
|
||||
(
|
||||
"year>=1954",
|
||||
["The Hobbit", "The Fellowship", "Dune", "Programming Book"],
|
||||
),
|
||||
("rating>=4", "", ["The Hobbit", "Programming Book"]),
|
||||
("rating=3", "", ["Dune"]),
|
||||
("shelf>1", "", ["The Fellowship", "Programming Book"]),
|
||||
("year>=1954", "", ["The Fellowship", "Dune", "Programming Book"]),
|
||||
# Date field filters
|
||||
(
|
||||
"added>=2026-03-15",
|
||||
"",
|
||||
["The Hobbit", "The Fellowship", "Dune", "Programming Book"],
|
||||
),
|
||||
("bought<2026-01-01", ["Programming Book"]),
|
||||
("bought<2026-01-01", "", ["Programming Book"]),
|
||||
# Negation
|
||||
("-genre:Fantasy", ["Dune", "Programming Book"]),
|
||||
("-owner:bob", ["The Hobbit", "The Fellowship", "Dune"]),
|
||||
("-genre:Fantasy", "", ["Dune", "Programming Book"]),
|
||||
("-owner:bob", "", ["The Hobbit", "The Fellowship", "Dune"]),
|
||||
# Complex query with multiple filters
|
||||
("-genre:Fantasy owner:alice", ["Dune"]),
|
||||
("-genre:Fantasy owner:alice", "", ["Dune"]),
|
||||
# User-specific queries
|
||||
("rating>=4", "alice", ["The Hobbit"]),
|
||||
("is:reading", "alice", ["The Fellowship"]),
|
||||
("is:read", "alice", ["The Hobbit", "Dune"]),
|
||||
("is:wished", "alice", ["Programming Book"]),
|
||||
],
|
||||
)
|
||||
def test_book_search_advanced_queries(
|
||||
self, app: Flask, cli_runner: CliRunner, query: str, expected_titles: list[str]
|
||||
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
|
||||
@@ -390,7 +372,8 @@ class TestBookSearchCommand:
|
||||
|
||||
# Execute the search query
|
||||
result = cli_runner.invoke(
|
||||
cli, ["book", "search", "--format", "json", "--", query]
|
||||
cli,
|
||||
["book", "search", "--format", "json", "--username", username, "--", query],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"Search query '{query}' failed: {result.output}"
|
||||
@@ -517,6 +500,9 @@ class TestBookSearchCommand:
|
||||
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
|
||||
@@ -562,6 +548,12 @@ class TestBookSearchCommand:
|
||||
|
||||
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."""
|
||||
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
from hxbooks.search import (
|
||||
ComparisonOperator,
|
||||
Field,
|
||||
IsOperatorValue,
|
||||
QueryParser,
|
||||
SearchQuery,
|
||||
_convert_value, # noqa: PLC2701
|
||||
@@ -87,6 +88,14 @@ class TestFieldFilters:
|
||||
assert filter.field == Field.AUTHOR
|
||||
assert filter.value == "tolkien"
|
||||
|
||||
def test_parse_is_filter(self, parser: QueryParser) -> None:
|
||||
"""Test parsing 'is' operator field filter."""
|
||||
result = parser.parse("is:reading")
|
||||
assert len(result.field_filters) == 1
|
||||
filter = result.field_filters[0]
|
||||
assert filter.field == Field.IS
|
||||
assert filter.value == IsOperatorValue.READING
|
||||
|
||||
def test_parse_negated_filter(self, parser: QueryParser) -> None:
|
||||
"""Test parsing negated field filter."""
|
||||
result = parser.parse("-genre:romance")
|
||||
@@ -243,6 +252,21 @@ class TestTypeConversion:
|
||||
assert result == "123"
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_convert_is_operator(self, parser: QueryParser) -> None:
|
||||
"""Test converting values for 'is' operator fields."""
|
||||
result = _convert_value(Field.IS, "reading")
|
||||
assert result == IsOperatorValue.READING
|
||||
|
||||
result = _convert_value(Field.IS, "dropped")
|
||||
assert result == IsOperatorValue.DROPPED
|
||||
|
||||
result = _convert_value(Field.IS, "wished")
|
||||
assert result == IsOperatorValue.WISHED
|
||||
|
||||
# Invalid value should return UNKNOWN
|
||||
result = _convert_value(Field.IS, "invalid-status")
|
||||
assert result == IsOperatorValue.UNKNOWN
|
||||
|
||||
|
||||
class TestParsingEdgeCases:
|
||||
"""Test edge cases and error handling in query parsing."""
|
||||
|
||||
Reference in New Issue
Block a user