Add option to share bookmarks publicly (#503)

* Make shared view public, add user profile fallback

* Allow unauthenticated access to shared bookmarks API

* Link shared bookmarks in unauthenticated layout

* Add public sharing setting

* Only show shared bookmarks link if there are publicly shared bookmarks

* Disable public sharing if sharing is disabled

* Show specific helper text when public sharing is enabled

* Fix tests

* Add more tests

* Improve setting description
This commit is contained in:
Sascha Ißbrücker
2023-08-15 00:20:52 +02:00
committed by GitHub
parent 22e8750c24
commit ea240eefd9
29 changed files with 667 additions and 87 deletions

View File

@@ -76,11 +76,12 @@ class BookmarkFactoryMixin:
tag.save()
return tag
def setup_user(self, name: str = None, enable_sharing: bool = False):
def setup_user(self, name: str = None, enable_sharing: bool = False, enable_public_sharing: bool = False):
if not name:
name = get_random_string(length=32)
user = User.objects.create_user(name, 'user@example.com', 'password123')
user.profile.enable_sharing = enable_sharing
user.profile.enable_public_sharing = enable_public_sharing
user.profile.save()
return user

View File

@@ -0,0 +1,26 @@
from django.test import TestCase
from django.urls import reverse
from bookmarks.tests.helpers import BookmarkFactoryMixin
class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin):
def assertSharedBookmarksLinkCount(self, response, count):
url = reverse('bookmarks:shared')
self.assertContains(response, f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
count=count)
def test_publicly_shared_bookmarks_link(self):
# should not render link if no public shares exist
user = self.setup_user(enable_sharing=True)
self.setup_bookmark(user=user, shared=True)
response = self.client.get(reverse('login'))
self.assertSharedBookmarksLinkCount(response, 0)
# should render link if public shares exist
user.profile.enable_public_sharing = True
user.profile.save()
response = self.client.get(reverse('login'))
self.assertSharedBookmarksLinkCount(response, 1)

View File

@@ -75,7 +75,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
'placeholder=" " autofocus class="form-input" required '
'id="id_url">',
html)
def test_should_prefill_title_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title')
html = response.content.decode()
@@ -85,7 +85,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
'class="form-input" maxlength="512" autocomplete="off" '
'id="id_title">',
html)
def test_should_prefill_description_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description')
html = response.content.decode()
@@ -160,8 +160,32 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
</label>
''', html, count=1)
def test_should_hide_notes_if_there_are_no_notes(self):
bookmark = self.setup_bookmark()
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
def test_should_show_respective_share_hint(self):
self.user.profile.enable_sharing = True
self.user.profile.save()
self.assertContains(response, '<details class="notes">', count=1)
response = self.client.get(reverse('bookmarks:new'))
html = response.content.decode()
self.assertInHTML('''
<div class="form-input-hint">
Share this bookmark with other registered users.
</div>
''', html)
self.user.profile.enable_public_sharing = True
self.user.profile.save()
response = self.client.get(reverse('bookmarks:new'))
html = response.content.decode()
self.assertInHTML('''
<div class="form-input-hint">
Share this bookmark with other registered users and anonymous users.
</div>
''', html)
def test_should_hide_notes_if_there_are_no_notes(self):
bookmark = self.setup_bookmark()
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
self.assertContains(response, '<details class="notes">', count=1)

View File

@@ -10,6 +10,8 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all()):
rf = RequestFactory()
request = rf.get(url)
request.user = self.get_or_create_test_user()
request.user_profile = self.get_or_create_test_user().profile
filters = BookmarkFilters(request)
context = RequestContext(request, {
'request': request,

View File

@@ -10,7 +10,7 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None:
def authenticate(self) -> None:
user = self.get_or_create_test_user()
self.client.force_login(user)
@@ -65,6 +65,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
''', html, count=0)
def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
user3 = self.setup_user(enable_sharing=True)
@@ -89,6 +90,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_shared_bookmarks_from_selected_user(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
user3 = self.setup_user(enable_sharing=True)
@@ -108,6 +110,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_bookmarks_matching_query(self):
self.authenticate()
user = self.setup_user(enable_sharing=True)
visible_bookmarks = [
self.setup_bookmark(shared=True, title='searchvalue', user=user),
@@ -126,7 +129,29 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_only_publicly_shared_bookmarks_without_login(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True)
visible_bookmarks = [
self.setup_bookmark(shared=True, user=user1),
self.setup_bookmark(shared=True, user=user1),
self.setup_bookmark(shared=True, user=user1),
]
invisible_bookmarks = [
self.setup_bookmark(shared=True, user=user2),
self.setup_bookmark(shared=True, user=user2),
self.setup_bookmark(shared=True, user=user2),
]
response = self.client.get(reverse('bookmarks:shared'))
self.assertContains(response, '<ul class="bookmark-list">') # Should render list
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
user3 = self.setup_user(enable_sharing=True)
@@ -158,6 +183,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertInvisibleTags(response, invisible_tags)
def test_should_list_tags_for_shared_bookmarks_from_selected_user(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
user3 = self.setup_user(enable_sharing=True)
@@ -180,6 +206,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertInvisibleTags(response, invisible_tags)
def test_should_list_tags_for_bookmarks_matching_query(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
user3 = self.setup_user(enable_sharing=True)
@@ -207,7 +234,32 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
def test_should_list_only_tags_for_publicly_shared_bookmarks_without_login(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True)
visible_tags = [
self.setup_tag(user=user1),
self.setup_tag(user=user1),
]
invisible_tags = [
self.setup_tag(user=user2),
self.setup_tag(user=user2),
]
self.setup_bookmark(shared=True, user=user1, tags=[visible_tags[0]])
self.setup_bookmark(shared=True, user=user1, tags=[visible_tags[1]])
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
response = self.client.get(reverse('bookmarks:shared'))
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
self.authenticate()
expected_visible_users = [
self.setup_user(enable_sharing=True),
self.setup_user(enable_sharing=True),
@@ -226,30 +278,53 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertVisibleUserOptions(response, expected_visible_users)
self.assertInvisibleUserOptions(response, expected_invisible_users)
def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
expected_visible_users = [
self.setup_user(enable_sharing=True, enable_public_sharing=True),
self.setup_user(enable_sharing=True, enable_public_sharing=True),
]
self.setup_bookmark(shared=True, user=expected_visible_users[0])
self.setup_bookmark(shared=True, user=expected_visible_users[1])
def test_should_open_bookmarks_in_new_page_by_default(self):
visible_bookmarks = [
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True)
]
expected_invisible_users = [
self.setup_user(enable_sharing=True),
self.setup_user(enable_sharing=True),
]
self.setup_bookmark(shared=True, user=expected_invisible_users[0])
self.setup_bookmark(shared=True, user=expected_invisible_users[1])
response = self.client.get(reverse('bookmarks:shared'))
response = self.client.get(reverse('bookmarks:shared'))
self.assertVisibleUserOptions(response, expected_visible_users)
self.assertInvisibleUserOptions(response, expected_invisible_users)
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
def test_should_open_bookmarks_in_new_page_by_default(self):
self.authenticate()
user = self.get_or_create_test_user()
user.profile.enable_sharing = True
user.profile.save()
visible_bookmarks = [
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True)
]
response = self.client.get(reverse('bookmarks:shared'))
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
user = self.get_or_create_test_user()
user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
user.profile.save()
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
visible_bookmarks = [
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True)
]
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
self.authenticate()
user = self.get_or_create_test_user()
user.profile.enable_sharing = True
user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
user.profile.save()
response = self.client.get(reverse('bookmarks:shared'))
visible_bookmarks = [
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True)
]
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
response = self.client.get(reverse('bookmarks:shared'))
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')

View File

@@ -16,8 +16,6 @@ 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], notes='Test notes')
@@ -26,6 +24,10 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.archived_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2])
self.archived_bookmark2 = self.setup_bookmark(is_archived=True)
def authenticate(self):
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)
def assertBookmarkListEqual(self, data_list, bookmarks):
expectations = []
for bookmark in bookmarks:
@@ -53,24 +55,34 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertCountEqual(data_list, expectations)
def test_list_bookmarks(self):
self.authenticate()
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):
self.authenticate()
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):
self.authenticate()
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):
self.authenticate()
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_list_shared_bookmarks(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
user3 = self.setup_user(enable_sharing=True)
@@ -89,7 +101,23 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)
def test_list_only_publicly_shared_bookmarks_when_not_logged_in(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True)
shared_bookmarks = [
self.setup_bookmark(shared=True, user=user1),
self.setup_bookmark(shared=True, user=user1)
]
self.setup_bookmark(shared=True, user=user2)
self.setup_bookmark(shared=True, user=user2)
response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)
def test_list_shared_bookmarks_should_filter_by_query_and_user(self):
self.authenticate()
# Search by query
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
@@ -131,6 +159,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)
def test_create_bookmark(self):
self.authenticate()
data = {
'url': 'https://example.com/',
'title': 'Test title',
@@ -155,6 +185,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
def test_create_bookmark_with_same_url_updates_existing_bookmark(self):
self.authenticate()
original_bookmark = self.setup_bookmark()
data = {
'url': original_bookmark.url,
@@ -182,6 +214,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
def test_create_bookmark_replaces_whitespace_in_tag_names(self):
self.authenticate()
data = {
'url': 'https://example.com/',
'title': 'Test title',
@@ -194,10 +228,14 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertListEqual(tag_names, ['tag-1', 'tag-2'])
def test_create_bookmark_minimal_payload(self):
self.authenticate()
data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
def test_create_archived_bookmark(self):
self.authenticate()
data = {
'url': 'https://example.com/',
'title': 'Test title',
@@ -216,41 +254,55 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
def test_create_bookmark_is_not_archived_by_default(self):
self.authenticate()
data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url'])
self.assertFalse(bookmark.is_archived)
def test_create_unread_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com/', 'unread': True}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url'])
self.assertTrue(bookmark.unread)
def test_create_bookmark_is_not_unread_by_default(self):
self.authenticate()
data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url'])
self.assertFalse(bookmark.unread)
def test_create_shared_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com/', 'shared': True}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url'])
self.assertTrue(bookmark.shared)
def test_create_bookmark_is_not_shared_by_default(self):
self.authenticate()
data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url'])
self.assertFalse(bookmark.shared)
def test_get_bookmark(self):
self.authenticate()
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):
self.authenticate()
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)
@@ -258,11 +310,15 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(updated_bookmark.url, data['url'])
def test_update_bookmark_fails_without_required_fields(self):
self.authenticate()
data = {'title': 'https://example.com/'}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
def test_update_bookmark_with_minimal_payload_clears_all_fields(self):
self.authenticate()
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)
@@ -274,6 +330,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(updated_bookmark.tag_names, [])
def test_update_bookmark_unread_flag(self):
self.authenticate()
data = {'url': 'https://example.com/', 'unread': True}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
@@ -281,6 +339,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(updated_bookmark.unread, True)
def test_update_bookmark_shared_flag(self):
self.authenticate()
data = {'url': 'https://example.com/', 'shared': True}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
@@ -288,6 +348,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(updated_bookmark.shared, True)
def test_patch_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com'}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
@@ -344,6 +406,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2'])
def test_patch_with_empty_payload_does_not_modify_bookmark(self):
self.authenticate()
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
@@ -353,23 +417,31 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertListEqual(updated_bookmark.tag_names, self.bookmark1.tag_names)
def test_delete_bookmark(self):
self.authenticate()
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):
self.authenticate()
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):
self.authenticate()
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_check_returns_no_bookmark_if_url_is_not_bookmarked(self):
self.authenticate()
url = reverse('bookmarks:bookmark-check')
check_url = urllib.parse.quote_plus('https://example.com')
response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
@@ -378,6 +450,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertIsNone(bookmark_data)
def test_check_returns_scraped_metadata_if_url_is_not_bookmarked(self):
self.authenticate()
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
expected_metadata = WebsiteMetadata(
'https://example.com',
@@ -397,6 +471,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertIsNotNone(expected_metadata.description, metadata['description'])
def test_check_returns_bookmark_if_url_is_bookmarked(self):
self.authenticate()
bookmark = self.setup_bookmark(url='https://example.com',
title='Example title',
description='Example description')
@@ -413,6 +489,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.description, bookmark_data['description'])
def test_check_returns_existing_metadata_if_url_is_bookmarked(self):
self.authenticate()
bookmark = self.setup_bookmark(url='https://example.com',
website_title='Existing title',
website_description='Existing description')
@@ -430,6 +508,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertIsNotNone(bookmark.website_description, metadata['description'])
def test_can_only_access_own_bookmarks(self):
self.authenticate()
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
inaccessible_bookmark = self.setup_bookmark(user=other_user)
inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True)

View File

@@ -0,0 +1,113 @@
import urllib.parse
from django.urls import reverse
from rest_framework import status
from rest_framework.authtoken.models import Token
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def authenticate(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)
def test_list_bookmarks_requires_authentication(self):
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
def test_list_archived_bookmarks_requires_authentication(self):
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK)
def test_list_shared_bookmarks_does_not_require_authentication(self):
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
self.authenticate()
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
def test_create_bookmark_requires_authentication(self):
data = {
'url': 'https://example.com/',
'title': 'Test title',
'description': 'Test description',
'notes': 'Test notes',
'is_archived': False,
'unread': False,
'shared': False,
'tag_names': ['tag1', 'tag2']
}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
def test_get_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.get(url, expected_status_code=status.HTTP_200_OK)
def test_update_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
data = {'url': 'https://example.com/'}
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.put(url, data, expected_status_code=status.HTTP_200_OK)
def test_patch_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
data = {'url': 'https://example.com'}
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
def test_delete_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
def test_archive_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse('bookmarks:bookmark-archive', args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
def test_unarchive_requires_authentication(self):
bookmark = self.setup_bookmark(is_archived=True)
url = reverse('bookmarks:bookmark-unarchive', args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
def test_check_requires_authentication(self):
url = reverse('bookmarks:bookmark-check')
check_url = urllib.parse.quote_plus('https://example.com')
self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)

View File

@@ -1,5 +1,7 @@
from dateutil.relativedelta import relativedelta
from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator
from django.http import HttpResponse
from django.template import Template, RequestContext
from django.test import TestCase, RequestFactory
from django.urls import reverse
@@ -7,17 +9,23 @@ from django.utils import timezone, formats
from bookmarks.models import Bookmark, UserProfile, User
from bookmarks.tests.helpers import BookmarkFactoryMixin
from bookmarks.middlewares import UserProfileMiddleware
class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank', unread: bool = False):
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
unread = bookmark.unread
favicon_img = f'<img src="/static/{bookmark.favicon_file}" alt="">' if bookmark.favicon_file else ''
self.assertInHTML(
f'''
<a href="{bookmark.url}"
target="{link_target}"
rel="noopener"
class="{'text-italic' if unread else ''}">{bookmark.resolved_title}</a>
class="{'text-italic' if unread else ''}">
{favicon_img}
{bookmark.resolved_title}
</a>
''',
html
)
@@ -130,22 +138,26 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
</button>
''', html, count=count)
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test',
user: User | AnonymousUser = None) -> str:
rf = RequestFactory()
request = rf.get(url)
request.user = self.get_or_create_test_user()
request.user = user or self.get_or_create_test_user()
middleware = UserProfileMiddleware(lambda r: HttpResponse())
middleware(request)
paginator = Paginator(bookmarks, 10)
page = paginator.page(1)
context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'})
return template.render(context)
def render_default_template(self, bookmarks: [Bookmark], url: str = '/test') -> str:
def render_default_template(self, bookmarks: [Bookmark], url: str = '/test',
user: User | AnonymousUser = None) -> str:
template = Template(
'{% load bookmarks %}'
'{% bookmark_list bookmarks return_url %}'
)
return self.render_template(bookmarks, template, url)
return self.render_template(bookmarks, template, url, user)
def render_template_with_link_target(self, bookmarks: [Bookmark], link_target: str) -> str:
template = Template(
@@ -211,11 +223,11 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
def test_bookmark_link_target_should_respect_unread_flag(self):
bookmark = self.setup_bookmark()
html = self.render_template_with_link_target([bookmark], '_self')
self.assertBookmarksLink(html, bookmark, link_target='_self', unread=False)
self.assertBookmarksLink(html, bookmark, link_target='_self')
bookmark = self.setup_bookmark(unread=True)
html = self.render_template_with_link_target([bookmark], '_self')
self.assertBookmarksLink(html, bookmark, link_target='_self', unread=True)
self.assertBookmarksLink(html, bookmark, link_target='_self')
def test_web_archive_link_target_should_be_blank_by_default(self):
bookmark = self.setup_bookmark()
@@ -402,3 +414,20 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
html = self.render_default_template([bookmark])
self.assertNotesToggle(html, 0)
def test_with_anonymous_user(self):
bookmark = self.setup_bookmark()
bookmark.date_added = timezone.now() - relativedelta(days=8)
bookmark.web_archive_snapshot_url = 'https://web.archive.org/web/20230531200136/https://example.com'
bookmark.notes = '**Example:** `print("Hello world!")`'
bookmark.favicon_file = 'https_example_com.png'
bookmark.save()
html = self.render_default_template([bookmark], '/test', AnonymousUser())
self.assertBookmarksLink(html, bookmark, link_target='_blank')
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
self.assertNoBookmarkActions(html, bookmark)
self.assertShareInfo(html, bookmark)
note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
self.assertNotes(html, note_html, 1)
self.assertFaviconVisible(html, bookmark)

View File

@@ -1,13 +1,17 @@
from django.core.paginator import Paginator
from django.template import Template, RequestContext
from django.test import SimpleTestCase, RequestFactory
from django.test import TestCase, RequestFactory
from bookmarks.tests.helpers import BookmarkFactoryMixin
class PaginationTagTest(SimpleTestCase):
class PaginationTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, num_items: int, page_size: int, current_page: int, url: str = '/test') -> str:
rf = RequestFactory()
request = rf.get(url)
request.user = self.get_or_create_test_user()
request.user_profile = self.get_or_create_test_user().profile
paginator = Paginator(range(0, num_items), page_size)
page = paginator.page(current_page)

View File

@@ -679,16 +679,26 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(user=user4, shared=True, tags=[tag]),
# Should return shared bookmarks from all users
query_set = queries.query_shared_bookmarks(None, self.profile, '')
query_set = queries.query_shared_bookmarks(None, self.profile, '', False)
self.assertQueryResult(query_set, [shared_bookmarks])
# Should respect search query
query_set = queries.query_shared_bookmarks(None, self.profile, 'test title')
query_set = queries.query_shared_bookmarks(None, self.profile, 'test title', False)
self.assertQueryResult(query_set, [[shared_bookmarks[0]]])
query_set = queries.query_shared_bookmarks(None, self.profile, '#' + tag.name)
query_set = queries.query_shared_bookmarks(None, self.profile, '#' + tag.name, False)
self.assertQueryResult(query_set, [[shared_bookmarks[2]]])
def test_query_publicly_shared_bookmarks(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True)
bookmark1 = self.setup_bookmark(user=user1, shared=True)
self.setup_bookmark(user=user2, shared=True)
query_set = queries.query_shared_bookmarks(None, self.profile, '', True)
self.assertQueryResult(query_set, [[bookmark1]])
def test_query_shared_bookmark_tags(self):
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)
@@ -710,10 +720,24 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(user=user3, shared=False, tags=[self.setup_tag(user=user3)]),
self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]),
query_set = queries.query_shared_bookmark_tags(None, self.profile, '')
query_set = queries.query_shared_bookmark_tags(None, self.profile, '', False)
self.assertQueryResult(query_set, [shared_tags])
def test_query_publicly_shared_bookmark_tags(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True)
tag1 = self.setup_tag(user=user1)
tag2 = self.setup_tag(user=user2)
self.setup_bookmark(user=user1, shared=True, tags=[tag1]),
self.setup_bookmark(user=user2, shared=True, tags=[tag2]),
query_set = queries.query_shared_bookmark_tags(None, self.profile, '', True)
self.assertQueryResult(query_set, [[tag1]])
def test_query_shared_bookmark_users(self):
users_with_shared_bookmarks = [
self.setup_user(enable_sharing=True),
@@ -735,9 +759,19 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),
# Should return users with shared bookmarks
query_set = queries.query_shared_bookmark_users(self.profile, '')
query_set = queries.query_shared_bookmark_users(self.profile, '', False)
self.assertQueryResult(query_set, [users_with_shared_bookmarks])
# Should respect search query
query_set = queries.query_shared_bookmark_users(self.profile, 'test title')
query_set = queries.query_shared_bookmark_users(self.profile, 'test title', False)
self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])
def test_query_publicly_shared_bookmark_users(self):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True)
self.setup_bookmark(user=user1, shared=True)
self.setup_bookmark(user=user2, shared=True)
query_set = queries.query_shared_bookmark_users(self.profile, '', True)
self.assertQueryResult(query_set, [[user1]])

View File

@@ -27,6 +27,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_BLANK,
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
'enable_sharing': False,
'enable_public_sharing': False,
'enable_favicons': False,
'tag_search': UserProfile.TAG_SEARCH_STRICT,
'display_url': False,
@@ -54,6 +55,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF,
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
'enable_sharing': True,
'enable_public_sharing': True,
'enable_favicons': True,
'tag_search': UserProfile.TAG_SEARCH_LAX,
'display_url': True,
@@ -70,6 +72,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(self.user.profile.bookmark_link_target, form_data['bookmark_link_target'])
self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration'])
self.assertEqual(self.user.profile.enable_sharing, form_data['enable_sharing'])
self.assertEqual(self.user.profile.enable_public_sharing, form_data['enable_public_sharing'])
self.assertEqual(self.user.profile.enable_favicons, form_data['enable_favicons'])
self.assertEqual(self.user.profile.tag_search, form_data['tag_search'])
self.assertEqual(self.user.profile.display_url, form_data['display_url'])

View File

@@ -1,20 +1,26 @@
from typing import List
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpResponse
from django.template import Template, RequestContext
from django.test import TestCase, RequestFactory
from bookmarks.middlewares import UserProfileMiddleware
from bookmarks.models import Tag, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def render_template(self, tags: List[Tag], selected_tags: List[Tag] = None, url: str = '/test'):
def render_template(self, tags: List[Tag], selected_tags: List[Tag] = None, url: str = '/test',
user: User | AnonymousUser = None):
if not selected_tags:
selected_tags = []
rf = RequestFactory()
request = rf.get(url)
request.user = self.get_or_create_test_user()
request.user = user or self.get_or_create_test_user()
middleware = UserProfileMiddleware(lambda r: HttpResponse())
middleware(request)
context = RequestContext(request, {
'request': request,
'tags': tags,
@@ -209,3 +215,37 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertTagGroups(rendered_template, [
['tag3', 'tag4', 'tag5']
])
def test_with_anonymous_user(self):
tags = [
self.setup_tag(name='tag1'),
self.setup_tag(name='tag2'),
self.setup_tag(name='tag3'),
self.setup_tag(name='tag4'),
self.setup_tag(name='tag5'),
]
selected_tags = [
tags[0],
tags[1],
]
rendered_template = self.render_template(tags, selected_tags, url='/test?q=%23tag1 %23tag2',
user=AnonymousUser())
self.assertTagGroups(rendered_template, [
['tag3', 'tag4', 'tag5']
])
self.assertNumSelectedTags(rendered_template, 2)
self.assertInHTML('''
<a href="?q=%23tag2"
class="text-bold mr-2">
<span>-tag1</span>
</a>
''', rendered_template)
self.assertInHTML('''
<a href="?q=%23tag1"
class="text-bold mr-2">
<span>-tag2</span>
</a>
''', rendered_template)

View File

@@ -10,6 +10,8 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, url: str, users: QuerySet[User] = User.objects.all()):
rf = RequestFactory()
request = rf.get(url)
request.user = self.get_or_create_test_user()
request.user_profile = self.get_or_create_test_user().profile
filters = BookmarkFilters(request)
context = RequestContext(request, {
'request': request,