import datetime from dataclasses import dataclass from typing import Any from unittest import mock import waybackpy from background_task.models import Task 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 from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging def create_wayback_machine_save_api_mock( archive_url: str = "https://example.com/created_snapshot", fail_on_save: bool = False, ): mock_api = mock.Mock(archive_url=archive_url) if fail_on_save: mock_api.save.side_effect = WaybackError return mock_api @dataclass class MockCdxSnapshot: archive_url: str datetime_timestamp: datetime.datetime def create_cdx_server_api_mock( archive_url: str | None = "https://example.com/newest_snapshot", fail_loading_snapshot=False, ): mock_api = mock.Mock() if fail_loading_snapshot: mock_api.newest.side_effect = WaybackError elif archive_url: mock_api.newest.return_value = MockCdxSnapshot( archive_url, datetime.datetime.now() ) else: mock_api.newest.return_value = None return mock_api 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: Any): 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() @disable_logging def run_all_pending_tasks(self, task_function: Any): func = getattr(task_function, "task_function", None) 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() def test_create_web_archive_snapshot_should_update_snapshot_url(self): bookmark = self.setup_bookmark() mock_save_api = create_wayback_machine_save_api_mock() with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=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) bookmark.refresh_from_db() mock_save_api.save.assert_called_once() self.assertEqual( bookmark.web_archive_snapshot_url, "https://example.com/created_snapshot", ) def test_create_web_archive_snapshot_should_handle_missing_bookmark_id(self): mock_save_api = create_wayback_machine_save_api_mock() with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api ): tasks._create_web_archive_snapshot_task(123, False) self.run_pending_task(tasks._create_web_archive_snapshot_task) mock_save_api.save.assert_not_called() def test_create_web_archive_snapshot_should_skip_if_snapshot_exists(self): bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com") mock_save_api = create_wayback_machine_save_api_mock() with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=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) mock_save_api.assert_not_called() def test_create_web_archive_snapshot_should_force_update_snapshot(self): bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com") mock_save_api = create_wayback_machine_save_api_mock( archive_url="https://other.com" ) with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api ): 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() self.assertEqual(bookmark.web_archive_snapshot_url, "https://other.com") def test_create_web_archive_snapshot_should_use_newest_snapshot_as_fallback(self): bookmark = self.setup_bookmark() mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True) mock_cdx_api = create_cdx_server_api_mock() with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api ): with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): 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() mock_cdx_api.newest.assert_called_once() self.assertEqual( "https://example.com/newest_snapshot", bookmark.web_archive_snapshot_url, ) def test_create_web_archive_snapshot_should_ignore_missing_newest_snapshot(self): bookmark = self.setup_bookmark() mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True) mock_cdx_api = create_cdx_server_api_mock(archive_url=None) with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api ): with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): 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() self.assertEqual("", bookmark.web_archive_snapshot_url) def test_create_web_archive_snapshot_should_ignore_newest_snapshot_errors(self): bookmark = self.setup_bookmark() mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True) mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True) with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api ): with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): 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() self.assertEqual("", bookmark.web_archive_snapshot_url) def test_create_web_archive_snapshot_should_not_save_stale_bookmark_data(self): bookmark = self.setup_bookmark() mock_save_api = create_wayback_machine_save_api_mock() # update bookmark during API call to check that saving # the snapshot does not overwrite updated bookmark data def mock_save_impl(): bookmark.title = "Updated title" bookmark.save() mock_save_api.save.side_effect = mock_save_impl with mock.patch.object( waybackpy, "WaybackMachineSaveAPI", return_value=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) bookmark.refresh_from_db() self.assertEqual(bookmark.title, "Updated title") self.assertEqual( "https://example.com/created_snapshot", bookmark.web_archive_snapshot_url, ) def test_load_web_archive_snapshot_should_update_snapshot_url(self): bookmark = self.setup_bookmark() mock_cdx_api = create_cdx_server_api_mock() with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): tasks._load_web_archive_snapshot_task(bookmark.id) self.run_pending_task(tasks._load_web_archive_snapshot_task) bookmark.refresh_from_db() mock_cdx_api.newest.assert_called_once() self.assertEqual( "https://example.com/newest_snapshot", bookmark.web_archive_snapshot_url ) def test_load_web_archive_snapshot_should_handle_missing_bookmark_id(self): mock_cdx_api = create_cdx_server_api_mock() with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): tasks._load_web_archive_snapshot_task(123) self.run_pending_task(tasks._load_web_archive_snapshot_task) mock_cdx_api.newest.assert_not_called() def test_load_web_archive_snapshot_should_skip_if_snapshot_exists(self): bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com") mock_cdx_api = create_cdx_server_api_mock() with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): tasks._load_web_archive_snapshot_task(bookmark.id) self.run_pending_task(tasks._load_web_archive_snapshot_task) mock_cdx_api.newest.assert_not_called() def test_load_web_archive_snapshot_should_handle_missing_snapshot(self): bookmark = self.setup_bookmark() mock_cdx_api = create_cdx_server_api_mock(archive_url=None) with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): tasks._load_web_archive_snapshot_task(bookmark.id) self.run_pending_task(tasks._load_web_archive_snapshot_task) self.assertEqual("", bookmark.web_archive_snapshot_url) def test_load_web_archive_snapshot_should_handle_wayback_errors(self): bookmark = self.setup_bookmark() mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True) with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): tasks._load_web_archive_snapshot_task(bookmark.id) self.run_pending_task(tasks._load_web_archive_snapshot_task) self.assertEqual("", bookmark.web_archive_snapshot_url) def test_load_web_archive_snapshot_should_not_save_stale_bookmark_data(self): bookmark = self.setup_bookmark() mock_cdx_api = create_cdx_server_api_mock() # update bookmark during API call to check that saving # the snapshot does not overwrite updated bookmark data def mock_newest_impl(): bookmark.title = "Updated title" bookmark.save() return mock.DEFAULT mock_cdx_api.newest.side_effect = mock_newest_impl with mock.patch.object( bookmarks.services.wayback, "CustomWaybackMachineCDXServerAPI", return_value=mock_cdx_api, ): tasks._load_web_archive_snapshot_task(bookmark.id) self.run_pending_task(tasks._load_web_archive_snapshot_task) bookmark.refresh_from_db() self.assertEqual("Updated title", bookmark.title) self.assertEqual( "https://example.com/newest_snapshot", bookmark.web_archive_snapshot_url ) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True) def test_create_web_archive_snapshot_should_not_run_when_background_tasks_are_disabled( self, ): bookmark = self.setup_bookmark() tasks.create_web_archive_snapshot( self.get_or_create_test_user(), bookmark, False ) self.assertEqual(Task.objects.count(), 0) def test_create_web_archive_snapshot_should_not_run_when_web_archive_integration_is_disabled( self, ): self.user.profile.web_archive_integration = ( UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED ) self.user.profile.save() bookmark = self.setup_bookmark() tasks.create_web_archive_snapshot( self.get_or_create_test_user(), bookmark, False ) self.assertEqual(Task.objects.count(), 0) def test_schedule_bookmarks_without_snapshots_should_load_snapshot_for_all_bookmarks_without_snapshot( self, ): user = self.get_or_create_test_user() self.setup_bookmark() self.setup_bookmark() self.setup_bookmark() self.setup_bookmark(web_archive_snapshot_url="https://example.com") self.setup_bookmark(web_archive_snapshot_url="https://example.com") self.setup_bookmark(web_archive_snapshot_url="https://example.com") tasks.schedule_bookmarks_without_snapshots(user) self.run_pending_task(tasks._schedule_bookmarks_without_snapshots_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_web_archive_snapshot_task", ) def test_schedule_bookmarks_without_snapshots_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_snapshots(user) self.run_pending_task(tasks._schedule_bookmarks_without_snapshots_task) task_list = Task.objects.all() self.assertEqual(task_list.count(), 3) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True) def test_schedule_bookmarks_without_snapshots_should_not_run_when_background_tasks_are_disabled( self, ): tasks.schedule_bookmarks_without_snapshots(self.user) self.assertEqual(Task.objects.count(), 0) def test_schedule_bookmarks_without_snapshots_should_not_run_when_web_archive_integration_is_disabled( self, ): self.user.profile.web_archive_integration = ( UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED ) self.user.profile.save() 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() def test_load_favicon_should_not_save_stale_bookmark_data(self): bookmark = self.setup_bookmark() # update bookmark during API call to check that saving # the favicon does not overwrite updated bookmark data def mock_load_favicon_impl(url): bookmark.title = "Updated title" bookmark.save() return "https_example_com.png" with mock.patch( "bookmarks.services.favicon_loader.load_favicon" ) as mock_load_favicon: mock_load_favicon.side_effect = mock_load_favicon_impl 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.title, "Updated title") self.assertEqual(bookmark.favicon_file, "https_example_com.png") @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)