Archived
Reviewed and merged by Reid (Hermes Reviewer) Co-authored-by: crisleo-hermes <hermes@codescripters.org> Co-committed-by: crisleo-hermes <hermes@codescripters.org>
255 lines
9.2 KiB
Python
255 lines
9.2 KiB
Python
"""Tests for the Book search & discovery endpoints."""
|
|
|
|
import pytest
|
|
from django.urls import reverse
|
|
from rest_framework import status
|
|
from rest_framework.test import APIClient
|
|
|
|
from apps.books.models import Book, ReadingStatus
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def api_client():
|
|
return APIClient()
|
|
|
|
|
|
@pytest.fixture
|
|
def user(django_user_model):
|
|
return django_user_model.objects.create_user(
|
|
email="reader@example.com",
|
|
password="testpass123",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_client(api_client, user):
|
|
api_client.force_authenticate(user=user)
|
|
return api_client
|
|
|
|
|
|
@pytest.fixture
|
|
def books():
|
|
books_data = [
|
|
Book.objects.create(
|
|
title="Dune",
|
|
author="Frank Herbert",
|
|
genre="Science Fiction",
|
|
reading_status=ReadingStatus.FINISHED,
|
|
total_pages=688,
|
|
description="A desert planet saga.",
|
|
),
|
|
Book.objects.create(
|
|
title="Neuromancer",
|
|
author="William Gibson",
|
|
genre="Science Fiction",
|
|
reading_status=ReadingStatus.READING,
|
|
total_pages=271,
|
|
description="Cyberpunk classic.",
|
|
),
|
|
Book.objects.create(
|
|
title="The Hobbit",
|
|
author="J.R.R. Tolkien",
|
|
genre="Fantasy",
|
|
reading_status=ReadingStatus.WANT_TO_READ,
|
|
total_pages=310,
|
|
description="A hobbit's adventure.",
|
|
),
|
|
Book.objects.create(
|
|
title="1984",
|
|
author="George Orwell",
|
|
genre="Dystopian",
|
|
reading_status=ReadingStatus.FINISHED,
|
|
total_pages=328,
|
|
description="Big Brother is watching.",
|
|
),
|
|
]
|
|
return books_data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Search tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBookSearch:
|
|
"""Verify the search endpoint returns correct results."""
|
|
|
|
def test_search_by_title(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "Dune"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "Dune" in titles
|
|
assert "Neuromancer" not in titles
|
|
|
|
def test_search_by_author(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "Tolkien"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "The Hobbit" in titles
|
|
|
|
def test_search_by_genre(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "Fantasy"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "The Hobbit" in titles
|
|
|
|
def test_search_case_insensitive(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "dune"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert any(b["title"] == "Dune" for b in response.data["results"])
|
|
|
|
def test_search_partial_match(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "Neu"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "Neuromancer" in titles
|
|
|
|
def test_search_empty_query_returns_all(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": ""})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert len(response.data["results"]) == 4
|
|
|
|
def test_search_no_results(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "zzzznotfound"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert len(response.data["results"]) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Filter tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBookFilters:
|
|
"""Verify filters for genre, author, and reading_status."""
|
|
|
|
def test_filter_by_genre(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"genre": "Science Fiction"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "Dune" in titles
|
|
assert "Neuromancer" in titles
|
|
assert "The Hobbit" not in titles
|
|
|
|
def test_filter_by_author(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"author": "George Orwell"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "1984" in titles
|
|
assert "Dune" not in titles
|
|
|
|
def test_filter_by_reading_status(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"reading_status": ReadingStatus.FINISHED})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "Dune" in titles
|
|
assert "1984" in titles
|
|
assert "Neuromancer" not in titles
|
|
assert "The Hobbit" not in titles
|
|
|
|
def test_filter_combined_with_search(self, auth_client, books):
|
|
"""Search + filter should intersect results."""
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"q": "Dune", "reading_status": ReadingStatus.FINISHED})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert "Dune" in titles
|
|
# 1984 matches reading_status but not search
|
|
assert "1984" not in titles
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Discovery endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBookDiscovery:
|
|
"""Verify genre and author discovery endpoints."""
|
|
|
|
def test_genres_endpoint(self, auth_client, books):
|
|
url = reverse("book-genres")
|
|
response = auth_client.get(url)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert isinstance(response.data, list)
|
|
assert "Science Fiction" in response.data
|
|
assert "Fantasy" in response.data
|
|
assert "Dystopian" in response.data
|
|
# No duplicate genres
|
|
assert response.data.count("Science Fiction") == 1
|
|
|
|
def test_authors_endpoint(self, auth_client, books):
|
|
url = reverse("book-authors")
|
|
response = auth_client.get(url)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert isinstance(response.data, list)
|
|
assert "Frank Herbert" in response.data
|
|
assert "J.R.R. Tolkien" in response.data
|
|
|
|
def test_genres_requires_auth(self, api_client, books):
|
|
url = reverse("book-genres")
|
|
response = api_client.get(url)
|
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
|
|
|
def test_authors_requires_auth(self, api_client, books):
|
|
url = reverse("book-authors")
|
|
response = api_client.get(url)
|
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Detail view
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBookDetail:
|
|
"""Verify the book detail endpoint."""
|
|
|
|
def test_retrieve_book(self, auth_client, books):
|
|
book = books[0]
|
|
url = reverse("book-detail", kwargs={"pk": book.pk})
|
|
response = auth_client.get(url)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.data["title"] == "Dune"
|
|
assert response.data["author"] == "Frank Herbert"
|
|
assert response.data["description"] == "A desert planet saga."
|
|
assert response.data["total_pages"] == 688
|
|
|
|
def test_retrieve_nonexistent_returns_404(self, auth_client, books):
|
|
url = reverse("book-detail", kwargs={"pk": 99999})
|
|
response = auth_client.get(url)
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
def test_pagination(self, auth_client, books):
|
|
"""List with small page size should paginate."""
|
|
url = reverse("book-list")
|
|
response = auth_client.get(f"{url}?page_size=2")
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert "count" in response.data
|
|
assert "results" in response.data
|
|
assert response.data["count"] == 4
|
|
|
|
def test_ordering(self, auth_client, books):
|
|
url = reverse("book-list")
|
|
response = auth_client.get(url, {"ordering": "title"})
|
|
assert response.status_code == status.HTTP_200_OK
|
|
titles = [b["title"] for b in response.data["results"]]
|
|
assert titles == sorted(titles) |