import json from django.contrib.auth.models import User from django.test import TestCase from .models import ApiToken from books.models import EBook, EBookProgress, EBookHighlights class ApiTokenAuthMiddlewareTests(TestCase): def setUp(self): self.user = User.objects.create_user(username='alice', password='pw12345678') self.token = ApiToken.objects.create(user=self.user) def test_valid_bearer_token_authenticates(self): resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.status_code, 200) def test_missing_token_is_unauthenticated(self): resp = self.client.get('/books/') self.assertEqual(resp.status_code, 401) def test_invalid_token_is_unauthenticated(self): resp = self.client.get('/books/', HTTP_AUTHORIZATION='Bearer not-a-real-token') self.assertEqual(resp.status_code, 401) def test_token_scoped_to_owning_user(self): other = User.objects.create_user(username='bob', password='pw12345678') EBook.objects.create(user=other, meta_ct='ct', meta_iv='iv', data_ct='ct', data_iv='iv') resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.json(), []) def test_regenerate_invalidates_old_token(self): old = self.token.token self.token.regenerate() resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {old}') self.assertEqual(resp.status_code, 401) resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.status_code, 200) def test_regenerate_view_requires_login_and_rotates_token(self): resp = self.client.post('/accounts/api-token/regenerate/') self.assertEqual(resp.status_code, 302) self.assertIn('/accounts/login/', resp.url) self.client.login(username='alice', password='pw12345678') old = self.token.token resp = self.client.post('/accounts/api-token/regenerate/') self.assertRedirects(resp, '/accounts/settings/') self.token.refresh_from_db() self.assertNotEqual(self.token.token, old) class SyncSnapshotTests(TestCase): def setUp(self): self.user = User.objects.create_user(username='alice', password='pw12345678') self.token = ApiToken.objects.create(user=self.user) self.book = EBook.objects.create( user=self.user, meta_ct='meta-ct', meta_iv='meta-iv', data_ct='data-ct', data_iv='data-iv', ) EBookProgress.objects.create( user=self.user, book=self.book, scroll_fraction=0.5, position_anchor='3:0.5', ) EBookHighlights.objects.create(user=self.user, book=self.book, ct='hl-ct', iv='hl-iv') def test_requires_auth(self): resp = self.client.get('/api/sync/') self.assertEqual(resp.status_code, 401) def test_snapshot_shape_and_data(self): resp = self.client.get('/api/sync/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.status_code, 200) data = resp.json() for key in ( 'server_time', 'books', 'book_progress', 'book_highlights', 'book_bookmarks', 'podcast_feeds', 'episode_progress', 'podcast_queue', 'saved_stations', ): self.assertIn(key, data) self.assertEqual(len(data['books']), 1) self.assertEqual(data['books'][0]['meta_ct'], 'meta-ct') self.assertEqual(len(data['book_progress']), 1) self.assertEqual(data['book_progress'][0]['book_id'], self.book.id) self.assertEqual(data['book_progress'][0]['position_anchor'], '3:0.5') self.assertEqual(len(data['book_highlights']), 1) self.assertEqual(data['book_highlights'][0]['ct'], 'hl-ct') # No data_ct/data_iv leaked into the snapshot (book bytes stay lazy-fetched) self.assertNotIn('data_ct', data['books'][0])