Archived
feat: uv config other feats
- add uv configuration for the backend - update frontend to make auth work - add new auth endpoints - add bookmars feat - add reader feat
This commit is contained in:
@@ -6,14 +6,10 @@ from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.annotations.models import Bookmark, Note
|
||||
from apps.books.models import Book
|
||||
from apps.books.models import Book, EBook
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def api_client() -> APIClient:
|
||||
return APIClient()
|
||||
@@ -53,12 +49,26 @@ def book() -> Book:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bookmark(auth_client, user: User, book: Book) -> Bookmark:
|
||||
def ebook(user: User) -> EBook:
|
||||
return EBook.objects.create(
|
||||
user=user,
|
||||
title="Test Ebook",
|
||||
author="Test Author",
|
||||
format="epub",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bookmark(auth_client, user: User, ebook: EBook) -> Bookmark:
|
||||
return Bookmark.objects.create(
|
||||
user=user,
|
||||
book=book,
|
||||
page=42,
|
||||
ebook=ebook,
|
||||
epub_cfi="epubcfi(/6/4!/4/2,/1:0,/1:10)",
|
||||
chapter_index=2,
|
||||
chapter_title="Chapter 3",
|
||||
page=3,
|
||||
location_text="important passage",
|
||||
content="",
|
||||
)
|
||||
|
||||
|
||||
@@ -73,259 +83,84 @@ def note(auth_client, user: User, book: Book) -> Note:
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bookmark tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBookmarkList:
|
||||
url = reverse("bookmark-list")
|
||||
|
||||
def test_unauthenticated_user_cannot_list(self, api_client: APIClient):
|
||||
def test_list_requires_auth(self, api_client: APIClient):
|
||||
response = api_client.get(self.url)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_list_returns_user_bookmarks_only(
|
||||
self, auth_client: APIClient, user: User, other_user: User, book: Book
|
||||
self, auth_client: APIClient, user: User, other_user: User, ebook: EBook
|
||||
):
|
||||
Bookmark.objects.create(user=user, book=book, page=1)
|
||||
Bookmark.objects.create(user=other_user, book=book, page=2)
|
||||
|
||||
Bookmark.objects.create(
|
||||
user=user,
|
||||
ebook=ebook,
|
||||
epub_cfi="epubcfi(/6/4!/4/2,/1:0,/1:5)",
|
||||
chapter_index=0,
|
||||
page=1,
|
||||
)
|
||||
other_ebook = EBook.objects.create(user=other_user, title="Other", format="epub")
|
||||
Bookmark.objects.create(
|
||||
user=other_user,
|
||||
ebook=other_ebook,
|
||||
epub_cfi="epubcfi(/6/4!/4/2,/2:0,/2:5)",
|
||||
chapter_index=0,
|
||||
page=1,
|
||||
)
|
||||
response = auth_client.get(self.url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.data["results"]
|
||||
assert len(results) == 1
|
||||
assert results[0]["page"] == 1
|
||||
|
||||
def test_list_returns_empty_when_no_bookmarks(
|
||||
self, auth_client: APIClient
|
||||
):
|
||||
response = auth_client.get(self.url)
|
||||
def test_filter_by_ebook(self, auth_client: APIClient, bookmark: Bookmark, ebook: EBook):
|
||||
response = auth_client.get(self.url, {"ebook": str(ebook.id)})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 0
|
||||
|
||||
def test_list_orders_by_newest_first(
|
||||
self, auth_client: APIClient, user: User, book: Book
|
||||
):
|
||||
b1 = Bookmark.objects.create(user=user, book=book, page=1)
|
||||
b2 = Bookmark.objects.create(user=user, book=book, page=2)
|
||||
response = auth_client.get(self.url)
|
||||
results = response.data["results"]
|
||||
assert results[0]["page"] == 2
|
||||
assert results[1]["page"] == 1
|
||||
|
||||
def test_list_includes_book_title(
|
||||
self, auth_client: APIClient, bookmark: Bookmark
|
||||
):
|
||||
response = auth_client.get(self.url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["results"][0]["book_title"] == "Test Book"
|
||||
assert len(response.data["results"]) == 1
|
||||
|
||||
|
||||
class TestBookmarkCreate:
|
||||
url = reverse("bookmark-list")
|
||||
|
||||
def test_create_bookmark(self, auth_client: APIClient, book: Book):
|
||||
data = {"book": str(book.id), "page": 10, "location_text": "key insight"}
|
||||
def test_create_marker(self, auth_client: APIClient, ebook: EBook):
|
||||
data = {
|
||||
"ebook": ebook.id,
|
||||
"epub_cfi": "epubcfi(/6/4!/4/2,/1:0,/1:20)",
|
||||
"chapter_index": 1,
|
||||
"chapter_title": "Chapter 2",
|
||||
"location_text": "Selected text",
|
||||
"content": "My thought",
|
||||
}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["page"] == 10
|
||||
assert response.data["content"] == "My thought"
|
||||
assert response.data["ebook"] == ebook.id
|
||||
|
||||
def test_create_bookmark_without_location_text(
|
||||
self, auth_client: APIClient, book: Book
|
||||
):
|
||||
data = {"book": str(book.id), "page": 5}
|
||||
def test_create_bookmark_only_empty_content(self, auth_client: APIClient, ebook: EBook):
|
||||
data = {
|
||||
"ebook": ebook.id,
|
||||
"epub_cfi": "epubcfi(/6/4!/4/2,/3:0,/3:8)",
|
||||
"chapter_index": 0,
|
||||
"location_text": "Quote only",
|
||||
"content": "",
|
||||
}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["page"] == 5
|
||||
assert response.data["content"] == ""
|
||||
|
||||
def test_duplicate_bookmark_page_is_rejected(
|
||||
self, auth_client: APIClient, bookmark: Bookmark
|
||||
):
|
||||
data = {"book": str(bookmark.book.id), "page": bookmark.page}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_unauthenticated_user_cannot_create(
|
||||
self, api_client: APIClient, book: Book
|
||||
):
|
||||
data = {"book": str(book.id), "page": 10}
|
||||
response = api_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_invalid_page_rejected(
|
||||
self, auth_client: APIClient, book: Book
|
||||
):
|
||||
data = {"book": str(book.id), "page": 0}
|
||||
def test_duplicate_cfi_rejected(self, auth_client: APIClient, bookmark: Bookmark, ebook: EBook):
|
||||
data = {
|
||||
"ebook": ebook.id,
|
||||
"epub_cfi": bookmark.epub_cfi,
|
||||
"chapter_index": 0,
|
||||
"location_text": "dup",
|
||||
}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
class TestBookmarkDetail:
|
||||
def test_get_bookmark(
|
||||
self, auth_client: APIClient, bookmark: Bookmark
|
||||
):
|
||||
url = reverse("bookmark-detail", args=[str(bookmark.id)])
|
||||
response = auth_client.get(url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["page"] == bookmark.page
|
||||
|
||||
def test_cannot_access_other_users_bookmark(
|
||||
self, api_client: APIClient, other_user: User, bookmark: Bookmark
|
||||
):
|
||||
api_client.force_authenticate(user=other_user)
|
||||
url = reverse("bookmark-detail", args=[str(bookmark.id)])
|
||||
response = api_client.get(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
class TestBookmarkDelete:
|
||||
def test_delete_bookmark(
|
||||
self, auth_client: APIClient, bookmark: Bookmark
|
||||
):
|
||||
def test_delete_bookmark(self, auth_client: APIClient, bookmark: Bookmark):
|
||||
url = reverse("bookmark-detail", args=[str(bookmark.id)])
|
||||
response = auth_client.delete(url)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert Bookmark.objects.count() == 0
|
||||
|
||||
def test_cannot_delete_other_users_bookmark(
|
||||
self, api_client: APIClient, other_user: User, bookmark: Bookmark
|
||||
):
|
||||
api_client.force_authenticate(user=other_user)
|
||||
url = reverse("bookmark-detail", args=[str(bookmark.id)])
|
||||
response = api_client.delete(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
class TestBookmarkFilterByBook:
|
||||
def test_filter_by_book(
|
||||
self, auth_client: APIClient, user: User, book: Book
|
||||
):
|
||||
other_book = Book.objects.create(title="Other", author="Other")
|
||||
Bookmark.objects.create(user=user, book=book, page=1)
|
||||
Bookmark.objects.create(user=user, book=other_book, page=2)
|
||||
|
||||
url = reverse("bookmark-list")
|
||||
response = auth_client.get(url, {"book": str(book.id)})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert response.data["results"][0]["page"] == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Note tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestNoteList:
|
||||
url = reverse("note-list")
|
||||
|
||||
def test_unauthenticated_user_cannot_list(self, api_client: APIClient):
|
||||
response = api_client.get(self.url)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_list_returns_user_notes_only(
|
||||
self, auth_client: APIClient, user: User, other_user: User, book: Book
|
||||
):
|
||||
Note.objects.create(user=user, book=book, page=1, content="My note")
|
||||
Note.objects.create(user=other_user, book=book, page=2, content="Other's note")
|
||||
|
||||
response = auth_client.get(self.url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.data["results"]
|
||||
assert len(results) == 1
|
||||
assert results[0]["content"] == "My note"
|
||||
|
||||
def test_list_includes_book_title(
|
||||
self, auth_client: APIClient, note: Note
|
||||
):
|
||||
response = auth_client.get(self.url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["results"][0]["book_title"] == "Test Book"
|
||||
|
||||
|
||||
class TestNoteCreate:
|
||||
url = reverse("note-list")
|
||||
|
||||
def test_create_note(self, auth_client: APIClient, book: Book):
|
||||
data = {
|
||||
"book": str(book.id),
|
||||
"page": 20,
|
||||
"location_text": "interesting part",
|
||||
"content": "This is a thoughtful note.",
|
||||
}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["content"] == "This is a thoughtful note."
|
||||
|
||||
def test_create_note_without_location_text(
|
||||
self, auth_client: APIClient, book: Book
|
||||
):
|
||||
data = {"book": str(book.id), "page": 20, "content": "A note."}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
def test_empty_content_rejected(
|
||||
self, auth_client: APIClient, book: Book
|
||||
):
|
||||
data = {"book": str(book.id), "page": 20, "content": " "}
|
||||
response = auth_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_unauthenticated_user_cannot_create(
|
||||
self, api_client: APIClient, book: Book
|
||||
):
|
||||
data = {"book": str(book.id), "page": 20, "content": "Note"}
|
||||
response = api_client.post(self.url, data, format="json")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestNoteUpdate:
|
||||
def test_update_note_content(
|
||||
self, auth_client: APIClient, note: Note
|
||||
):
|
||||
url = reverse("note-detail", args=[str(note.id)])
|
||||
data = {"content": "Updated note content."}
|
||||
response = auth_client.patch(url, data, format="json")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["content"] == "Updated note content."
|
||||
|
||||
def test_cannot_update_other_users_note(
|
||||
self, api_client: APIClient, other_user: User, note: Note
|
||||
):
|
||||
api_client.force_authenticate(user=other_user)
|
||||
url = reverse("note-detail", args=[str(note.id)])
|
||||
data = {"content": "Hacked!"}
|
||||
response = api_client.patch(url, data, format="json")
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
class TestNoteDelete:
|
||||
def test_delete_note(self, auth_client: APIClient, note: Note):
|
||||
url = reverse("note-detail", args=[str(note.id)])
|
||||
response = auth_client.delete(url)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert Note.objects.count() == 0
|
||||
|
||||
def test_batch_delete_notes(
|
||||
self, auth_client: APIClient, user: User, book: Book
|
||||
):
|
||||
n1 = Note.objects.create(user=user, book=book, page=1, content="A")
|
||||
n2 = Note.objects.create(user=user, book=book, page=2, content="B")
|
||||
url = reverse("note-batch-delete")
|
||||
response = auth_client.delete(url, {"ids": [str(n1.id), str(n2.id)]}, format="json")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["deleted"] == 2
|
||||
|
||||
|
||||
class TestNoteFilterByBook:
|
||||
def test_filter_by_book(
|
||||
self, auth_client: APIClient, user: User, book: Book
|
||||
):
|
||||
other_book = Book.objects.create(title="Other", author="Other")
|
||||
Note.objects.create(user=user, book=book, page=1, content="In book")
|
||||
Note.objects.create(user=user, book=other_book, page=2, content="In other")
|
||||
|
||||
url = reverse("note-list")
|
||||
response = auth_client.get(url, {"book": str(book.id)})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert response.data["results"][0]["content"] == "In book"
|
||||
Reference in New Issue
Block a user