import gzip import logging import os import random import shutil import tempfile from datetime import datetime from typing import List from unittest import TestCase from bs4 import BeautifulSoup from django.conf import settings from django.test import override_settings from django.utils import timezone from django.utils.crypto import get_random_string from rest_framework import status from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase from bookmarks.models import Bookmark, BookmarkAsset, BookmarkBundle, Tag, User class BookmarkFactoryMixin: user = None def setup_temp_assets_dir(self): self.assets_dir = tempfile.mkdtemp() self.settings_override = override_settings(LD_ASSET_FOLDER=self.assets_dir) self.settings_override.enable() self.addCleanup(self.cleanup_temp_assets_dir) def cleanup_temp_assets_dir(self): shutil.rmtree(self.assets_dir) self.settings_override.disable() def get_or_create_test_user(self): if self.user is None: self.user = User.objects.create_user( "testuser", "test@example.com", "password123" ) return self.user def setup_superuser(self): return User.objects.create_superuser( "superuser", "superuser@example.com", "password123" ) def setup_bookmark( self, is_archived: bool = False, unread: bool = False, shared: bool = False, tags=None, user: User = None, url: str = "", title: str = None, description: str = "", notes: str = "", web_archive_snapshot_url: str = "", favicon_file: str = "", preview_image_file: str = "", added: datetime = None, modified: datetime = None, ): if title is None: title = get_random_string(length=32) if tags is None: tags = [] if user is None: user = self.get_or_create_test_user() if not url: unique_id = get_random_string(length=32) url = "https://example.com/" + unique_id if added is None: added = timezone.now() if modified is None: modified = timezone.now() bookmark = Bookmark( url=url, title=title, description=description, notes=notes, date_added=added, date_modified=modified, owner=user, is_archived=is_archived, unread=unread, shared=shared, web_archive_snapshot_url=web_archive_snapshot_url, favicon_file=favicon_file, preview_image_file=preview_image_file, ) bookmark.save() for tag in tags: bookmark.tags.add(tag) bookmark.save() return bookmark def setup_numbered_bookmarks( self, count: int, prefix: str = "", suffix: str = "", tag_prefix: str = "", archived: bool = False, unread: bool = False, shared: bool = False, with_tags: bool = False, with_web_archive_snapshot_url: bool = False, with_favicon_file: bool = False, with_preview_image_file: bool = False, user: User = None, ): user = user or self.get_or_create_test_user() bookmarks = [] if not prefix: if archived: prefix = "Archived Bookmark" elif shared: prefix = "Shared Bookmark" else: prefix = "Bookmark" if not tag_prefix: if archived: tag_prefix = "Archived Tag" elif shared: tag_prefix = "Shared Tag" else: tag_prefix = "Tag" for i in range(1, count + 1): title = f"{prefix} {i}{suffix}" url = f"https://example.com/{prefix}/{i}" tags = [] if with_tags: tag_name = f"{tag_prefix} {i}{suffix}" tags = [self.setup_tag(name=tag_name, user=user)] web_archive_snapshot_url = "" if with_web_archive_snapshot_url: web_archive_snapshot_url = f"https://web.archive.org/web/{i}" favicon_file = "" if with_favicon_file: favicon_file = f"favicon_{i}.png" preview_image_file = "" if with_preview_image_file: preview_image_file = f"preview_image_{i}.png" bookmark = self.setup_bookmark( url=url, title=title, is_archived=archived, unread=unread, shared=shared, tags=tags, web_archive_snapshot_url=web_archive_snapshot_url, favicon_file=favicon_file, preview_image_file=preview_image_file, user=user, ) bookmarks.append(bookmark) return bookmarks def get_numbered_bookmark(self, title: str): return Bookmark.objects.get(title=title) def setup_bundle( self, user: User = None, name: str = None, search: str = "", any_tags: str = "", all_tags: str = "", excluded_tags: str = "", order: int = 0, ): if user is None: user = self.get_or_create_test_user() if not name: name = get_random_string(length=32) bundle = BookmarkBundle( name=name, owner=user, date_created=timezone.now(), search=search, any_tags=any_tags, all_tags=all_tags, excluded_tags=excluded_tags, order=order, ) bundle.save() return bundle def setup_asset( self, bookmark: Bookmark, date_created: datetime = None, file: str = None, file_size: int = None, asset_type: str = BookmarkAsset.TYPE_SNAPSHOT, content_type: str = "image/html", display_name: str = None, status: str = BookmarkAsset.STATUS_COMPLETE, gzip: bool = False, ): if date_created is None: date_created = timezone.now() if not file: file = get_random_string(length=32) if not display_name: display_name = file asset = BookmarkAsset( bookmark=bookmark, date_created=date_created, file=file, file_size=file_size, asset_type=asset_type, content_type=content_type, display_name=display_name, status=status, gzip=gzip, ) asset.save() return asset def setup_asset_file(self, asset: BookmarkAsset, file_content: str = "test"): filepath = os.path.join(settings.LD_ASSET_FOLDER, asset.file) if asset.gzip: with gzip.open(filepath, "wb") as f: f.write(file_content.encode()) else: with open(filepath, "w") as f: f.write(file_content) def read_asset_file(self, asset: BookmarkAsset): filepath = os.path.join(settings.LD_ASSET_FOLDER, asset.file) with open(filepath, "rb") as f: return f.read() def has_asset_file(self, asset: BookmarkAsset): filepath = os.path.join(settings.LD_ASSET_FOLDER, asset.file) return os.path.exists(filepath) def setup_tag(self, user: User = None, name: str = ""): if user is None: user = self.get_or_create_test_user() if not name: name = get_random_string(length=32) tag = Tag(name=name, date_added=timezone.now(), owner=user) tag.save() return tag 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 def get_tags_from_bookmarks(self, bookmarks: list[Bookmark]): all_tags = [] for bookmark in bookmarks: all_tags = all_tags + list(bookmark.tags.all()) return all_tags def get_random_string(self, length: int = 32): return get_random_string(length=length) class HtmlTestMixin: def make_soup(self, html: str): return BeautifulSoup(html, features="html.parser") class BookmarkListTestMixin(TestCase, HtmlTestMixin): def assertVisibleBookmarks( self, response, bookmarks: List[Bookmark], link_target: str = "_blank" ): soup = self.make_soup(response.content.decode()) bookmark_list = soup.select_one( f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]' ) self.assertIsNotNone(bookmark_list) bookmark_items = bookmark_list.select("li[ld-bookmark-item]") self.assertEqual(len(bookmark_items), len(bookmarks)) for bookmark in bookmarks: bookmark_item = bookmark_list.select_one( f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]' ) self.assertIsNotNone(bookmark_item) def assertInvisibleBookmarks( self, response, bookmarks: List[Bookmark], link_target: str = "_blank" ): soup = self.make_soup(response.content.decode()) for bookmark in bookmarks: bookmark_item = soup.select_one( f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]' ) self.assertIsNone(bookmark_item) class TagCloudTestMixin(TestCase, HtmlTestMixin): def assertVisibleTags(self, response, tags: List[Tag]): soup = self.make_soup(response.content.decode()) tag_cloud = soup.select_one("div.tag-cloud") self.assertIsNotNone(tag_cloud) tag_items = tag_cloud.select("a[data-is-tag-item]") self.assertEqual(len(tag_items), len(tags)) tag_item_names = [tag_item.text.strip() for tag_item in tag_items] for tag in tags: self.assertTrue(tag.name in tag_item_names) def assertInvisibleTags(self, response, tags: List[Tag]): soup = self.make_soup(response.content.decode()) tag_items = soup.select("a[data-is-tag-item]") tag_item_names = [tag_item.text.strip() for tag_item in tag_items] for tag in tags: self.assertFalse(tag.name in tag_item_names) def assertSelectedTags(self, response, tags: List[Tag]): soup = self.make_soup(response.content.decode()) selected_tags = soup.select_one("p.selected-tags") self.assertIsNotNone(selected_tags) tag_list = selected_tags.select("a") self.assertEqual(len(tag_list), len(tags)) for tag in tags: self.assertTrue( tag.name in selected_tags.text, msg=f"Selected tags do not contain: {tag.name}", ) class LinkdingApiTestCase(APITestCase): 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 get(self, url, expected_status_code=status.HTTP_200_OK): response = self.client.get(url) self.assertEqual(response.status_code, expected_status_code) return response def post(self, url, data=None, expected_status_code=status.HTTP_200_OK): response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, expected_status_code) return response def put(self, url, data=None, expected_status_code=status.HTTP_200_OK): response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, expected_status_code) return response def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK): response = self.client.patch(url, data, format="json") self.assertEqual(response.status_code, expected_status_code) return response def delete(self, url, expected_status_code=status.HTTP_200_OK): response = self.client.delete(url) self.assertEqual(response.status_code, expected_status_code) return response class BookmarkHtmlTag: def __init__( self, href: str = "", title: str = "", description: str = "", add_date: str = "", last_modified: str = "", tags: str = "", to_read: bool = False, private: bool = True, ): self.href = href self.title = title self.description = description self.add_date = add_date self.last_modified = last_modified self.tags = tags self.to_read = to_read self.private = private class ImportTestMixin: def render_tag(self, tag: BookmarkHtmlTag): return f"""
{tag.title if tag.title else ''} {f'
{tag.description}' if tag.description else ''} """ def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ""): if tags: rendered_tags = [self.render_tag(tag) for tag in tags] tags_html = "\n".join(rendered_tags) return f""" Bookmarks

Bookmarks

{tags_html}

""" _words = [ "quasi", "consequatur", "necessitatibus", "debitis", "quod", "vero", "qui", "commodi", "quod", "odio", "aliquam", "veniam", "architecto", "consequatur", "autem", "qui", "iste", "asperiores", "soluta", "et", ] def random_sentence(num_words: int = None, including_word: str = ""): if num_words is None: num_words = random.randint(5, 10) selected_words = random.choices(_words, k=num_words) if including_word: selected_words.append(including_word) random.shuffle(selected_words) return " ".join(selected_words) def disable_logging(f): def wrapper(*args): logging.disable(logging.CRITICAL) result = f(*args) logging.disable(logging.NOTSET) return result return wrapper def collapse_whitespace(text: str): text = text.replace("\n", "").replace("\r", "") return " ".join(text.split())