diff --git a/bookmarks/tests/helpers.py b/bookmarks/tests/helpers.py new file mode 100644 index 0000000..412a768 --- /dev/null +++ b/bookmarks/tests/helpers.py @@ -0,0 +1,64 @@ +from django.contrib.auth.models import User +from django.utils import timezone +from django.utils.crypto import get_random_string +from rest_framework import status +from rest_framework.test import APITestCase + +from bookmarks.models import Bookmark, Tag + + +class BookmarkFactoryMixin: + user = None + + def get_or_create_test_user(self): + if self.user is None: + self.user = User.objects.create_user('testuser', 'test@example.com', 'password123') + + return self.user + + def setup_bookmark(self, is_archived: bool = False, tags: [Tag] = [], user: User = None): + if user is None: + user = self.get_or_create_test_user() + unique_id = get_random_string(length=32) + bookmark = Bookmark( + url='https://example.com/' + unique_id, + date_added=timezone.now(), + date_modified=timezone.now(), + owner=user, + is_archived=is_archived + ) + bookmark.save() + for tag in tags: + bookmark.tags.add(tag) + bookmark.save() + return bookmark + + def setup_tag(self, user: User = None): + if user is None: + user = self.get_or_create_test_user() + name = get_random_string(length=32) + tag = Tag(name=name, date_added=timezone.now(), owner=user) + tag.save() + return tag + + +class LinkdingApiTestCase(APITestCase): + def get(self, url, expected_status_code=status.HTTP_200_OK): + response = self.client.get(url) + self.assertEqual(response.status_code, expected_status_code) + return response + + def post(self, url, data=None, expected_status_code=status.HTTP_200_OK): + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, expected_status_code) + return response + + def put(self, url, data=None, expected_status_code=status.HTTP_200_OK): + response = self.client.put(url, data, format='json') + self.assertEqual(response.status_code, expected_status_code) + return response + + def delete(self, url, expected_status_code=status.HTTP_200_OK): + response = self.client.delete(url) + self.assertEqual(response.status_code, expected_status_code) + return response diff --git a/bookmarks/tests/test_bookmarks_api.py b/bookmarks/tests/test_bookmarks_api.py new file mode 100644 index 0000000..83d839a --- /dev/null +++ b/bookmarks/tests/test_bookmarks_api.py @@ -0,0 +1,138 @@ +from collections import OrderedDict + +from django.contrib.auth.models import User +from django.urls import reverse +from rest_framework import status +from rest_framework.authtoken.models import Token + +from bookmarks.models import Bookmark +from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin + + +class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): + + def setUp(self) -> None: + self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0] + self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key) + self.tag1 = self.setup_tag() + self.tag2 = self.setup_tag() + self.bookmark1 = self.setup_bookmark(tags=[self.tag1, self.tag2]) + self.bookmark2 = self.setup_bookmark() + self.bookmark3 = self.setup_bookmark(tags=[self.tag2]) + self.archived_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2]) + self.archived_bookmark2 = self.setup_bookmark(is_archived=True) + + def assertBookmarkListEqual(self, data_list, bookmarks): + expectations = [] + for bookmark in bookmarks: + tag_names = [tag.name for tag in bookmark.tags.all()] + tag_names.sort(key=str.lower) + expectation = OrderedDict() + expectation['id'] = bookmark.id + expectation['url'] = bookmark.url + expectation['title'] = bookmark.title + expectation['description'] = bookmark.description + expectation['website_title'] = bookmark.website_title + expectation['website_description'] = bookmark.website_description + expectation['tag_names'] = tag_names + expectation['date_added'] = bookmark.date_added.isoformat().replace('+00:00', 'Z') + expectation['date_modified'] = bookmark.date_modified.isoformat().replace('+00:00', 'Z') + expectations.append(expectation) + + for data in data_list: + data['tag_names'].sort(key=str.lower) + + self.assertCountEqual(data_list, expectations) + + def test_create_bookmark(self): + data = { + 'url': 'https://example.com/', + 'title': 'Test title', + 'description': 'Test description', + 'tag_names': ['tag1', 'tag2'] + } + self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) + bookmark = Bookmark.objects.get(url=data['url']) + self.assertEqual(bookmark.url, data['url']) + self.assertEqual(bookmark.title, data['title']) + self.assertEqual(bookmark.description, data['description']) + self.assertEqual(bookmark.tags.count(), 2) + self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1) + self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1) + + def test_create_bookmark_minimal_payload(self): + data = {'url': 'https://example.com/'} + self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) + + def test_list_bookmarks(self): + response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], [self.bookmark1, self.bookmark2, self.bookmark3]) + + def test_list_bookmarks_should_filter_by_query(self): + response = self.get(reverse('bookmarks:bookmark-list') + '?q=#' + self.tag1.name, expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], [self.bookmark1]) + + def test_list_archived_bookmarks_does_not_return_unarchived_bookmarks(self): + response = self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1, self.archived_bookmark2]) + + def test_list_archived_bookmarks_should_filter_by_query(self): + response = self.get(reverse('bookmarks:bookmark-archived') + '?q=#' + self.tag1.name, expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1]) + + def test_get_bookmark(self): + url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + response = self.get(url, expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual([response.data], [self.bookmark1]) + + def test_update_bookmark(self): + data = {'url': 'https://example.com/'} + url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + self.put(url, data, expected_status_code=status.HTTP_200_OK) + updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) + self.assertEqual(updated_bookmark.url, data['url']) + + def test_delete_bookmark(self): + url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT) + self.assertEqual(len(Bookmark.objects.filter(id=self.bookmark1.id)), 0) + + def test_archive(self): + url = reverse('bookmarks:bookmark-archive', args=[self.bookmark1.id]) + self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) + bookmark = Bookmark.objects.get(id=self.bookmark1.id) + self.assertTrue(bookmark.is_archived) + + def test_unarchive(self): + url = reverse('bookmarks:bookmark-unarchive', args=[self.archived_bookmark1.id]) + self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) + bookmark = Bookmark.objects.get(id=self.archived_bookmark1.id) + self.assertFalse(bookmark.is_archived) + + def test_can_only_access_own_bookmarks(self): + other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') + inaccessible_bookmark = self.setup_bookmark(user=other_user) + self.setup_bookmark(user=other_user, is_archived=True) + + url = reverse('bookmarks:bookmark-list') + response = self.get(url, expected_status_code=status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), 3) + + url = reverse('bookmarks:bookmark-archived') + response = self.get(url, expected_status_code=status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), 2) + + url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id]) + self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND) + + url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id]) + self.put(url, {url: 'https://example.com/'}, expected_status_code=status.HTTP_404_NOT_FOUND) + + url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id]) + self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND) + + url = reverse('bookmarks:bookmark-archive', args=[inaccessible_bookmark.id]) + self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND) + + url = reverse('bookmarks:bookmark-unarchive', args=[inaccessible_bookmark.id]) + self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND) diff --git a/bookmarks/tests/test_queries.py b/bookmarks/tests/test_queries.py index 01fd6e5..d5d203d 100644 --- a/bookmarks/tests/test_queries.py +++ b/bookmarks/tests/test_queries.py @@ -1,38 +1,13 @@ from django.contrib.auth import get_user_model from django.test import TestCase -from django.utils import timezone -from django.utils.crypto import get_random_string -from bookmarks.models import Bookmark, Tag + from bookmarks import queries +from bookmarks.tests.helpers import BookmarkFactoryMixin User = get_user_model() -class QueriesTestCase(TestCase): - - def setUp(self) -> None: - self.user = User.objects.create_user('testuser', 'test@example.com', 'password123') - - def setup_bookmark(self, is_archived: bool = False, tags: [Tag] = []): - unique_id = get_random_string(length=32) - bookmark = Bookmark( - url='https://example.com/' + unique_id, - date_added=timezone.now(), - date_modified=timezone.now(), - owner=self.user, - is_archived=is_archived - ) - bookmark.save() - for tag in tags: - bookmark.tags.add(tag) - bookmark.save() - return bookmark - - def setup_tag(self): - name = get_random_string(length=32) - tag = Tag(name=name, date_added=timezone.now(), owner=self.user) - tag.save() - return tag +class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmarks_should_not_return_archived_bookmarks(self): bookmark1 = self.setup_bookmark() @@ -41,7 +16,7 @@ class QueriesTestCase(TestCase): self.setup_bookmark(is_archived=True) self.setup_bookmark(is_archived=True) - query = queries.query_bookmarks(self.user, '') + query = queries.query_bookmarks(self.get_or_create_test_user(), '') self.assertCountEqual([bookmark1, bookmark2], list(query)) @@ -52,7 +27,7 @@ class QueriesTestCase(TestCase): self.setup_bookmark() self.setup_bookmark() - query = queries.query_archived_bookmarks(self.user, '') + query = queries.query_archived_bookmarks(self.get_or_create_test_user(), '') self.assertCountEqual([bookmark1, bookmark2], list(query)) @@ -63,7 +38,7 @@ class QueriesTestCase(TestCase): self.setup_bookmark() self.setup_bookmark(is_archived=True, tags=[tag2]) - query = queries.query_bookmark_tags(self.user, '') + query = queries.query_bookmark_tags(self.get_or_create_test_user(), '') self.assertCountEqual([tag1], list(query)) @@ -73,7 +48,7 @@ class QueriesTestCase(TestCase): self.setup_bookmark(tags=[tag]) self.setup_bookmark(tags=[tag]) - query = queries.query_bookmark_tags(self.user, '') + query = queries.query_bookmark_tags(self.get_or_create_test_user(), '') self.assertCountEqual([tag], list(query)) @@ -84,7 +59,7 @@ class QueriesTestCase(TestCase): self.setup_bookmark() self.setup_bookmark(is_archived=True, tags=[tag2]) - query = queries.query_archived_bookmark_tags(self.user, '') + query = queries.query_archived_bookmark_tags(self.get_or_create_test_user(), '') self.assertCountEqual([tag2], list(query)) @@ -94,6 +69,6 @@ class QueriesTestCase(TestCase): self.setup_bookmark(is_archived=True, tags=[tag]) self.setup_bookmark(is_archived=True, tags=[tag]) - query = queries.query_archived_bookmark_tags(self.user, '') + query = queries.query_archived_bookmark_tags(self.get_or_create_test_user(), '') self.assertCountEqual([tag], list(query)) diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py index d310098..1084f42 100644 --- a/siteroot/settings/base.py +++ b/siteroot/settings/base.py @@ -41,7 +41,7 @@ INSTALLED_APPS = [ 'widget_tweaks', 'django_generate_secret_key', 'rest_framework', - 'rest_framework.authtoken' + 'rest_framework.authtoken', ] MIDDLEWARE = [