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