from unittest.mock import patch from django.test import TestCase, override_settings from django.urls import reverse from django.utils import formats from bookmarks.models import BookmarkAsset, UserProfile from bookmarks.services import tasks from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin): def setUp(self): user = self.get_or_create_test_user() self.client.force_login(user) def get_view_name(self): return "bookmarks:details_modal" def get_base_url(self, bookmark): return reverse(self.get_view_name(), args=[bookmark.id]) def get_details_form(self, soup, bookmark): expected_url = reverse("bookmarks:details", args=[bookmark.id]) return soup.find("form", {"action": expected_url}) def get_details(self, bookmark, return_url=""): url = self.get_base_url(bookmark) if return_url: url += f"?return_url={return_url}" response = self.client.get(url) soup = self.make_soup(response.content) return soup def find_section(self, soup, section_name): dt = soup.find("dt", string=section_name) dd = dt.find_next_sibling("dd") if dt else None return dd def get_section(self, soup, section_name): dd = self.find_section(soup, section_name) self.assertIsNotNone(dd) return dd def find_weblink(self, soup, url): return soup.find("a", {"class": "weblink", "href": url}) def find_asset(self, soup, asset): return soup.find("div", {"data-asset-id": asset.id}) def details_route_access_test(self, view_name: str, shareable: bool): # own bookmark bookmark = self.setup_bookmark() response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 200) # other user's bookmark other_user = self.setup_user() bookmark = self.setup_bookmark(user=other_user) response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 404) # non-existent bookmark response = self.client.get(reverse(view_name, args=[9999])) self.assertEqual(response.status_code, 404) # guest user self.client.logout() response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 404 if shareable else 302) def details_route_sharing_access_test(self, view_name: str, shareable: bool): # shared bookmark, sharing disabled other_user = self.setup_user() bookmark = self.setup_bookmark(shared=True, user=other_user) response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 404) # shared bookmark, sharing enabled profile = other_user.profile profile.enable_sharing = True profile.save() response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 200 if shareable else 404) # shared bookmark, guest user, no public sharing self.client.logout() response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 404 if shareable else 302) # shared bookmark, guest user, public sharing profile.enable_public_sharing = True profile.save() response = self.client.get(reverse(view_name, args=[bookmark.id])) self.assertEqual(response.status_code, 200 if shareable else 302) def test_access(self): self.details_route_access_test(self.get_view_name(), True) def test_access_with_sharing(self): self.details_route_sharing_access_test(self.get_view_name(), True) def test_form_partial_access(self): # form partial is only used when submitting forms, which should be only # accessible to the owner of the bookmark. As such assume it requires # login. self.details_route_access_test("bookmarks:partials.details_form", False) def test_form_partial_access_with_sharing(self): # form partial is only used when submitting forms, which should be only # accessible to the owner of the bookmark. As such assume it requires # login. self.details_route_sharing_access_test("bookmarks:partials.details_form", False) def test_displays_title(self): # with title bookmark = self.setup_bookmark(title="Test title") soup = self.get_details(bookmark) title = soup.find("h2") self.assertIsNotNone(title) self.assertEqual(title.text.strip(), bookmark.title) # with website title bookmark = self.setup_bookmark(title="", website_title="Website title") soup = self.get_details(bookmark) title = soup.find("h2") self.assertIsNotNone(title) self.assertEqual(title.text.strip(), bookmark.website_title) # with URL only bookmark = self.setup_bookmark(title="", website_title="") soup = self.get_details(bookmark) title = soup.find("h2") self.assertIsNotNone(title) self.assertEqual(title.text.strip(), bookmark.url) def test_website_link(self): # basics bookmark = self.setup_bookmark() soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.url) self.assertIsNotNone(link) self.assertEqual(link["href"], bookmark.url) self.assertEqual(link.text.strip(), bookmark.url) # favicons disabled bookmark = self.setup_bookmark(favicon_file="example.png") soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.url) image = link.select_one("img") self.assertIsNone(image) # favicons enabled, no favicon profile = self.get_or_create_test_user().profile profile.enable_favicons = True profile.save() bookmark = self.setup_bookmark(favicon_file="") soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.url) image = link.select_one("img") self.assertIsNone(image) # favicons enabled, favicon present bookmark = self.setup_bookmark(favicon_file="example.png") soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.url) image = link.select_one("img") self.assertIsNotNone(image) self.assertEqual(image["src"], "/static/example.png") def test_internet_archive_link(self): # without snapshot url bookmark = self.setup_bookmark() soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) self.assertIsNone(link) # with snapshot url bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com/") soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) self.assertIsNotNone(link) self.assertEqual(link["href"], bookmark.web_archive_snapshot_url) self.assertEqual(link.text.strip(), "View on Internet Archive") # favicons disabled bookmark = self.setup_bookmark( web_archive_snapshot_url="https://example.com/", favicon_file="example.png" ) soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) image = link.select_one("svg") self.assertIsNone(image) # favicons enabled, no favicon profile = self.get_or_create_test_user().profile profile.enable_favicons = True profile.save() bookmark = self.setup_bookmark( web_archive_snapshot_url="https://example.com/", favicon_file="" ) soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) image = link.select_one("svg") self.assertIsNone(image) # favicons enabled, favicon present bookmark = self.setup_bookmark( web_archive_snapshot_url="https://example.com/", favicon_file="example.png" ) soup = self.get_details(bookmark) link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) image = link.select_one("svg") self.assertIsNotNone(image) def test_weblinks_respect_target_setting(self): bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com/") # target blank profile = self.get_or_create_test_user().profile profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_BLANK profile.save() soup = self.get_details(bookmark) website_link = self.find_weblink(soup, bookmark.url) self.assertIsNotNone(website_link) self.assertEqual(website_link["target"], UserProfile.BOOKMARK_LINK_TARGET_BLANK) web_archive_link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) self.assertIsNotNone(web_archive_link) self.assertEqual( web_archive_link["target"], UserProfile.BOOKMARK_LINK_TARGET_BLANK ) # target self profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF profile.save() soup = self.get_details(bookmark) website_link = self.find_weblink(soup, bookmark.url) self.assertIsNotNone(website_link) self.assertEqual(website_link["target"], UserProfile.BOOKMARK_LINK_TARGET_SELF) web_archive_link = self.find_weblink(soup, bookmark.web_archive_snapshot_url) self.assertIsNotNone(web_archive_link) self.assertEqual( web_archive_link["target"], UserProfile.BOOKMARK_LINK_TARGET_SELF ) def test_status(self): # renders form bookmark = self.setup_bookmark() soup = self.get_details(bookmark) form = self.get_details_form(soup, bookmark) self.assertIsNotNone(form) self.assertEqual( form["action"], reverse("bookmarks:details", args=[bookmark.id]) ) self.assertEqual(form["method"], "post") # sharing disabled bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.get_section(soup, "Status") archived = section.find("input", {"type": "checkbox", "name": "is_archived"}) self.assertIsNotNone(archived) unread = section.find("input", {"type": "checkbox", "name": "unread"}) self.assertIsNotNone(unread) shared = section.find("input", {"type": "checkbox", "name": "shared"}) self.assertIsNone(shared) # sharing enabled profile = self.get_or_create_test_user().profile profile.enable_sharing = True profile.save() bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.get_section(soup, "Status") archived = section.find("input", {"type": "checkbox", "name": "is_archived"}) self.assertIsNotNone(archived) unread = section.find("input", {"type": "checkbox", "name": "unread"}) self.assertIsNotNone(unread) shared = section.find("input", {"type": "checkbox", "name": "shared"}) self.assertIsNotNone(shared) # unchecked bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.get_section(soup, "Status") archived = section.find("input", {"type": "checkbox", "name": "is_archived"}) self.assertFalse(archived.has_attr("checked")) unread = section.find("input", {"type": "checkbox", "name": "unread"}) self.assertFalse(unread.has_attr("checked")) shared = section.find("input", {"type": "checkbox", "name": "shared"}) self.assertFalse(shared.has_attr("checked")) # checked bookmark = self.setup_bookmark(is_archived=True, unread=True, shared=True) soup = self.get_details(bookmark) section = self.get_section(soup, "Status") archived = section.find("input", {"type": "checkbox", "name": "is_archived"}) self.assertTrue(archived.has_attr("checked")) unread = section.find("input", {"type": "checkbox", "name": "unread"}) self.assertTrue(unread.has_attr("checked")) shared = section.find("input", {"type": "checkbox", "name": "shared"}) self.assertTrue(shared.has_attr("checked")) def test_status_visibility(self): # own bookmark bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.find_section(soup, "Status") self.assertIsNotNone(section) # other user's bookmark other_user = self.setup_user(enable_sharing=True) bookmark = self.setup_bookmark(user=other_user, shared=True) soup = self.get_details(bookmark) section = self.find_section(soup, "Status") self.assertIsNone(section) # guest user self.client.logout() bookmark = self.setup_bookmark(user=other_user, shared=True) soup = self.get_details(bookmark) section = self.find_section(soup, "Status") self.assertIsNone(section) def test_status_update(self): bookmark = self.setup_bookmark() # update status response = self.client.post( self.get_base_url(bookmark), {"is_archived": "on", "unread": "on", "shared": "on"}, ) self.assertEqual(response.status_code, 302) bookmark.refresh_from_db() self.assertTrue(bookmark.is_archived) self.assertTrue(bookmark.unread) self.assertTrue(bookmark.shared) # update individual status response = self.client.post( self.get_base_url(bookmark), {"is_archived": "", "unread": "on", "shared": ""}, ) self.assertEqual(response.status_code, 302) bookmark.refresh_from_db() self.assertFalse(bookmark.is_archived) self.assertTrue(bookmark.unread) self.assertFalse(bookmark.shared) def test_status_update_access(self): # no sharing other_user = self.setup_user() bookmark = self.setup_bookmark(user=other_user) response = self.client.post( self.get_base_url(bookmark), {"is_archived": "on", "unread": "on", "shared": "on"}, ) self.assertEqual(response.status_code, 404) # shared, sharing disabled bookmark = self.setup_bookmark(user=other_user, shared=True) response = self.client.post( self.get_base_url(bookmark), {"is_archived": "on", "unread": "on", "shared": "on"}, ) self.assertEqual(response.status_code, 404) # shared, sharing enabled bookmark = self.setup_bookmark(user=other_user, shared=True) profile = other_user.profile profile.enable_sharing = True profile.save() response = self.client.post( self.get_base_url(bookmark), {"is_archived": "on", "unread": "on", "shared": "on"}, ) self.assertEqual(response.status_code, 404) # shared, public sharing enabled bookmark = self.setup_bookmark(user=other_user, shared=True) profile = other_user.profile profile.enable_public_sharing = True profile.save() response = self.client.post( self.get_base_url(bookmark), {"is_archived": "on", "unread": "on", "shared": "on"}, ) self.assertEqual(response.status_code, 404) # guest user self.client.logout() bookmark = self.setup_bookmark(user=other_user, shared=True) response = self.client.post( self.get_base_url(bookmark), {"is_archived": "on", "unread": "on", "shared": "on"}, ) self.assertEqual(response.status_code, 404) def test_date_added(self): bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.get_section(soup, "Date added") expected_date = formats.date_format(bookmark.date_added, "DATETIME_FORMAT") date = section.find("span", string=expected_date) self.assertIsNotNone(date) def test_tags(self): # without tags bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.find_section(soup, "Tags") self.assertIsNone(section) # with tags bookmark = self.setup_bookmark(tags=[self.setup_tag(), self.setup_tag()]) soup = self.get_details(bookmark) section = self.get_section(soup, "Tags") for tag in bookmark.tags.all(): tag_link = section.find("a", string=f"#{tag.name}") self.assertIsNotNone(tag_link) expected_url = reverse("bookmarks:index") + f"?q=%23{tag.name}" self.assertEqual(tag_link["href"], expected_url) def test_description(self): # without description bookmark = self.setup_bookmark(description="", website_description="") soup = self.get_details(bookmark) section = self.find_section(soup, "Description") self.assertIsNone(section) # with description bookmark = self.setup_bookmark(description="Test description") soup = self.get_details(bookmark) section = self.get_section(soup, "Description") self.assertEqual(section.text.strip(), bookmark.description) # with website description bookmark = self.setup_bookmark( description="", website_description="Website description" ) soup = self.get_details(bookmark) section = self.get_section(soup, "Description") self.assertEqual(section.text.strip(), bookmark.website_description) def test_notes(self): # without notes bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.find_section(soup, "Notes") self.assertIsNone(section) # with notes bookmark = self.setup_bookmark(notes="Test notes") soup = self.get_details(bookmark) section = self.get_section(soup, "Notes") self.assertEqual(section.decode_contents(), "
Test notes
") def test_edit_link(self): bookmark = self.setup_bookmark() # with default return URL soup = self.get_details(bookmark) edit_link = soup.find("a", string="Edit") self.assertIsNotNone(edit_link) details_url = reverse("bookmarks:details", args=[bookmark.id]) expected_url = ( reverse("bookmarks:edit", args=[bookmark.id]) + "?return_url=" + details_url ) self.assertEqual(edit_link["href"], expected_url) # with custom return URL soup = self.get_details(bookmark, return_url="/custom") edit_link = soup.find("a", string="Edit") self.assertIsNotNone(edit_link) expected_url = ( reverse("bookmarks:edit", args=[bookmark.id]) + "?return_url=/custom" ) self.assertEqual(edit_link["href"], expected_url) def test_delete_button(self): bookmark = self.setup_bookmark() # basics soup = self.get_details(bookmark) delete_button = soup.find("button", {"type": "submit", "name": "remove"}) self.assertIsNotNone(delete_button) self.assertEqual(delete_button.text.strip(), "Delete...") self.assertEqual(delete_button["value"], str(bookmark.id)) form = delete_button.find_parent("form") self.assertIsNotNone(form) expected_url = reverse("bookmarks:index.action") + f"?return_url=/bookmarks" self.assertEqual(form["action"], expected_url) # with custom return URL soup = self.get_details(bookmark, return_url="/custom") delete_button = soup.find("button", {"type": "submit", "name": "remove"}) form = delete_button.find_parent("form") expected_url = reverse("bookmarks:index.action") + f"?return_url=/custom" self.assertEqual(form["action"], expected_url) def test_actions_visibility(self): # with sharing other_user = self.setup_user(enable_sharing=True) bookmark = self.setup_bookmark(user=other_user, shared=True) soup = self.get_details(bookmark) edit_link = soup.find("a", string="Edit") delete_button = soup.find("button", {"type": "submit", "name": "remove"}) self.assertIsNone(edit_link) self.assertIsNone(delete_button) # with public sharing profile = other_user.profile profile.enable_public_sharing = True profile.save() bookmark = self.setup_bookmark(user=other_user, shared=True) soup = self.get_details(bookmark) edit_link = soup.find("a", string="Edit") delete_button = soup.find("button", {"type": "submit", "name": "remove"}) self.assertIsNone(edit_link) self.assertIsNone(delete_button) # guest user self.client.logout() bookmark = self.setup_bookmark(user=other_user, shared=True) soup = self.get_details(bookmark) edit_link = soup.find("a", string="Edit") delete_button = soup.find("button", {"type": "submit", "name": "remove"}) self.assertIsNone(edit_link) self.assertIsNone(delete_button) def test_assets_visibility_no_snapshot_support(self): bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.find_section(soup, "Files") self.assertIsNone(section) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_assets_visibility_with_snapshot_support(self): bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.find_section(soup, "Files") self.assertIsNotNone(section) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_asset_list_visibility(self): # no assets bookmark = self.setup_bookmark() soup = self.get_details(bookmark) section = self.get_section(soup, "Files") asset_list = section.find("div", {"class": "assets"}) self.assertIsNone(asset_list) # with assets bookmark = self.setup_bookmark() self.setup_asset(bookmark) soup = self.get_details(bookmark) section = self.get_section(soup, "Files") asset_list = section.find("div", {"class": "assets"}) self.assertIsNotNone(asset_list) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_asset_list(self): bookmark = self.setup_bookmark() assets = [ self.setup_asset(bookmark), self.setup_asset(bookmark), self.setup_asset(bookmark), ] soup = self.get_details(bookmark) section = self.get_section(soup, "Files") asset_list = section.find("div", {"class": "assets"}) for asset in assets: asset_item = self.find_asset(asset_list, asset) self.assertIsNotNone(asset_item) asset_icon = asset_item.select_one(".asset-icon svg") self.assertIsNotNone(asset_icon) asset_text = asset_item.select_one(".asset-text span") self.assertIsNotNone(asset_text) self.assertIn(asset.display_name, asset_text.text) view_url = reverse("bookmarks:assets.view", args=[asset.id]) view_link = asset_item.find("a", {"href": view_url}) self.assertIsNotNone(view_link) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_asset_without_file(self): bookmark = self.setup_bookmark() asset = self.setup_asset(bookmark) asset.file = "" asset.save() soup = self.get_details(bookmark) asset_item = self.find_asset(soup, asset) view_url = reverse("bookmarks:assets.view", args=[asset.id]) view_link = asset_item.find("a", {"href": view_url}) self.assertIsNone(view_link) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_asset_status(self): bookmark = self.setup_bookmark() pending_asset = self.setup_asset(bookmark, status=BookmarkAsset.STATUS_PENDING) failed_asset = self.setup_asset(bookmark, status=BookmarkAsset.STATUS_FAILURE) soup = self.get_details(bookmark) asset_item = self.find_asset(soup, pending_asset) asset_text = asset_item.select_one(".asset-text span") self.assertIn("(queued)", asset_text.text) asset_item = self.find_asset(soup, failed_asset) asset_text = asset_item.select_one(".asset-text span") self.assertIn("(failed)", asset_text.text) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_asset_file_size(self): bookmark = self.setup_bookmark() asset1 = self.setup_asset(bookmark, file_size=None) asset2 = self.setup_asset(bookmark, file_size=54639) asset3 = self.setup_asset(bookmark, file_size=11492020) soup = self.get_details(bookmark) asset_item = self.find_asset(soup, asset1) asset_text = asset_item.select_one(".asset-text") self.assertEqual(asset_text.text.strip(), asset1.display_name) asset_item = self.find_asset(soup, asset2) asset_text = asset_item.select_one(".asset-text") self.assertIn("53.4\xa0KB", asset_text.text) asset_item = self.find_asset(soup, asset3) asset_text = asset_item.select_one(".asset-text") self.assertIn("11.0\xa0MB", asset_text.text) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_asset_actions_visibility(self): bookmark = self.setup_bookmark() # with file asset = self.setup_asset(bookmark) soup = self.get_details(bookmark) asset_item = self.find_asset(soup, asset) view_link = asset_item.find("a", string="View") delete_button = asset_item.find( "button", {"type": "submit", "name": "remove_asset"} ) self.assertIsNotNone(view_link) self.assertIsNotNone(delete_button) # without file asset.file = "" asset.save() soup = self.get_details(bookmark) asset_item = self.find_asset(soup, asset) view_link = asset_item.find("a", string="View") delete_button = asset_item.find( "button", {"type": "submit", "name": "remove_asset"} ) self.assertIsNone(view_link) self.assertIsNotNone(delete_button) # shared bookmark other_user = self.setup_user(enable_sharing=True, enable_public_sharing=True) bookmark = self.setup_bookmark(shared=True, user=other_user) asset = self.setup_asset(bookmark) soup = self.get_details(bookmark) asset_item = self.find_asset(soup, asset) view_link = asset_item.find("a", string="View") delete_button = asset_item.find( "button", {"type": "submit", "name": "remove_asset"} ) self.assertIsNotNone(view_link) self.assertIsNone(delete_button) # shared bookmark, guest user self.client.logout() soup = self.get_details(bookmark) asset_item = self.find_asset(soup, asset) view_link = asset_item.find("a", string="View") delete_button = asset_item.find( "button", {"type": "submit", "name": "remove_asset"} ) self.assertIsNotNone(view_link) self.assertIsNone(delete_button) def test_remove_asset(self): # remove asset bookmark = self.setup_bookmark() asset = self.setup_asset(bookmark) response = self.client.post( self.get_base_url(bookmark), {"remove_asset": asset.id} ) self.assertEqual(response.status_code, 302) self.assertFalse(BookmarkAsset.objects.filter(id=asset.id).exists()) # non-existent asset response = self.client.post(self.get_base_url(bookmark), {"remove_asset": 9999}) self.assertEqual(response.status_code, 404) # post without asset ID does not remove asset = self.setup_asset(bookmark) response = self.client.post(self.get_base_url(bookmark)) self.assertEqual(response.status_code, 302) self.assertTrue(BookmarkAsset.objects.filter(id=asset.id).exists()) # guest user asset = self.setup_asset(bookmark) self.client.logout() response = self.client.post( self.get_base_url(bookmark), {"remove_asset": asset.id} ) self.assertEqual(response.status_code, 404) self.assertTrue(BookmarkAsset.objects.filter(id=asset.id).exists()) @override_settings(LD_ENABLE_SNAPSHOTS=True) def test_create_snapshot(self): with patch.object( tasks, "_create_html_snapshot_task" ) as mock_create_html_snapshot_task: bookmark = self.setup_bookmark() response = self.client.post( self.get_base_url(bookmark), {"create_snapshot": ""} ) self.assertEqual(response.status_code, 302) self.assertEqual(bookmark.bookmarkasset_set.count(), 1)