578 lines
24 KiB
Python
578 lines
24 KiB
Python
# Standard library imports.
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
# External library imports.
|
|
from PIL import Image
|
|
|
|
# Django imports.
|
|
from django.test import Client, RequestFactory, TestCase, override_settings
|
|
from django.contrib.auth.models import User
|
|
from django.utils import timezone
|
|
from django.http import HttpResponse
|
|
from django.template.response import TemplateResponse
|
|
|
|
# Project imports.
|
|
from viewer.common import SORT_LABELS
|
|
from viewer.directory import do_recursive_search
|
|
from viewer.views import gallery_view
|
|
from viewer.models import Image as Im, UserSettings
|
|
from NibasaViewer.middleware import UserSettingsMiddleware
|
|
|
|
|
|
class GalleryBaseTests(TestCase):
|
|
def setUp(self):
|
|
self.tmp_gallery = tempfile.TemporaryDirectory()
|
|
self.tmp_thumbs = tempfile.TemporaryDirectory()
|
|
|
|
self.gallery_root = Path(self.tmp_gallery.name)
|
|
self.thumb_root = Path(self.tmp_thumbs.name)
|
|
|
|
self.settings_override = override_settings(
|
|
GALLERY_ROOT=self.gallery_root, THUMBNAILS_ROOT=self.thumb_root
|
|
)
|
|
self.settings_override.enable()
|
|
|
|
self.user = User.objects.create_user(
|
|
"tester", "tester@example.com", "password123"
|
|
)
|
|
self.client = Client()
|
|
self.client.force_login(self.user)
|
|
self.factory = RequestFactory()
|
|
# Ensure UserSettings exists for the test user so middleware-backed
|
|
# attributes (`theme`, `sort`) are available during view rendering
|
|
# even when tests call views directly via RequestFactory.
|
|
UserSettings.objects.get_or_create(user=self.user)
|
|
|
|
self._build_fixture_tree()
|
|
|
|
def tearDown(self):
|
|
self.settings_override.disable()
|
|
self.tmp_gallery.cleanup()
|
|
self.tmp_thumbs.cleanup()
|
|
|
|
def _create_image(self, image_path, color):
|
|
image_path.parent.mkdir(parents=True, exist_ok=True)
|
|
img = Image.new("RGB", (300, 200), color)
|
|
img.save(image_path)
|
|
|
|
def _set_mtime(self, file_path, timestamp):
|
|
os.utime(file_path, (timestamp, timestamp))
|
|
|
|
def _build_fixture_tree(self):
|
|
self._create_image(self.gallery_root / "alpha.jpg", "red")
|
|
self._create_image(self.gallery_root / "beta.jpg", "green")
|
|
self._create_image(self.gallery_root / "gamma.jpg", "blue")
|
|
self._create_image(self.gallery_root / "match-root.jpg", "yellow")
|
|
|
|
self._set_mtime(self.gallery_root / "alpha.jpg", 1000)
|
|
self._set_mtime(self.gallery_root / "beta.jpg", 2000)
|
|
self._set_mtime(self.gallery_root / "gamma.jpg", 3000)
|
|
self._set_mtime(self.gallery_root / "match-root.jpg", 4000)
|
|
|
|
self.sub_a = self.gallery_root / "sub_a"
|
|
self.sub_b = self.gallery_root / "sub_b"
|
|
self.empty_sub = self.gallery_root / "empty_sub"
|
|
self.nested = self.gallery_root / "nested"
|
|
|
|
self.sub_a.mkdir(parents=True, exist_ok=True)
|
|
self.sub_b.mkdir(parents=True, exist_ok=True)
|
|
self.empty_sub.mkdir(parents=True, exist_ok=True)
|
|
self.nested.mkdir(parents=True, exist_ok=True)
|
|
|
|
self._create_image(self.sub_a / "cover.jpg", "orange")
|
|
self._create_image(self.sub_a / "same.jpg", "purple")
|
|
self._create_image(self.sub_b / "same.jpg", "pink")
|
|
self._create_image(self.nested / "match-nested.jpg", "cyan")
|
|
|
|
self.symlink_supported = hasattr(os, "symlink")
|
|
self.symlink_dir = self.gallery_root / "symlink_sub"
|
|
self.symlink_file = self.gallery_root / "symlink_file.jpg"
|
|
|
|
if self.symlink_supported:
|
|
try:
|
|
os.symlink(self.sub_a, self.symlink_dir)
|
|
os.symlink(self.gallery_root / "alpha.jpg", self.symlink_file)
|
|
except OSError:
|
|
self.symlink_supported = False
|
|
|
|
|
|
class GalleryViewTests(GalleryBaseTests):
|
|
def test_gallery_rejects_out_of_root_path(self):
|
|
request = self.factory.get("/gallery/../")
|
|
request.user = self.user
|
|
|
|
# When calling the view directly we still need to allow any
|
|
# TemplateResponse processing to see the settings. Ensure the
|
|
# request has the attributes the middleware would provide.
|
|
request.theme = UserSettings._meta.get_field("theme").get_default()
|
|
request.sort = UserSettings._meta.get_field("sort").get_default()
|
|
|
|
response = gallery_view(request, "../")
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_gallery_missing_path_returns_404(self):
|
|
response = self.client.get("/gallery/does-not-exist/")
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
def test_invalid_sort_defaults_to_abc(self):
|
|
response = self.client.get("/gallery/?sort=invalid")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context["sort_key"], "abc")
|
|
|
|
def test_invalid_theme_defaults_to_dark(self):
|
|
response = self.client.get("/gallery/?theme=invalid")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context["theme"], "dark")
|
|
|
|
def test_root_path_context_is_empty_string(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context["path"], "")
|
|
|
|
def test_context_has_no_pagination_keys(self):
|
|
response = self.client.get("/gallery/")
|
|
context = response.context
|
|
|
|
self.assertNotIn("page", context)
|
|
self.assertNotIn("pages", context)
|
|
self.assertNotIn("num_pages", context)
|
|
self.assertNotIn("prev_page", context)
|
|
self.assertNotIn("next_page", context)
|
|
|
|
def test_sort_accepts_all_required_keys(self):
|
|
for sort_key in ["abc", "cba", "old", "new", "recent", "tnecer"]:
|
|
response = self.client.get("/gallery/?sort=" + sort_key)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context["sort_key"], sort_key)
|
|
|
|
def test_sort_labels_match_required_text(self):
|
|
response = self.client.get("/gallery/?sort=recent")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
options = {
|
|
item["key"]: item["label"] for item in response.context["sort_options"]
|
|
}
|
|
self.assertEqual(options, SORT_LABELS)
|
|
self.assertEqual(response.context["sort_label"], SORT_LABELS["recent"])
|
|
|
|
def test_sort_tie_breaker_filename_then_relative_path(self):
|
|
response = self.client.get("/gallery/?search=same&sort=abc")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
image_paths = [item["path"] for item in response.context["images"]]
|
|
self.assertEqual(len(image_paths), 2)
|
|
self.assertTrue(image_paths[0].find("/gallery/sub_a/same.jpg") != -1)
|
|
self.assertTrue(image_paths[1].find("/gallery/sub_b/same.jpg") != -1)
|
|
|
|
def test_sort_options_shape_and_is_active(self):
|
|
response = self.client.get("/gallery/?sort=cba&theme=light")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
options = response.context["sort_options"]
|
|
self.assertEqual(len(options), 6)
|
|
|
|
for option in options:
|
|
self.assertIn("key", option)
|
|
self.assertIn("label", option)
|
|
self.assertIn("url", option)
|
|
self.assertIn("is_active", option)
|
|
|
|
active = [option for option in options if option["is_active"]]
|
|
self.assertEqual(len(active), 1)
|
|
self.assertEqual(active[0]["key"], "cba")
|
|
|
|
def test_breadcrumb_links_preserve_query(self):
|
|
response = self.client.get(
|
|
"/gallery/sub_a/?search=match&sort=recent&theme=light"
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
breadcrumbs = response.context["breadcrumbs"]
|
|
self.assertTrue(breadcrumbs[0]["path"].find("/gallery/?") == 0)
|
|
self.assertIn("search=match", breadcrumbs[0]["path"])
|
|
# build_query now only preserves search; sort/theme are not included
|
|
self.assertNotIn("sort=", breadcrumbs[0]["path"])
|
|
self.assertNotIn("theme=", breadcrumbs[0]["path"])
|
|
|
|
def test_subdir_links_preserve_query(self):
|
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
subdir_paths = [subdir["path"] for subdir in response.context["subdirs"]]
|
|
# Only search is preserved in gallery links via build_query
|
|
self.assertTrue(any(path.find("search=match") != -1 for path in subdir_paths))
|
|
self.assertFalse(any(path.find("sort=") != -1 for path in subdir_paths))
|
|
self.assertFalse(any(path.find("theme=") != -1 for path in subdir_paths))
|
|
|
|
def test_image_links_preserve_query(self):
|
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=light")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
image_paths = [image["path"] for image in response.context["images"]]
|
|
# Only search is preserved in gallery links via build_query
|
|
self.assertTrue(any(path.find("search=match") != -1 for path in image_paths))
|
|
self.assertFalse(any(path.find("sort=") != -1 for path in image_paths))
|
|
self.assertFalse(any(path.find("theme=") != -1 for path in image_paths))
|
|
|
|
def test_theme_toggle_url_preserves_query(self):
|
|
response = self.client.get("/gallery/?search=match&sort=recent&theme=dark")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
url = response.context["theme_toggle_url"]
|
|
self.assertIn("search=match", url)
|
|
self.assertIn("sort=recent", url)
|
|
self.assertIn("theme=light", url)
|
|
|
|
def test_search_action_url_preserves_query(self):
|
|
response = self.client.get("/gallery/?sort=recent&theme=light")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
url = response.context["search_action_url"]
|
|
self.assertTrue(url.find("/gallery/?") == 0)
|
|
self.assertIn("sort=recent", url)
|
|
self.assertIn("theme=light", url)
|
|
|
|
def test_search_action_url_uses_nested_path(self):
|
|
response = self.client.get("/gallery/sub_a/?sort=recent&theme=light")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
url = response.context["search_action_url"]
|
|
self.assertTrue(url.find("/gallery/sub_a/?") == 0)
|
|
|
|
def test_subdirs_use_immediate_children_only(self):
|
|
response = self.client.get("/gallery/?search=match")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
names = [subdir["name"] for subdir in response.context["subdirs"]]
|
|
self.assertIn("nested", names)
|
|
self.assertNotIn("match-nested.jpg", names)
|
|
|
|
def test_subdir_order_is_deterministic(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
names = [subdir["name"] for subdir in response.context["subdirs"]]
|
|
self.assertEqual(names, sorted(names, key=lambda item: item.lower()))
|
|
|
|
def test_subdir_without_images_uses_fallback(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
empty_sub = [
|
|
subdir
|
|
for subdir in response.context["subdirs"]
|
|
if subdir["name"] == "empty_sub"
|
|
][0]
|
|
self.assertIsNone(empty_sub["thumbnail"])
|
|
|
|
def test_symlink_entries_ignored_in_gallery(self):
|
|
if not self.symlink_supported:
|
|
self.skipTest("Symlinks not supported in this environment")
|
|
|
|
response = self.client.get("/gallery/?search=symlink")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
names = [image["name"] for image in response.context["images"]]
|
|
self.assertNotIn("symlink_file.jpg", names)
|
|
|
|
def test_recursive_search_ignores_symlink_directories(self):
|
|
if not self.symlink_supported:
|
|
self.skipTest("Symlinks not supported in this environment")
|
|
|
|
subdirs, images = do_recursive_search(self.gallery_root, "cover")
|
|
subdir_names = [subdir.name for subdir in subdirs]
|
|
image_names = [image.name for image in images]
|
|
|
|
self.assertNotIn("symlink_sub", subdir_names)
|
|
self.assertEqual(image_names.count("cover.jpg"), 1)
|
|
|
|
def test_sidebar_subdirs_ignore_symlink_dirs(self):
|
|
if not self.symlink_supported:
|
|
self.skipTest("Symlinks not supported in this environment")
|
|
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
names = [subdir["name"] for subdir in response.context["subdirs"]]
|
|
self.assertNotIn("symlink_sub", names)
|
|
|
|
def test_thumbnail_failure_does_not_break_render(self):
|
|
with patch("viewer.common.make_thumbnail", side_effect=Exception("fail")):
|
|
response = self.client.get("/gallery/")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn("images", response.context)
|
|
self.assertIn("subdirs", response.context)
|
|
|
|
def test_login_required_for_gallery(self):
|
|
anon = Client()
|
|
response = anon.get("/gallery/")
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_image_view_still_renders_prev_next(self):
|
|
response = self.client.get("/gallery/beta.jpg/", follow=True)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.assertEqual(response.context["image_path"].name, "beta.jpg")
|
|
self.assertEqual(response.context["prev"], "alpha.jpg")
|
|
self.assertEqual(response.context["next"], "gamma.jpg")
|
|
# New context keys for image view should be present
|
|
self.assertIn("prev_url", response.context)
|
|
self.assertIn("next_url", response.context)
|
|
self.assertIsNone(response.context.get("prev_url")) if response.context.get(
|
|
"prev"
|
|
) is None else self.assertTrue(
|
|
"/gallery/alpha.jpg" in response.context.get("prev_url")
|
|
)
|
|
self.assertIsNone(response.context.get("next_url")) if response.context.get(
|
|
"next"
|
|
) is None else self.assertTrue(
|
|
"/gallery/gamma.jpg" in response.context.get("next_url")
|
|
)
|
|
|
|
# Thumbnails (if available) should be provided as /thumbs/... or be None
|
|
prev_thumb = response.context.get("prev_thumb")
|
|
next_thumb = response.context.get("next_thumb")
|
|
if prev_thumb is not None:
|
|
self.assertTrue(prev_thumb.startswith("/thumbs/"))
|
|
if next_thumb is not None:
|
|
self.assertTrue(next_thumb.startswith("/thumbs/"))
|
|
|
|
# Breadcrumbs should include filename as last label and it must be non-clickable (path None)
|
|
breadcrumbs = response.context.get("breadcrumbs")
|
|
self.assertIsNotNone(breadcrumbs)
|
|
self.assertEqual(breadcrumbs[-1]["label"], "beta.jpg")
|
|
self.assertIsNone(breadcrumbs[-1]["path"])
|
|
|
|
# Back and Home URLs should exist and preserve query state
|
|
back_url = response.context.get("back_url")
|
|
home_url = response.context.get("home_url")
|
|
self.assertIsNotNone(back_url)
|
|
self.assertIsNotNone(home_url)
|
|
# For images in root both should be the gallery root with query params
|
|
# normalize any './' segments to compare reliably
|
|
norm = lambda u: u.replace("/./", "/") if u is not None else u
|
|
self.assertEqual(norm(back_url), norm(home_url))
|
|
self.assertIn("/gallery/", norm(back_url))
|
|
# build_query no longer injects sort/theme into gallery URLs
|
|
self.assertNotIn("sort=", back_url)
|
|
self.assertNotIn("theme=", back_url)
|
|
|
|
# Back and Home thumbnails should be available and point to /thumbs/
|
|
back_thumb = response.context.get("back_thumb")
|
|
home_thumb = response.context.get("home_thumb")
|
|
self.assertIsNotNone(back_thumb)
|
|
self.assertIsNotNone(home_thumb)
|
|
self.assertTrue(back_thumb.startswith("/thumbs/"))
|
|
self.assertTrue(home_thumb.startswith("/thumbs/"))
|
|
self.assertEqual(back_thumb, home_thumb)
|
|
|
|
# Image metadata should be present and sensible
|
|
image_meta = response.context.get("image_meta")
|
|
self.assertIsNotNone(image_meta)
|
|
self.assertEqual(image_meta.get("filename"), "beta.jpg")
|
|
self.assertEqual(image_meta.get("width"), 300)
|
|
self.assertEqual(image_meta.get("height"), 200)
|
|
self.assertIsNotNone(image_meta.get("filesize"))
|
|
self.assertTrue(
|
|
"KB" in image_meta.get("filesize") or "MB" in image_meta.get("filesize")
|
|
)
|
|
self.assertIsNotNone(image_meta.get("created"))
|
|
self.assertIsNotNone(image_meta.get("modified"))
|
|
# New image metadata fields: visits (int), visited (formatted ts), favorite (bool)
|
|
self.assertIn("visits", image_meta)
|
|
self.assertIsInstance(image_meta.get("visits"), int)
|
|
self.assertGreaterEqual(image_meta.get("visits"), 1)
|
|
self.assertIn("visited", image_meta)
|
|
self.assertIsNotNone(image_meta.get("visited"))
|
|
self.assertIn("favorite", image_meta)
|
|
self.assertIsInstance(image_meta.get("favorite"), bool)
|
|
|
|
def test_sort_modes_recent_and_tnecer_use_mtime(self):
|
|
recent = self.client.get("/gallery/?sort=recent")
|
|
least_recent = self.client.get("/gallery/?sort=tnecer")
|
|
|
|
self.assertEqual(recent.status_code, 200)
|
|
self.assertEqual(least_recent.status_code, 200)
|
|
|
|
self.assertEqual(recent.context["images"][0]["name"], "match-root.jpg")
|
|
self.assertEqual(least_recent.context["images"][0]["name"], "alpha.jpg")
|
|
|
|
def test_sort_modes_old_and_new_use_creation_timestamp(self):
|
|
creation_values = {
|
|
"alpha.jpg": 30,
|
|
"beta.jpg": 10,
|
|
"gamma.jpg": 20,
|
|
"match-root.jpg": 40,
|
|
}
|
|
|
|
def fake_creation(path_obj):
|
|
return creation_values.get(path_obj.name, 0)
|
|
|
|
with patch("viewer.common.get_creation_timestamp", side_effect=fake_creation):
|
|
old = self.client.get("/gallery/?sort=old")
|
|
new = self.client.get("/gallery/?sort=new")
|
|
|
|
self.assertEqual(old.status_code, 200)
|
|
self.assertEqual(new.status_code, 200)
|
|
|
|
self.assertEqual(old.context["images"][0]["name"], "beta.jpg")
|
|
self.assertEqual(new.context["images"][0]["name"], "match-root.jpg")
|
|
|
|
|
|
class GalleryTemplateTests(GalleryBaseTests):
|
|
def test_template_includes_bootstrap_and_fontawesome(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
body = response.content.decode("utf-8")
|
|
self.assertIn("bootstrap@5", body)
|
|
self.assertIn("font-awesome/7.0.0", body)
|
|
|
|
def test_template_has_sidebar_controls_and_mobile_buttons(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
body = response.content.decode("utf-8")
|
|
self.assertIn("fa-circle-user", body)
|
|
self.assertIn("fa-bars", body)
|
|
self.assertIn("fa-xmark", body)
|
|
self.assertIn("fa-arrow-up-from-bracket", body)
|
|
self.assertIn("Favorites", body)
|
|
self.assertIn("Most visited", body)
|
|
self.assertIn("Recently visited", body)
|
|
|
|
def test_template_shows_fallback_icon_for_empty_subdir(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
body = response.content.decode("utf-8")
|
|
self.assertIn("fa-solid fa-image", body)
|
|
|
|
def test_search_placeholder_contrast_in_dark_theme(self):
|
|
response = self.client.get("/gallery/?theme=dark")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Styles are served via static files; assert the rules exist in the stylesheet
|
|
css_path = Path(__file__).resolve().parent / "static" / "css" / "styles.css"
|
|
css = css_path.read_text(encoding="utf-8")
|
|
self.assertIn(".search-input::placeholder", css)
|
|
self.assertIn("color: var(--muted);", css)
|
|
|
|
def test_sort_dropdown_active_option_underlined(self):
|
|
response = self.client.get("/gallery/?sort=recent")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
body = response.content.decode("utf-8")
|
|
self.assertIn("active-sort", body)
|
|
self.assertIn("Modification date most recent", body)
|
|
|
|
def test_template_has_requested_drop_shadows(self):
|
|
response = self.client.get("/gallery/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# The requested drop shadows should be defined in the stylesheet
|
|
css_path = Path(__file__).resolve().parent / "static" / "css" / "styles.css"
|
|
css = css_path.read_text(encoding="utf-8")
|
|
self.assertIn("box-shadow: 0 5px 20px", css)
|
|
|
|
|
|
class ImageModelTests(GalleryBaseTests):
|
|
def test_image_model_defaults_and_path_storage(self):
|
|
# Create a model entry and verify defaults
|
|
path_str = str(self.gallery_root / "alpha.jpg")
|
|
img = Im.objects.create(user=self.user, path=path_str)
|
|
|
|
self.assertEqual(img.visits, 0)
|
|
self.assertFalse(img.favorite)
|
|
self.assertIsNotNone(img.last_visited)
|
|
# FilePathField should store the provided path string
|
|
self.assertEqual(img.path, path_str)
|
|
|
|
def test_get_request_increments_visits_and_updates_last_visited(self):
|
|
path_url = "/gallery/alpha.jpg/"
|
|
before = timezone.now()
|
|
response = self.client.get(path_url, follow=True)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
img = Im.objects.get(user=self.user, path=str(self.gallery_root / "alpha.jpg"))
|
|
self.assertGreaterEqual(img.visits, 1)
|
|
self.assertIsNotNone(img.last_visited)
|
|
# last_visited should be updated to a recent timestamp
|
|
self.assertGreaterEqual(img.last_visited, before)
|
|
|
|
def test_post_toggles_favorite_flag(self):
|
|
path_url = "/gallery/alpha.jpg/"
|
|
# Ensure initial state
|
|
img, _ = Im.objects.get_or_create(
|
|
user=self.user, path=str(self.gallery_root / "alpha.jpg")
|
|
)
|
|
img.favorite = False
|
|
img.save()
|
|
|
|
response = self.client.post(path_url, follow=True)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
img.refresh_from_db()
|
|
self.assertTrue(img.favorite)
|
|
|
|
|
|
class UserSettingsModelTests(TestCase):
|
|
def test_usersettings_defaults(self):
|
|
user = User.objects.create_user("usertest", "u@example.com", "pw")
|
|
us = UserSettings.objects.create(user=user)
|
|
|
|
self.assertEqual(us.theme, "dark")
|
|
self.assertEqual(us.sort, "abc")
|
|
|
|
|
|
class UserSettingsMiddlewareTests(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user("mwuser", "mw@example.com", "pw")
|
|
self.factory = RequestFactory()
|
|
|
|
def test_middleware_creates_settings_and_sets_request(self):
|
|
request = self.factory.get("/")
|
|
request.user = self.user
|
|
|
|
def get_response(req):
|
|
return HttpResponse("ok")
|
|
|
|
mw = UserSettingsMiddleware(get_response)
|
|
response = mw(request)
|
|
|
|
# UserSettings should have been created and request attrs populated
|
|
self.assertTrue(UserSettings.objects.filter(user=self.user).exists())
|
|
self.assertEqual(
|
|
request.theme, UserSettings._meta.get_field("theme").get_default()
|
|
)
|
|
self.assertEqual(
|
|
request.sort, UserSettings._meta.get_field("sort").get_default()
|
|
)
|
|
|
|
def test_process_template_response_injects_and_preserves(self):
|
|
# Create a UserSettings with non-defaults
|
|
UserSettings.objects.create(user=self.user, theme="light", sort="cba")
|
|
|
|
request = self.factory.get("/")
|
|
request.user = self.user
|
|
|
|
def get_response(req):
|
|
# Provide a TemplateResponse that already sets `theme` to ensure
|
|
# the middleware does not overwrite existing keys.
|
|
return TemplateResponse(
|
|
req, "viewer/gallery_view.html", {"theme": "override-theme"}
|
|
)
|
|
|
|
mw = UserSettingsMiddleware(get_response)
|
|
response = mw(request)
|
|
|
|
# process_template_response should set missing keys but preserve existing ones
|
|
resp = mw.process_template_response(request, response)
|
|
self.assertIsInstance(resp, TemplateResponse)
|
|
self.assertEqual(resp.context_data.get("theme"), "override-theme")
|
|
self.assertEqual(resp.context_data.get("sort"), "cba")
|