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