feat: auth by default, theme toggle, CoderPad startup, review fixes
- Require a session on every route; public routes opt out with @allow_anonymous - Split password hashing and pepper loading into passwords.py - Add a system/light/dark theme toggle backed by light-dark() colors - Ignore stale 401s from an earlier session, PATCH only changed book fields, and block overlapping journal-entry saves - Add bin/start and CoderPad Vite server settings for the pad's start/restart - Rewrite README as a mise onboarding guide; expand .gitignore - Include review-round fixes and tests
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
from tests.support import ApiTestCase
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import psycopg2
|
||||
|
||||
from tests.support import ApiTestCase, db_execute
|
||||
|
||||
|
||||
class BookTests(ApiTestCase):
|
||||
@@ -147,3 +153,50 @@ class BookIsolationTests(ApiTestCase):
|
||||
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")
|
||||
|
||||
|
||||
class ConcurrentUpdateTests(ApiTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.register()
|
||||
|
||||
def wait_for_lock_wait(self):
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
waiting = db_execute(
|
||||
"SELECT count(*) FROM pg_stat_activity"
|
||||
" WHERE datname = current_database() AND wait_event_type = 'Lock'"
|
||||
)[0][0]
|
||||
if waiting:
|
||||
return
|
||||
time.sleep(0.02)
|
||||
self.fail("PATCH never waited on the locked book row")
|
||||
|
||||
def test_concurrent_updates_to_different_fields_both_persist(self):
|
||||
book = self.create_book()
|
||||
other_session = psycopg2.connect(os.environ["DATABASE_URL"])
|
||||
result = {}
|
||||
try:
|
||||
with other_session.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE books SET title = 'Dune Messiah' WHERE id = %s",
|
||||
(book["id"],),
|
||||
)
|
||||
patch = threading.Thread(
|
||||
target=lambda: result.update(
|
||||
response=self.call(
|
||||
"PATCH", f"/api/books/{book['id']}", {"current_page": 100}
|
||||
)
|
||||
)
|
||||
)
|
||||
patch.start()
|
||||
self.wait_for_lock_wait()
|
||||
other_session.commit()
|
||||
patch.join(timeout=10)
|
||||
finally:
|
||||
other_session.close()
|
||||
|
||||
self.assertFalse(patch.is_alive(), "PATCH did not finish")
|
||||
self.assertEqual(result["response"].status_code, 200)
|
||||
saved = self.call("GET", f"/api/books/{book['id']}").get_json()
|
||||
self.assertEqual((saved["title"], saved["current_page"]), ("Dune Messiah", 100))
|
||||
|
||||
Reference in New Issue
Block a user