feat: add per-book reading journal notes

Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
This commit is contained in:
2026-10-02 14:21:26 -05:00
parent c7a6cae714
commit e1096da1a7
2 changed files with 143 additions and 2 deletions
+68
View File
@@ -0,0 +1,68 @@
from tests.support import ApiTestCase, db_execute
class NoteTests(ApiTestCase):
def setUp(self):
super().setUp()
self.register()
self.book = self.create_book()
self.notes_path = f"/api/books/{self.book['id']}/notes"
def add(self, body: str, client=None):
return self.call("POST", self.notes_path, {"body": body}, client)
def test_add_and_list_newest_first(self):
first = self.add(" Started reading ")
self.assertEqual(first.status_code, 201)
self.assertEqual(first.get_json()["body"], "Started reading")
self.add("Chapter 3 was great")
bodies = [n["body"] for n in self.call("GET", self.notes_path).get_json()]
self.assertEqual(bodies, ["Chapter 3 was great", "Started reading"])
def test_edit_updates_body_and_timestamp(self):
note = self.add("Draft").get_json()
response = self.call("PATCH", f"/api/notes/{note['id']}", {"body": "Final"})
self.assertEqual(response.status_code, 200)
edited = response.get_json()
self.assertEqual(edited["body"], "Final")
self.assertGreater(edited["updated_at"], edited["created_at"])
def test_delete(self):
note = self.add("Temporary").get_json()
self.assertEqual(self.call("DELETE", f"/api/notes/{note['id']}").status_code, 204)
self.assertEqual(self.call("GET", self.notes_path).get_json(), [])
self.assertEqual(self.call("DELETE", f"/api/notes/{note['id']}").status_code, 404)
def test_validation(self):
for body, status in ((" ", 400), ("x" * 10_001, 400), ("a\x00b", 400)):
with self.subTest(length=len(body)):
self.assertEqual(self.add(body).status_code, status)
self.assertEqual(self.call("POST", self.notes_path, {"body": "ok", "book_id": 2}).status_code, 400)
def test_missing_book_and_out_of_range_ids_are_404(self):
self.assertEqual(self.call("GET", "/api/books/999/notes").status_code, 404)
self.assertEqual(self.call("PATCH", "/api/notes/99999999999", {"body": "x"}).status_code, 404)
def test_deleting_book_deletes_its_notes(self):
self.add("Will be gone")
self.call("DELETE", f"/api/books/{self.book['id']}")
self.assertEqual(db_execute("SELECT count(*) FROM notes")[0][0], 0)
class NoteIsolationTests(ApiTestCase):
def test_other_user_cannot_read_add_edit_or_delete_notes(self):
self.register("alice")
book = self.create_book()
note = self.call("POST", f"/api/books/{book['id']}/notes", {"body": "Private"}).get_json()
bob = self.other_user("bob")
attempts = (
("GET", f"/api/books/{book['id']}/notes", None),
("POST", f"/api/books/{book['id']}/notes", {"body": "Intrusion"}),
("PATCH", f"/api/notes/{note['id']}", {"body": "Hacked"}),
("DELETE", f"/api/notes/{note['id']}", None),
)
for method, path, payload in attempts:
with self.subTest(method=method, path=path):
self.assertEqual(self.call(method, path, payload, client=bob).status_code, 404)
bodies = [n["body"] for n in self.call("GET", f"/api/books/{book['id']}/notes").get_json()]
self.assertEqual(bodies, ["Private"])