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

1""" 

2Tests for WebP Content Negotiation (Performance Roadmap Item 5). 

3""" 

4 

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 

17 

18 

19class WebPContentNegotiationTestCase(TestCase): 

20 """ 

21 Test suite verifying RFC-compliant content negotiation for WebP format. 

22 """ 

23 

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() 

34 

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() 

48 

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} 

57 

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) 

63 

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) 

69 

70 # JPEG normalization 

71 fmt_jpg, _ = negotiate_image_format(req_legacy, "jpg") 

72 self.assertEqual(fmt_jpg, "jpeg") 

73 

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}"') 

86 

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)) 

91 

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}"') 

103 

104 img = Image.open(io.BytesIO(response.content)) 

105 self.assertEqual(img.format, "PNG") 

106 self.assertEqual(img.size, (80, 80)) 

107 

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") 

122 

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]) 

128 

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") 

134 

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") 

140 

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") 

154 

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) 

162 

163 img = Image.open(io.BytesIO(webp_bytes)) 

164 self.assertEqual(img.format, "WEBP") 

165 self.assertEqual(img.size, (128, 128))