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

1""" 

2Test our StatsView in ivatar.views 

3""" 

4 

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) 

23 

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

25django.setup() 

26 

27 

28class StatsTester(TestCase): 

29 """ 

30 Test class for StatsView 

31 """ 

32 

33 client = Client() 

34 user = None 

35 username = random_string() 

36 password = random_string() 

37 

38 def login(self): 

39 """ 

40 Login as user 

41 """ 

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

43 

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 ) 

53 

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

70 

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 ) 

79 

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 ) 

89 

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 ) 

98 

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 ) 

107 

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) 

112 

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 ) 

119 

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 ) 

129 

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

137 

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 ) 

144 

145 UnconfirmedOpenId.objects.create( 

146 user=self.user, 

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

148 ip_address=random_ip_address(), 

149 ) 

150 

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) 

155 

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

165 

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 ) 

185 

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 ) 

194 

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 ) 

203 

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

212 

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 ) 

230 

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 ) 

238 

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 ) 

251 

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 ) 

261 

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) 

270 

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 ) 

301 

302 def test_stats_with_bluesky_handles(self): 

303 """ 

304 Test stats with Bluesky handles 

305 """ 

306 from ivatar.ivataraccount.models import ConfirmedEmail, ConfirmedOpenId 

307 

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 ) 

319 

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 ) 

331 

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

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

334 j = json.loads(response.content) 

335 

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 ) 

350 

351 def test_stats_photo_duplicates(self): 

352 """ 

353 Test potential duplicate photos detection 

354 """ 

355 from ivatar.ivataraccount.models import Photo 

356 

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" 

360 

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 ) 

376 

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

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

379 j = json.loads(response.content) 

380 

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 )