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

1""" 

2Test our StatsView in ivatar.views 

3""" 

4 

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 

12 

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

14django.setup() 

15 

16 

17class StatsTester(TestCase): 

18 """ 

19 Test class for StatsView 

20 """ 

21 

22 client = Client() 

23 user = None 

24 username = random_string() 

25 password = random_string() 

26 

27 def login(self): 

28 """ 

29 Login as user 

30 """ 

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

32 

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 ) 

42 

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

58 

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 ) 

70 

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

79 

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

87 

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

95 

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" 

99 

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

105 

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

114 

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

122 

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 ) 

129 

130 UnconfirmedOpenId.objects.create( 

131 user=self.user, 

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

133 ip_address=random_ip_address(), 

134 ) 

135 

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) 

140 

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

150 

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 ) 

170 

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 ) 

179 

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 ) 

188 

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

197 

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 ) 

215 

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 ) 

223 

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 ) 

236 

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 ) 

246 

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) 

255 

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 ) 

286 

287 def test_stats_with_bluesky_handles(self): 

288 """ 

289 Test stats with Bluesky handles 

290 """ 

291 from ivatar.ivataraccount.models import ConfirmedEmail, ConfirmedOpenId 

292 

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

302 

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

312 

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

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

315 j = json.loads(response.content) 

316 

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 ) 

331 

332 def test_stats_photo_duplicates(self): 

333 """ 

334 Test potential duplicate photos detection 

335 """ 

336 from ivatar.ivataraccount.models import Photo 

337 

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" 

341 

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 ) 

357 

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

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

360 j = json.loads(response.content) 

361 

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 )