from tests.support import ApiTestCase class BookTests(ApiTestCase): def setUp(self): super().setUp() self.register() def test_create_returns_full_shape(self): book = self.create_book() self.assertEqual(book["title"], "Dune") self.assertEqual(book["genre"], {"id": self.genre_id("Science Fiction"), "name": "Science Fiction"}) self.assertEqual((book["current_page"], book["total_pages"], book["status"]), (0, 412, "not_started")) self.assertIn("T", book["created_at"]) # ISO 8601 def test_create_trims_text(self): book = self.create_book(title=" Dune ") self.assertEqual(book["title"], "Dune") def test_progress_updates_status(self): book = self.create_book() reading = self.call("PATCH", f"/api/books/{book['id']}", {"current_page": 100}).get_json() self.assertEqual((reading["current_page"], reading["status"]), (100, "reading")) finished = self.call("PATCH", f"/api/books/{book['id']}", {"current_page": 412}).get_json() self.assertEqual(finished["status"], "finished") self.assertEqual(self.call("GET", f"/api/books/{book['id']}").get_json()["current_page"], 412) def test_edit_details(self): book = self.create_book() response = self.call( "PATCH", f"/api/books/{book['id']}", {"title": "Dune Messiah", "genre_id": self.genre_id("Fantasy"), "total_pages": 256}, ) self.assertEqual(response.status_code, 200) updated = response.get_json() self.assertEqual((updated["title"], updated["genre"]["name"], updated["total_pages"]), ("Dune Messiah", "Fantasy", 256)) self.assertGreater(updated["updated_at"], book["updated_at"]) def test_create_validation(self): valid = {"title": "Dune", "author": "Frank Herbert", "genre_id": self.genre_id("Fiction"), "total_pages": 412} cases = [ ({**valid, "title": " "}, "title"), ({k: v for k, v in valid.items() if k != "author"}, "author"), ({**valid, "title": "x" * 501}, "title"), ({**valid, "title": "a\x00b"}, "title"), ({**valid, "title": "\ud800"}, "title"), ({**valid, "genre_id": 999}, "genre_id"), ({**valid, "genre_id": "5"}, "genre_id"), ({**valid, "total_pages": True}, "total_pages"), ({**valid, "total_pages": 12.5}, "total_pages"), ({**valid, "total_pages": 0}, "total_pages"), ({**valid, "current_page": 413, "total_pages": 412}, "current_page"), ({**valid, "user_id": 2}, "user_id"), ] for payload, field in cases: with self.subTest(field=field, payload=payload): response = self.call("POST", "/api/books", payload) self.assertEqual(response.status_code, 400, response.get_json()) self.assertEqual(response.get_json()["field"], field) def test_lowering_total_below_current_page_is_rejected_with_fix(self): book = self.create_book(current_page=312, total_pages=400) response = self.call("PATCH", f"/api/books/{book['id']}", {"total_pages": 300}) self.assertEqual(response.status_code, 400) body = response.get_json() self.assertEqual(body["field"], "total_pages") self.assertIn("312", body["error"]) self.assertIn("300", body["error"]) def test_empty_patch_is_rejected(self): book = self.create_book() self.assertEqual(self.call("PATCH", f"/api/books/{book['id']}", {}).status_code, 400) def test_delete(self): book = self.create_book() self.assertEqual(self.call("DELETE", f"/api/books/{book['id']}").status_code, 204) self.assertEqual(self.call("GET", f"/api/books/{book['id']}").status_code, 404) self.assertEqual(self.call("DELETE", f"/api/books/{book['id']}").status_code, 404) def test_out_of_range_id_is_404(self): self.assertEqual(self.call("GET", "/api/books/99999999999").status_code, 404) def test_list_is_most_recently_updated_first(self): first = self.create_book(title="First") self.create_book(title="Second") self.call("PATCH", f"/api/books/{first['id']}", {"current_page": 1}) titles = [b["title"] for b in self.call("GET", "/api/books").get_json()] self.assertEqual(titles, ["First", "Second"]) def test_genres_sorted_and_require_login(self): genres = self.call("GET", "/api/genres").get_json() self.assertEqual(len(genres), 16) self.assertEqual([g["name"] for g in genres], sorted(g["name"] for g in genres)) self.call("POST", "/api/auth/logout") self.assertEqual(self.call("GET", "/api/genres").status_code, 401) def test_books_require_login(self): self.call("POST", "/api/auth/logout") self.assertEqual(self.call("GET", "/api/books").status_code, 401) class BookIsolationTests(ApiTestCase): def test_other_user_cannot_see_or_modify_book(self): self.register("alice") book = self.create_book() bob = self.other_user("bob") path = f"/api/books/{book['id']}" for method, payload in (("GET", None), ("PATCH", {"title": "Hacked"}), ("DELETE", None)): with self.subTest(method=method): response = self.call(method, path, payload, client=bob) self.assertEqual(response.status_code, 404) self.assertEqual(response.get_json(), {"error": "Book not found"}) self.assertEqual(self.call("GET", "/api/books", client=bob).get_json(), []) self.assertEqual(self.call("GET", path).get_json()["title"], "Dune")