mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-13 13:39:27 +02:00
Add option for showing bookmark favicons (#390)
* Implement favicon loader * Implement load favicon task * Show favicons in bookmark list * Add missing migration * Load missing favicons on import * Automatically refresh favicons * Add enable favicon setting * Update uwsgi config to host favicons * Improve settings wording * Fix favicon loader test setup * Document LD_FAVICON_PROVIDER setting * Add refresh favicons button
This commit is contained in:
@@ -33,6 +33,7 @@ class BookmarkFactoryMixin:
|
||||
website_title: str = '',
|
||||
website_description: str = '',
|
||||
web_archive_snapshot_url: str = '',
|
||||
favicon_file: str = '',
|
||||
):
|
||||
if not title:
|
||||
title = get_random_string(length=32)
|
||||
@@ -56,6 +57,7 @@ class BookmarkFactoryMixin:
|
||||
unread=unread,
|
||||
shared=shared,
|
||||
web_archive_snapshot_url=web_archive_snapshot_url,
|
||||
favicon_file=favicon_file,
|
||||
)
|
||||
bookmark.save()
|
||||
for tag in tags:
|
||||
|
@@ -79,6 +79,17 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
</span>
|
||||
''', html, count=count)
|
||||
|
||||
def assertFaviconVisible(self, html: str, bookmark: Bookmark):
|
||||
self.assertFaviconCount(html, bookmark, 1)
|
||||
|
||||
def assertFaviconHidden(self, html: str, bookmark: Bookmark):
|
||||
self.assertFaviconCount(html, bookmark, 0)
|
||||
|
||||
def assertFaviconCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(f'''
|
||||
<img src="/static/{bookmark.favicon_file}" alt="">
|
||||
''', html, count=count)
|
||||
|
||||
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
@@ -211,3 +222,33 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
<a class="text-gray" href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||
</span>
|
||||
''', html)
|
||||
|
||||
def test_favicon_should_be_visible_when_favicons_enabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertFaviconVisible(html, bookmark)
|
||||
|
||||
def test_favicon_should_be_hidden_when_there_is_no_icon(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file='')
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertFaviconHidden(html, bookmark)
|
||||
|
||||
def test_favicon_should_be_hidden_when_favicons_disabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = False
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertFaviconHidden(html, bookmark)
|
||||
|
@@ -67,6 +67,13 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(self.user, bookmark, False)
|
||||
|
||||
def test_create_should_load_favicon(self):
|
||||
with patch.object(tasks, 'load_favicon') as mock_load_favicon:
|
||||
bookmark_data = Bookmark(url='https://example.com')
|
||||
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user)
|
||||
|
||||
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
||||
|
||||
def test_update_should_create_web_archive_snapshot_if_url_did_change(self):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
bookmark = self.setup_bookmark()
|
||||
@@ -109,6 +116,14 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
mock_load_website_metadata.assert_not_called()
|
||||
|
||||
def test_update_should_update_favicon(self):
|
||||
with patch.object(tasks, 'load_favicon') as mock_load_favicon:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.title = 'updated title'
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
|
||||
mock_load_favicon.assert_called_once_with(self.user, bookmark)
|
||||
|
||||
def test_archive_bookmark(self):
|
||||
bookmark = Bookmark(
|
||||
url='https://example.com',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import patch
|
||||
from unittest import mock
|
||||
|
||||
import waybackpy
|
||||
from background_task.models import Task
|
||||
@@ -8,6 +8,7 @@ from django.contrib.auth.models import User
|
||||
from django.test import TestCase, override_settings
|
||||
from waybackpy.exceptions import WaybackError
|
||||
|
||||
import bookmarks.services.favicon_loader
|
||||
import bookmarks.services.wayback
|
||||
from bookmarks.models import UserProfile
|
||||
from bookmarks.services import tasks
|
||||
@@ -53,12 +54,14 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def setUp(self):
|
||||
user = self.get_or_create_test_user()
|
||||
user.profile.web_archive_integration = UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
|
||||
user.profile.enable_favicons = True
|
||||
user.profile.save()
|
||||
|
||||
@disable_logging
|
||||
def run_pending_task(self, task_function):
|
||||
func = getattr(task_function, 'task_function', None)
|
||||
task = Task.objects.all()[0]
|
||||
self.assertEqual(task_function.name, task.task_name)
|
||||
args, kwargs = task.params()
|
||||
func(*args, **kwargs)
|
||||
task.delete()
|
||||
@@ -69,6 +72,7 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tasks = Task.objects.all()
|
||||
|
||||
for task in tasks:
|
||||
self.assertEqual(task_function.name, task.task_name)
|
||||
args, kwargs = task.params()
|
||||
func(*args, **kwargs)
|
||||
task.delete()
|
||||
@@ -76,7 +80,7 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_web_archive_snapshot_should_update_snapshot_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=MockWaybackMachineSaveAPI()):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=MockWaybackMachineSaveAPI()):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
bookmark.refresh_from_db()
|
||||
@@ -84,8 +88,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(bookmark.web_archive_snapshot_url, 'https://example.com/created_snapshot')
|
||||
|
||||
def test_create_web_archive_snapshot_should_handle_missing_bookmark_id(self):
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI()) as mock_save_api:
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI()) as mock_save_api:
|
||||
tasks._create_web_archive_snapshot_task(123, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -94,8 +98,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_web_archive_snapshot_should_skip_if_snapshot_exists(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com')
|
||||
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI()) as mock_save_api:
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI()) as mock_save_api:
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -104,8 +108,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_web_archive_snapshot_should_force_update_snapshot(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com')
|
||||
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI('https://other.com')):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI('https://other.com')):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, True)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
bookmark.refresh_from_db()
|
||||
@@ -115,10 +119,10 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_web_archive_snapshot_should_use_newest_snapshot_as_fallback(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -128,10 +132,10 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_web_archive_snapshot_should_ignore_missing_newest_snapshot(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(has_no_snapshot=True)):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(has_no_snapshot=True)):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -141,10 +145,10 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_web_archive_snapshot_should_ignore_newest_snapshot_errors(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(fail_loading_snapshot=True)):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(fail_loading_snapshot=True)):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -154,8 +158,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_load_web_archive_snapshot_should_update_snapshot_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
@@ -163,8 +167,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_load_web_archive_snapshot_should_handle_missing_bookmark_id(self):
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()) as mock_cdx_api:
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()) as mock_cdx_api:
|
||||
tasks._load_web_archive_snapshot_task(123)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
@@ -173,8 +177,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_load_web_archive_snapshot_should_skip_if_snapshot_exists(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com')
|
||||
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()) as mock_cdx_api:
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()) as mock_cdx_api:
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
@@ -183,8 +187,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_load_web_archive_snapshot_should_handle_missing_snapshot(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(has_no_snapshot=True)):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(has_no_snapshot=True)):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
@@ -193,8 +197,8 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_load_web_archive_snapshot_should_handle_wayback_errors(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(fail_loading_snapshot=True)):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(fail_loading_snapshot=True)):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
@@ -262,3 +266,157 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tasks.schedule_bookmarks_without_snapshots(self.user)
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
def test_load_favicon_should_create_favicon_file(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon:
|
||||
mock_load_favicon.return_value = 'https_example_com.png'
|
||||
|
||||
tasks.load_favicon(self.get_or_create_test_user(), bookmark)
|
||||
self.run_pending_task(tasks._load_favicon_task)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual(bookmark.favicon_file, 'https_example_com.png')
|
||||
|
||||
def test_load_favicon_should_update_favicon_file(self):
|
||||
bookmark = self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
|
||||
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon:
|
||||
mock_load_favicon.return_value = 'https_example_updated_com.png'
|
||||
tasks.load_favicon(self.get_or_create_test_user(), bookmark)
|
||||
self.run_pending_task(tasks._load_favicon_task)
|
||||
|
||||
mock_load_favicon.assert_called()
|
||||
bookmark.refresh_from_db()
|
||||
self.assertEqual(bookmark.favicon_file, 'https_example_updated_com.png')
|
||||
|
||||
def test_load_favicon_should_handle_missing_bookmark(self):
|
||||
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon:
|
||||
tasks._load_favicon_task(123)
|
||||
self.run_pending_task(tasks._load_favicon_task)
|
||||
|
||||
mock_load_favicon.assert_not_called()
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
|
||||
def test_load_favicon_should_not_run_when_background_tasks_are_disabled(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
tasks.load_favicon(self.get_or_create_test_user(), bookmark)
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
def test_load_favicon_should_not_run_when_favicon_feature_is_disabled(self):
|
||||
self.user.profile.enable_favicons = False
|
||||
self.user.profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
tasks.load_favicon(self.get_or_create_test_user(), bookmark)
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
def test_schedule_bookmarks_without_favicons_should_load_favicon_for_all_bookmarks_without_favicon(self):
|
||||
user = self.get_or_create_test_user()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
|
||||
tasks.schedule_bookmarks_without_favicons(user)
|
||||
self.run_pending_task(tasks._schedule_bookmarks_without_favicons_task)
|
||||
|
||||
task_list = Task.objects.all()
|
||||
self.assertEqual(task_list.count(), 3)
|
||||
|
||||
for task in task_list:
|
||||
self.assertEqual(task.task_name, 'bookmarks.services.tasks._load_favicon_task')
|
||||
|
||||
def test_schedule_bookmarks_without_favicons_should_only_update_user_owned_bookmarks(self):
|
||||
user = self.get_or_create_test_user()
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
|
||||
tasks.schedule_bookmarks_without_favicons(user)
|
||||
self.run_pending_task(tasks._schedule_bookmarks_without_favicons_task)
|
||||
|
||||
task_list = Task.objects.all()
|
||||
self.assertEqual(task_list.count(), 3)
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
|
||||
def test_schedule_bookmarks_without_favicons_should_not_run_when_background_tasks_are_disabled(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
tasks.schedule_bookmarks_without_favicons(self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
def test_schedule_bookmarks_without_favicons_should_not_run_when_favicon_feature_is_disabled(self):
|
||||
self.user.profile.enable_favicons = False
|
||||
self.user.profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
tasks.schedule_bookmarks_without_favicons(self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
def test_schedule_refresh_favicons_should_update_favicon_for_all_bookmarks(self):
|
||||
user = self.get_or_create_test_user()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
self.setup_bookmark(favicon_file='https_example_com.png')
|
||||
|
||||
tasks.schedule_refresh_favicons(user)
|
||||
self.run_pending_task(tasks._schedule_refresh_favicons_task)
|
||||
|
||||
task_list = Task.objects.all()
|
||||
self.assertEqual(task_list.count(), 6)
|
||||
|
||||
for task in task_list:
|
||||
self.assertEqual(task.task_name, 'bookmarks.services.tasks._load_favicon_task')
|
||||
|
||||
def test_schedule_refresh_favicons_should_only_update_user_owned_bookmarks(self):
|
||||
user = self.get_or_create_test_user()
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
|
||||
tasks.schedule_refresh_favicons(user)
|
||||
self.run_pending_task(tasks._schedule_refresh_favicons_task)
|
||||
|
||||
task_list = Task.objects.all()
|
||||
self.assertEqual(task_list.count(), 3)
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
|
||||
def test_schedule_refresh_favicons_should_not_run_when_background_tasks_are_disabled(self):
|
||||
self.setup_bookmark()
|
||||
tasks.schedule_refresh_favicons(self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
@override_settings(LD_ENABLE_REFRESH_FAVICONS=False)
|
||||
def test_schedule_refresh_favicons_should_not_run_when_refresh_is_disabled(self):
|
||||
self.setup_bookmark()
|
||||
tasks.schedule_refresh_favicons(self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
||||
def test_schedule_refresh_favicons_should_not_run_when_favicon_feature_is_disabled(self):
|
||||
self.user.profile.enable_favicons = False
|
||||
self.user.profile.save()
|
||||
|
||||
self.setup_bookmark()
|
||||
tasks.schedule_refresh_favicons(self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(Task.objects.count(), 0)
|
||||
|
127
bookmarks/tests/test_favicon_loader.py
Normal file
127
bookmarks/tests/test_favicon_loader.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import io
|
||||
import os.path
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks.services import favicon_loader
|
||||
|
||||
mock_icon_data = b'mock_icon'
|
||||
|
||||
|
||||
class FaviconLoaderTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.ensure_favicon_folder()
|
||||
self.clear_favicon_folder()
|
||||
|
||||
def create_mock_response(self, icon_data=mock_icon_data):
|
||||
mock_response = mock.Mock()
|
||||
mock_response.raw = io.BytesIO(icon_data)
|
||||
return mock_response
|
||||
|
||||
def ensure_favicon_folder(self):
|
||||
Path(settings.LD_FAVICON_FOLDER).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def clear_favicon_folder(self):
|
||||
folder = Path(settings.LD_FAVICON_FOLDER)
|
||||
for file in folder.iterdir():
|
||||
file.unlink()
|
||||
|
||||
def get_icon_path(self, filename):
|
||||
return Path(os.path.join(settings.LD_FAVICON_FOLDER, filename))
|
||||
|
||||
def icon_exists(self, filename):
|
||||
return self.get_icon_path(filename).exists()
|
||||
|
||||
def get_icon_data(self, filename):
|
||||
return self.get_icon_path(filename).read_bytes()
|
||||
|
||||
def count_icons(self):
|
||||
files = os.listdir(settings.LD_FAVICON_FOLDER)
|
||||
return len(files)
|
||||
|
||||
def test_load_favicon(self):
|
||||
with mock.patch('requests.get') as mock_get:
|
||||
mock_get.return_value = self.create_mock_response()
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
|
||||
# should create icon file
|
||||
self.assertTrue(self.icon_exists('https_example_com.png'))
|
||||
|
||||
# should store image data
|
||||
self.assertEqual(mock_icon_data, self.get_icon_data('https_example_com.png'))
|
||||
|
||||
def test_load_favicon_creates_folder_if_not_exists(self):
|
||||
with mock.patch('requests.get') as mock_get:
|
||||
mock_get.return_value = self.create_mock_response()
|
||||
|
||||
folder = Path(settings.LD_FAVICON_FOLDER)
|
||||
folder.rmdir()
|
||||
|
||||
self.assertFalse(folder.exists())
|
||||
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
|
||||
self.assertTrue(folder.exists())
|
||||
|
||||
def test_load_favicon_creates_single_icon_for_same_base_url(self):
|
||||
with mock.patch('requests.get') as mock_get:
|
||||
mock_get.return_value = self.create_mock_response()
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
favicon_loader.load_favicon('https://example.com?foo=bar')
|
||||
favicon_loader.load_favicon('https://example.com/foo')
|
||||
|
||||
self.assertEqual(1, self.count_icons())
|
||||
self.assertTrue(self.icon_exists('https_example_com.png'))
|
||||
|
||||
def test_load_favicon_creates_multiple_icons_for_different_base_url(self):
|
||||
with mock.patch('requests.get') as mock_get:
|
||||
mock_get.return_value = self.create_mock_response()
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
favicon_loader.load_favicon('https://sub.example.com')
|
||||
favicon_loader.load_favicon('https://other-domain.com')
|
||||
|
||||
self.assertEqual(3, self.count_icons())
|
||||
self.assertTrue(self.icon_exists('https_example_com.png'))
|
||||
self.assertTrue(self.icon_exists('https_sub_example_com.png'))
|
||||
self.assertTrue(self.icon_exists('https_other_domain_com.png'))
|
||||
|
||||
def test_load_favicon_caches_icons(self):
|
||||
with mock.patch('requests.get') as mock_get:
|
||||
mock_get.return_value = self.create_mock_response()
|
||||
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
mock_get.assert_called()
|
||||
|
||||
mock_get.reset_mock()
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
mock_get.assert_not_called()
|
||||
|
||||
def test_load_favicon_updates_stale_icon(self):
|
||||
with mock.patch('requests.get') as mock_get:
|
||||
mock_get.return_value = self.create_mock_response()
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
|
||||
icon_path = self.get_icon_path('https_example_com.png')
|
||||
|
||||
updated_mock_icon_data = b'updated_mock_icon'
|
||||
mock_get.return_value = self.create_mock_response(icon_data=updated_mock_icon_data)
|
||||
mock_get.reset_mock()
|
||||
|
||||
# change icon modification date so it is not stale yet
|
||||
nearly_one_day_ago = time.time() - 60 * 60 * 23
|
||||
os.utime(icon_path.absolute(), (nearly_one_day_ago, nearly_one_day_ago))
|
||||
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
mock_get.assert_not_called()
|
||||
|
||||
# change icon modification date so it is considered stale
|
||||
one_day_ago = time.time() - 60 * 60 * 24
|
||||
os.utime(icon_path.absolute(), (one_day_ago, one_day_ago))
|
||||
|
||||
favicon_loader.load_favicon('https://example.com')
|
||||
mock_get.assert_called()
|
||||
self.assertEqual(updated_mock_icon_data, self.get_icon_data('https_example_com.png'))
|
@@ -262,3 +262,12 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
|
||||
import_netscape_html(test_html, user)
|
||||
|
||||
mock_schedule_bookmarks_without_snapshots.assert_called_once_with(user)
|
||||
|
||||
def test_schedule_favicon_loading(self):
|
||||
user = self.get_or_create_test_user()
|
||||
test_html = self.render_html(tags_html='')
|
||||
|
||||
with patch.object(tasks, 'schedule_bookmarks_without_favicons') as mock_schedule_bookmarks_without_favicons:
|
||||
import_netscape_html(test_html, user)
|
||||
|
||||
mock_schedule_bookmarks_without_favicons.assert_called_once_with(user)
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import random
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
import requests
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from requests import RequestException
|
||||
|
||||
from bookmarks.models import UserProfile
|
||||
from bookmarks.services import tasks
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.views.settings import app_version, get_version_info
|
||||
|
||||
@@ -17,6 +18,20 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def create_profile_form_data(self, overrides=None):
|
||||
if not overrides:
|
||||
overrides = {}
|
||||
form_data = {
|
||||
'theme': UserProfile.THEME_AUTO,
|
||||
'bookmark_date_display': UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_BLANK,
|
||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
|
||||
'enable_sharing': False,
|
||||
'enable_favicons': False,
|
||||
}
|
||||
|
||||
return {**form_data, **overrides}
|
||||
|
||||
def test_should_render_successfully(self):
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
|
||||
@@ -28,15 +43,18 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.general'))
|
||||
|
||||
def test_should_save_profile(self):
|
||||
def test_update_profile(self):
|
||||
form_data = {
|
||||
'update_profile': '',
|
||||
'theme': UserProfile.THEME_DARK,
|
||||
'bookmark_date_display': UserProfile.BOOKMARK_DATE_DISPLAY_HIDDEN,
|
||||
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF,
|
||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
|
||||
'enable_sharing': True,
|
||||
'enable_favicons': True,
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
html = response.content.decode()
|
||||
|
||||
self.user.profile.refresh_from_db()
|
||||
|
||||
@@ -46,6 +64,118 @@ 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_favicons, form_data['enable_favicons'])
|
||||
self.assertInHTML('''
|
||||
<p class="form-input-hint">Profile updated</p>
|
||||
''', html)
|
||||
|
||||
def test_update_profile_should_not_be_called_without_respective_form_action(self):
|
||||
form_data = {
|
||||
'theme': UserProfile.THEME_DARK,
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
html = response.content.decode()
|
||||
|
||||
self.user.profile.refresh_from_db()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.user.profile.theme, UserProfile.THEME_AUTO)
|
||||
self.assertInHTML('''
|
||||
<p class="form-input-hint">Profile updated</p>
|
||||
''', html, count=0)
|
||||
|
||||
def test_enable_favicons_should_schedule_icon_update(self):
|
||||
with patch.object(tasks, 'schedule_bookmarks_without_favicons') as mock_schedule_bookmarks_without_favicons:
|
||||
# Enabling favicons schedules update
|
||||
form_data = self.create_profile_form_data({
|
||||
'update_profile': '',
|
||||
'enable_favicons': True,
|
||||
})
|
||||
self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
|
||||
mock_schedule_bookmarks_without_favicons.assert_called_once_with(self.user)
|
||||
|
||||
# No update scheduled if favicons are already enabled
|
||||
mock_schedule_bookmarks_without_favicons.reset_mock()
|
||||
|
||||
self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
|
||||
mock_schedule_bookmarks_without_favicons.assert_not_called()
|
||||
|
||||
# No update scheduled when disabling favicons
|
||||
form_data = self.create_profile_form_data({
|
||||
'enable_favicons': False,
|
||||
})
|
||||
|
||||
self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
|
||||
mock_schedule_bookmarks_without_favicons.assert_not_called()
|
||||
|
||||
def test_refresh_favicons(self):
|
||||
with patch.object(tasks, 'schedule_refresh_favicons') as mock_schedule_refresh_favicons:
|
||||
form_data = {
|
||||
'refresh_favicons': '',
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
html = response.content.decode()
|
||||
|
||||
mock_schedule_refresh_favicons.assert_called_once()
|
||||
self.assertInHTML('''
|
||||
<p class="form-input-hint">
|
||||
Scheduled favicon update. This may take a while...
|
||||
</p>
|
||||
''', html)
|
||||
|
||||
def test_refresh_favicons_should_not_be_called_without_respective_form_action(self):
|
||||
with patch.object(tasks, 'schedule_refresh_favicons') as mock_schedule_refresh_favicons:
|
||||
form_data = {
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
html = response.content.decode()
|
||||
|
||||
mock_schedule_refresh_favicons.assert_not_called()
|
||||
self.assertInHTML('''
|
||||
<p class="form-input-hint">
|
||||
Scheduled favicon update. This may take a while...
|
||||
</p>
|
||||
''', html, count=0)
|
||||
|
||||
def test_refresh_favicons_should_be_visible_when_favicons_enabled_in_profile(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<button class="btn mt-2" name="refresh_favicons">Refresh Favicons</button>
|
||||
''', html, count=1)
|
||||
|
||||
def test_refresh_favicons_should_not_be_visible_when_favicons_disabled_in_profile(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = False
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<button class="btn mt-2" name="refresh_favicons">Refresh Favicons</button>
|
||||
''', html, count=0)
|
||||
|
||||
@override_settings(LD_ENABLE_REFRESH_FAVICONS=False)
|
||||
def test_refresh_favicons_should_not_be_visible_when_disabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.enable_favicons = True
|
||||
profile.save()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<button class="btn mt-2" name="refresh_favicons">Refresh Favicons</button>
|
||||
''', html, count=0)
|
||||
|
||||
def test_about_shows_version_info(self):
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
|
Reference in New Issue
Block a user