Coverage for ivatar/test_views_stats.py: 99%
124 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 11:51 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 11:51 +0000
1"""
2Test our StatsView in ivatar.views
3"""
5import base64
6import json
7import os
8import django
9from django.test import TestCase
10from django.test import Client
11from django.contrib.auth.models import User
12from ivatar.utils import random_string, random_ip_address
13from ivatar.ivataraccount.models import (
14 ConfirmedEmail,
15 ConfirmedOpenId,
16 Photo,
17 UnconfirmedEmail,
18 UnconfirmedOpenId,
19 PhotoAccessStat,
20 ConfirmedEmailAccessStat,
21 ConfirmedOpenIdAccessStat,
22)
24os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
25django.setup()
28class StatsTester(TestCase):
29 """
30 Test class for StatsView
31 """
33 client = Client()
34 user = None
35 username = random_string()
36 password = random_string()
38 def login(self):
39 """
40 Login as user
41 """
42 self.client.login(username=self.username, password=self.password)
44 def setUp(self):
45 """
46 Prepare for tests.
47 - Create user
48 """
49 self.user = User.objects.create_user(
50 username=self.username,
51 password=self.password,
52 )
54 def test_stats_basic(self):
55 """
56 Test basic stats functionality
57 """
58 response = self.client.get("/stats/", follow=True)
59 self.assertEqual(response.status_code, 200, "unable to fetch stats!")
60 j = json.loads(response.content)
61 self.assertEqual(j["users"], 1, "user count incorrect")
62 self.assertEqual(j["mails"], 0, "mails count incorrect")
63 self.assertEqual(j["openids"], 0, "openids count incorrect")
64 self.assertEqual(j["unconfirmed_mails"], 0, "unconfirmed mails count incorrect")
65 self.assertEqual(
66 j["unconfirmed_openids"], 0, "unconfirmed openids count incorrect"
67 )
68 self.assertEqual(j["avatars"], 0, "avatars count incorrect")
69 self.assertIn("cache_stats", j, "cache_stats missing from stats endpoint")
71 def test_stats_comprehensive(self):
72 """
73 Test comprehensive stats with actual data
74 """
75 from ivatar.ivataraccount.models import (
76 UnconfirmedEmail,
77 UnconfirmedOpenId,
78 )
80 # Create test data with random values
81 email1 = ConfirmedEmail.objects.create(
82 user=self.user,
83 email=f"{random_string()}@{random_string()}.{random_string(2)}",
84 ip_address=random_ip_address(),
85 )
86 ConfirmedEmailAccessStat.objects.update_or_create(
87 email=email1, defaults={"access_count": 100}
88 )
90 email2 = ConfirmedEmail.objects.create(
91 user=self.user,
92 email=f"{random_string()}@{random_string()}.{random_string(2)}",
93 ip_address=random_ip_address(),
94 )
95 ConfirmedEmailAccessStat.objects.update_or_create(
96 email=email2, defaults={"access_count": 50}
97 )
99 openid1 = ConfirmedOpenId.objects.create(
100 user=self.user,
101 openid=f"http://{random_string()}.{random_string()}.org/",
102 ip_address=random_ip_address(),
103 )
104 ConfirmedOpenIdAccessStat.objects.update_or_create(
105 openid=openid1, defaults={"access_count": 75}
106 )
108 # Create photos with valid image data (minimal PNG, Base64-encoded for pycco compatibility)
109 # PNG header + minimal data
110 png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAACklEQVQIHWNgYAAAAAMAAWveibQAAAAASUVORK5CYII="
111 png_data = base64.b64decode(png_base64)
113 photo1 = Photo.objects.create(
114 user=self.user, data=png_data, format="png", ip_address=random_ip_address()
115 )
116 PhotoAccessStat.objects.update_or_create(
117 photo=photo1, defaults={"access_count": 200}
118 )
120 photo2 = Photo.objects.create(
121 user=self.user,
122 data=png_data, # Same data for testing
123 format="png", # Same format for testing
124 ip_address=random_ip_address(),
125 )
126 PhotoAccessStat.objects.update_or_create(
127 photo=photo2, defaults={"access_count": 150}
128 )
130 # Associate photos with emails/openids
131 email1.photo = photo1
132 email1.save()
133 email2.photo = photo2
134 email2.save()
135 openid1.photo = photo1
136 openid1.save()
138 # Create unconfirmed entries
139 UnconfirmedEmail.objects.create(
140 user=self.user,
141 email=f"{random_string()}@{random_string()}.{random_string(2)}",
142 ip_address=random_ip_address(),
143 )
145 UnconfirmedOpenId.objects.create(
146 user=self.user,
147 openid=f"http://{random_string()}.{random_string()}.org/",
148 ip_address=random_ip_address(),
149 )
151 # Test the stats endpoint
152 response = self.client.get("/stats/")
153 self.assertEqual(response.status_code, 200, "unable to fetch stats!")
154 j = json.loads(response.content)
156 # Test basic counts
157 self.assertEqual(j["users"], 1, "user count incorrect")
158 self.assertEqual(j["mails"], 2, "mails count incorrect")
159 self.assertEqual(j["openids"], 1, "openids count incorrect")
160 self.assertEqual(j["unconfirmed_mails"], 1, "unconfirmed mails count incorrect")
161 self.assertEqual(
162 j["unconfirmed_openids"], 1, "unconfirmed openids count incorrect"
163 )
164 self.assertEqual(j["avatars"], 2, "avatars count incorrect")
166 # Test top viewed avatars
167 self.assertIn("top_viewed_avatars", j, "top_viewed_avatars missing")
168 self.assertEqual(
169 len(j["top_viewed_avatars"]), 2, "should have 2 top viewed avatars"
170 )
171 # The top viewed avatar should be the one with highest associated email/openid access count
172 self.assertEqual(
173 j["top_viewed_avatars"][0]["access_count"],
174 100,
175 "top avatar access count incorrect",
176 )
177 # Check that avatar_url is present and starts with the correct base URL
178 self.assertIn("avatar_url", j["top_viewed_avatars"][0], "avatar_url missing")
179 self.assertTrue(
180 j["top_viewed_avatars"][0]["avatar_url"].startswith(
181 "https://libravatar.org/avatar/"
182 ),
183 "avatar_url should start with https://libravatar.org/avatar/",
184 )
186 # Check that avatar_url is present and starts with the correct base URL
187 self.assertIn("avatar_url", j["top_queried_emails"][0], "avatar_url missing")
188 self.assertTrue(
189 j["top_queried_emails"][0]["avatar_url"].startswith(
190 "https://libravatar.org/avatar/"
191 ),
192 "avatar_url should start with https://libravatar.org/avatar/",
193 )
195 # Check that avatar_url is present and starts with the correct base URL
196 self.assertIn("avatar_url", j["top_queried_openids"][0], "avatar_url missing")
197 self.assertTrue(
198 j["top_queried_openids"][0]["avatar_url"].startswith(
199 "https://libravatar.org/avatar/"
200 ),
201 "avatar_url should start with https://libravatar.org/avatar/",
202 )
204 # Test photo format distribution
205 self.assertIn(
206 "photo_format_distribution", j, "photo_format_distribution missing"
207 )
208 formats = {
209 item["format"]: item["count"] for item in j["photo_format_distribution"]
210 }
211 self.assertEqual(formats["png"], 2, "png format count incorrect")
213 # Test user activity stats
214 self.assertIn("user_activity", j, "user_activity missing")
215 self.assertEqual(
216 j["user_activity"]["users_with_multiple_photos"],
217 1,
218 "users with multiple photos incorrect",
219 )
220 self.assertEqual(
221 j["user_activity"]["users_with_both_email_and_openid"],
222 1,
223 "users with both email and openid incorrect",
224 )
225 self.assertEqual(
226 j["user_activity"]["average_photos_per_user"],
227 2.0,
228 "average photos per user incorrect",
229 )
231 # Test Bluesky handles (should be empty)
232 self.assertIn("bluesky_handles", j, "bluesky_handles missing")
233 self.assertEqual(
234 j["bluesky_handles"]["total_bluesky_handles"],
235 0,
236 "total bluesky handles should be 0",
237 )
239 # Test photo size stats
240 self.assertIn("photo_size_stats", j, "photo_size_stats missing")
241 self.assertGreater(
242 j["photo_size_stats"]["average_size_bytes"],
243 0,
244 "average photo size should be > 0",
245 )
246 self.assertEqual(
247 j["photo_size_stats"]["total_photos_analyzed"],
248 2,
249 "total photos analyzed incorrect",
250 )
252 # Test potential duplicate photos
253 self.assertIn(
254 "potential_duplicate_photos", j, "potential_duplicate_photos missing"
255 )
256 self.assertEqual(
257 j["potential_duplicate_photos"]["potential_duplicate_groups"],
258 1,
259 "should have 1 duplicate group (same PNG data)",
260 )
262 def test_stats_edge_cases(self):
263 """
264 Test edge cases for stats
265 """
266 # Test with no data
267 response = self.client.get("/stats/")
268 self.assertEqual(response.status_code, 200, "unable to fetch stats!")
269 j = json.loads(response.content)
271 # All lists should be empty
272 self.assertEqual(
273 len(j["top_viewed_avatars"]), 0, "top_viewed_avatars should be empty"
274 )
275 self.assertEqual(
276 len(j["top_queried_emails"]), 0, "top_queried_emails should be empty"
277 )
278 self.assertEqual(
279 len(j["top_queried_openids"]), 0, "top_queried_openids should be empty"
280 )
281 self.assertEqual(
282 len(j["photo_format_distribution"]),
283 0,
284 "photo_format_distribution should be empty",
285 )
286 self.assertEqual(
287 j["bluesky_handles"]["total_bluesky_handles"],
288 0,
289 "bluesky_handles should be 0",
290 )
291 self.assertEqual(
292 j["photo_size_stats"]["total_photos_analyzed"],
293 0,
294 "photo_size_stats should be 0",
295 )
296 self.assertEqual(
297 j["potential_duplicate_photos"]["potential_duplicate_groups"],
298 0,
299 "potential_duplicate_photos should be 0",
300 )
302 def test_stats_with_bluesky_handles(self):
303 """
304 Test stats with Bluesky handles
305 """
306 from ivatar.ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
308 # Create email with Bluesky handle
309 email = ConfirmedEmail.objects.create(
310 user=self.user,
311 email=f"{random_string()}@{random_string()}.{random_string(2)}",
312 ip_address=random_ip_address(),
313 )
314 email.bluesky_handle = f"{random_string()}.bsky.social"
315 email.save()
316 ConfirmedEmailAccessStat.objects.update_or_create(
317 email=email, defaults={"access_count": 100}
318 )
320 # Create OpenID with Bluesky handle
321 openid = ConfirmedOpenId.objects.create(
322 user=self.user,
323 openid=f"http://{random_string()}.{random_string()}.org/",
324 ip_address=random_ip_address(),
325 )
326 openid.bluesky_handle = f"{random_string()}.bsky.social"
327 openid.save()
328 ConfirmedOpenIdAccessStat.objects.update_or_create(
329 openid=openid, defaults={"access_count": 50}
330 )
332 response = self.client.get("/stats/")
333 self.assertEqual(response.status_code, 200, "unable to fetch stats!")
334 j = json.loads(response.content)
336 # Test Bluesky handles stats
337 self.assertEqual(
338 j["bluesky_handles"]["total_bluesky_handles"],
339 2,
340 "total bluesky handles incorrect",
341 )
342 self.assertEqual(
343 j["bluesky_handles"]["bluesky_emails"], 1, "bluesky emails count incorrect"
344 )
345 self.assertEqual(
346 j["bluesky_handles"]["bluesky_openids"],
347 1,
348 "bluesky openids count incorrect",
349 )
351 def test_stats_photo_duplicates(self):
352 """
353 Test potential duplicate photos detection
354 """
355 from ivatar.ivataraccount.models import Photo
357 # Create photos with same format and size (potential duplicates)
358 # PNG header + minimal data
359 png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\nIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xdd\x8d\xb4\x1c\x00\x00\x00\x00IEND\xaeB`\x82"
361 Photo.objects.create(
362 user=self.user, data=png_data, format="png", ip_address=random_ip_address()
363 )
364 Photo.objects.create(
365 user=self.user,
366 data=png_data, # Same size
367 format="png", # Same format
368 ip_address=random_ip_address(),
369 )
370 Photo.objects.create(
371 user=self.user,
372 data=png_data, # Same size but different format
373 format="png", # Same format for testing
374 ip_address=random_ip_address(),
375 )
377 response = self.client.get("/stats/")
378 self.assertEqual(response.status_code, 200, "unable to fetch stats!")
379 j = json.loads(response.content)
381 # Should detect potential duplicates
382 self.assertEqual(
383 j["potential_duplicate_photos"]["potential_duplicate_groups"],
384 1,
385 "should have 1 duplicate group",
386 )
387 self.assertEqual(
388 j["potential_duplicate_photos"]["total_potential_duplicate_photos"],
389 3,
390 "should have 3 potential duplicate photos",
391 )
392 self.assertEqual(
393 len(j["potential_duplicate_photos"]["potential_duplicate_groups_detail"]),
394 1,
395 "should have 1 duplicate group detail",
396 )