Coverage for ivatar/test_webp_content_negotiation.py: 100%
81 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 05:10 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 05:10 +0000
1"""
2Tests for WebP Content Negotiation (Performance Roadmap Item 5).
3"""
5import io
6from PIL import Image
7from django.test import TestCase, Client, override_settings
8from django.contrib.auth.models import User
9from ivatar.ivataraccount.models import Photo, ConfirmedEmail
10from ivatar.photo_cache import (
11 get_cached_photo,
12 render_photo_thumbnail,
13 pregenerate_photo_thumbnails,
14 clear_local_photo_cache,
15)
16from ivatar.views import negotiate_image_format
19class WebPContentNegotiationTestCase(TestCase):
20 """
21 Test suite verifying RFC-compliant content negotiation for WebP format.
22 """
24 def setUp(self):
25 clear_local_photo_cache()
26 self.user = User.objects.create_user(
27 username="webpuser", email="webpuser@example.com", password="secretpassword"
28 )
29 # Create a 256x256 test PNG photo
30 img = Image.new("RGBA", (256, 256), (50, 100, 150, 255))
31 buf = io.BytesIO()
32 img.save(buf, format="PNG")
33 self.png_bytes = buf.getvalue()
35 self.photo = Photo.objects.create(
36 user=self.user,
37 data=self.png_bytes,
38 format="png",
39 ip_address="127.0.0.1",
40 )
41 self.email = ConfirmedEmail.objects.create(
42 user=self.user,
43 email="webpuser@example.com",
44 photo=self.photo,
45 ip_address="127.0.0.1",
46 )
47 self.client = Client()
49 def test_negotiate_image_format_helper(self):
50 """
51 Test negotiate_image_format logic with various Accept headers.
52 """
53 class MockRequest:
54 def __init__(self, accept_val):
55 self.headers = {"Accept": accept_val}
56 self.META = {"HTTP_ACCEPT": accept_val}
58 # Modern browser accepting webp
59 req_webp = MockRequest("text/html,application/xhtml+xml,image/webp,image/apng,*/*;q=0.8")
60 fmt, is_neg = negotiate_image_format(req_webp, "png")
61 self.assertEqual(fmt, "webp")
62 self.assertTrue(is_neg)
64 # Legacy client not accepting webp
65 req_legacy = MockRequest("image/png,image/jpeg,*/*")
66 fmt_legacy, is_neg_legacy = negotiate_image_format(req_legacy, "png")
67 self.assertEqual(fmt_legacy, "png")
68 self.assertFalse(is_neg_legacy)
70 # JPEG normalization
71 fmt_jpg, _ = negotiate_image_format(req_legacy, "jpg")
72 self.assertEqual(fmt_jpg, "jpeg")
74 def test_avatar_request_with_webp_accept_header(self):
75 """
76 When client sends Accept: image/webp, response is image/webp with Vary: Accept.
77 """
78 response = self.client.get(
79 f"/avatar/{self.email.digest}?s=80",
80 HTTP_ACCEPT="image/webp,image/png,image/*;q=0.8",
81 )
82 self.assertEqual(response.status_code, 200)
83 self.assertEqual(response["Content-Type"], "image/webp")
84 self.assertEqual(response["Vary"], "Accept")
85 self.assertEqual(response["Etag"], f'"{self.email.digest}"')
87 # Verify returned content is indeed valid WebP
88 img = Image.open(io.BytesIO(response.content))
89 self.assertEqual(img.format, "WEBP")
90 self.assertEqual(img.size, (80, 80))
92 def test_avatar_request_without_webp_accept_header(self):
93 """
94 When client does not send image/webp, original PNG format is preserved.
95 """
96 response = self.client.get(
97 f"/avatar/{self.email.digest}?s=80",
98 HTTP_ACCEPT="image/png,image/jpeg,*/*",
99 )
100 self.assertEqual(response.status_code, 200)
101 self.assertEqual(response["Content-Type"], "image/png")
102 self.assertEqual(response["Etag"], f'"{self.email.digest}"')
104 img = Image.open(io.BytesIO(response.content))
105 self.assertEqual(img.format, "PNG")
106 self.assertEqual(img.size, (80, 80))
108 def test_conditional_304_with_webp_etag(self):
109 """
110 Test that conditional requests with WebP ETag return 304 Not Modified.
111 """
112 etag = f'"{self.email.digest}"'
113 response = self.client.get(
114 f"/avatar/{self.email.digest}?s=80",
115 HTTP_ACCEPT="image/webp,*/*",
116 HTTP_IF_NONE_MATCH=etag,
117 )
118 self.assertEqual(response.status_code, 304)
119 self.assertEqual(response.content, b"")
120 self.assertEqual(response["Etag"], etag)
121 self.assertEqual(response["Vary"], "Accept")
123 def test_pregeneration_caches_both_native_and_webp(self):
124 """
125 Test that pregenerate_photo_thumbnails caches both PNG and WebP variants.
126 """
127 pregenerate_photo_thumbnails(self.photo, sizes=[48, 80])
129 # Verify PNG thumbnail is cached
130 cached_png = get_cached_photo(self.photo.id, 80, "png")
131 self.assertIsNotNone(cached_png)
132 img_png = Image.open(cached_png)
133 self.assertEqual(img_png.format, "PNG")
135 # Verify WebP thumbnail is also pre-cached
136 cached_webp = get_cached_photo(self.photo.id, 80, "webp")
137 self.assertIsNotNone(cached_webp)
138 img_webp = Image.open(cached_webp)
139 self.assertEqual(img_webp.format, "WEBP")
141 @override_settings(ENABLE_WEBP_CONTENT_NEGOTIATION=False)
142 def test_disabled_webp_content_negotiation(self):
143 """
144 When ENABLE_WEBP_CONTENT_NEGOTIATION is False, always serve original format.
145 """
146 response = self.client.get(
147 f"/avatar/{self.email.digest}?s=80",
148 HTTP_ACCEPT="image/webp,image/*",
149 )
150 self.assertEqual(response.status_code, 200)
151 self.assertEqual(response["Content-Type"], "image/png")
152 img = Image.open(io.BytesIO(response.content))
153 self.assertEqual(img.format, "PNG")
155 def test_render_photo_thumbnail_webp_conversion(self):
156 """
157 Test direct render_photo_thumbnail converting RGB/RGBA to WebP.
158 """
159 webp_bytes = render_photo_thumbnail(self.png_bytes, "png", 128, target_format="webp")
160 self.assertIsInstance(webp_bytes, bytes)
161 self.assertGreater(len(webp_bytes), 0)
163 img = Image.open(io.BytesIO(webp_bytes))
164 self.assertEqual(img.format, "WEBP")
165 self.assertEqual(img.size, (128, 128))