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

1# -*- coding: utf-8 -*- 

2""" 

3Test our StatsView in ivatar.views 

4""" 

5 

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 

13 

14os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" 

15django.setup() 

16 

17 

18class StatsTester(TestCase): 

19 """ 

20 Test class for StatsView 

21 """ 

22 

23 client = Client() 

24 user = None 

25 username = random_string() 

26 password = random_string() 

27 

28 def login(self): 

29 """ 

30 Login as user 

31 """ 

32 self.client.login(username=self.username, password=self.password) 

33 

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 ) 

43 

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

59 

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 ) 

71 

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

80 

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

88 

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

96 

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" 

100 

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

106 

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

115 

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

123 

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 ) 

130 

131 UnconfirmedOpenId.objects.create( 

132 user=self.user, 

133 openid=f"http://{random_string()}.{random_string()}.org/", 

134 ip_address=random_ip_address(), 

135 ) 

136 

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) 

141 

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

151 

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 ) 

171 

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 ) 

180 

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 ) 

189 

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

198 

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 ) 

216 

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 ) 

224 

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 ) 

237 

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 ) 

247 

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) 

256 

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 ) 

287 

288 def test_stats_with_bluesky_handles(self): 

289 """ 

290 Test stats with Bluesky handles 

291 """ 

292 from ivatar.ivataraccount.models import ConfirmedEmail, ConfirmedOpenId 

293 

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

303 

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

313 

314 response = self.client.get("/stats/") 

315 self.assertEqual(response.status_code, 200, "unable to fetch stats!") 

316 j = json.loads(response.content) 

317 

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 ) 

332 

333 def test_stats_photo_duplicates(self): 

334 """ 

335 Test potential duplicate photos detection 

336 """ 

337 from ivatar.ivataraccount.models import Photo 

338 

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" 

342 

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 ) 

358 

359 response = self.client.get("/stats/") 

360 self.assertEqual(response.status_code, 200, "unable to fetch stats!") 

361 j = json.loads(response.content) 

362 

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 )