Coverage for ivatar/ivataraccount/test_views.py: 100%

807 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-20 05:10 +0000

1""" 

2Test our views in ivatar.ivataraccount.views and ivatar.views 

3""" 

4 

5import contextlib 

6 

7# pylint: disable=too-many-lines 

8from urllib.parse import urlsplit 

9from io import BytesIO 

10import io 

11import os 

12import gzip 

13import xml.etree.ElementTree 

14import base64 

15import django 

16from django.test import TestCase 

17from django.test import Client 

18from django.test import override_settings 

19from django.urls import reverse 

20from django.core import mail 

21from django.contrib.auth.models import User 

22from django.contrib.auth import authenticate 

23import hashlib 

24 

25from libravatar import libravatar_url 

26 

27from PIL import Image 

28 

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

30django.setup() 

31 

32# pylint: disable=wrong-import-position 

33from ivatar import settings 

34from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT 

35from ivatar.ivataraccount.models import Photo, ConfirmedOpenId, ConfirmedEmail 

36from ivatar.utils import random_string 

37 

38# pylint: enable=wrong-import-position 

39 

40TEST_IMAGE_FILE = os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png") 

41 

42# Disable the default cache for this class via a scoped override. The previous 

43# in-place `settings.CACHES["default"] = DummyCache` leaked into every test 

44# module that ran afterwards (no cache), breaking tests that rely on it. 

45_DUMMY_CACHES = { 

46 **settings.CACHES, 

47 "default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}, 

48} 

49 

50 

51@override_settings(CACHES=_DUMMY_CACHES) 

52class Tester(TestCase): # pylint: disable=too-many-public-methods 

53 """ 

54 Main test class 

55 """ 

56 

57 client = Client() 

58 user = None 

59 username = random_string() 

60 password = random_string() 

61 email = "{}@{}.org".format(username, random_string()) 

62 # Dunno why random tld doesn't work, but I'm too lazy now to investigate 

63 openid = "http://{}.{}.{}/".format(username, random_string(), "org") 

64 first_name = random_string() 

65 last_name = random_string() 

66 

67 def login(self): 

68 """ 

69 Login as user 

70 """ 

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

72 

73 def setUp(self): 

74 """ 

75 Prepare for tests. 

76 - Create user 

77 """ 

78 self.user = User.objects.create_user( 

79 username=self.username, 

80 password=self.password, 

81 first_name=self.first_name, 

82 last_name=self.last_name, 

83 ) 

84 

85 def test_new_user(self): 

86 """ 

87 Create a new user 

88 """ 

89 response = self.client.get(reverse("new_account")) 

90 self.assertEqual(response.status_code, 200, "no 200 ok?") 

91 # Empty database / eliminate existing users 

92 User.objects.all().delete() 

93 url = reverse("new_account") 

94 response = self.client.post( 

95 url, 

96 { 

97 "username": self.username, 

98 "password1": self.password, 

99 "password2": self.password, 

100 }, 

101 follow=True, 

102 ) 

103 self.assertEqual(response.status_code, 200, "unable to create user?") 

104 self.assertEqual(response.context[0]["user"].username, self.username) 

105 

106 def test_new_user_twice(self): 

107 """ 

108 Try to create a user that already exists 

109 """ 

110 response = self.client.get(reverse("new_account")) 

111 self.assertEqual(response.status_code, 200, "no 200 ok?") 

112 # Due to setUp(), we already have this user! 

113 url = reverse("new_account") 

114 response = self.client.post( 

115 url, 

116 { 

117 "username": self.username, 

118 "password1": self.password, 

119 "password2": self.password, 

120 }, 

121 follow=True, 

122 ) 

123 self.assertEqual(response.status_code, 200, "unable to create user?") 

124 self.assertEqual(response.context[0]["user"].username, "") 

125 self.assertContains( 

126 response, 

127 "A user with that username already exists.", 

128 1, 

129 200, 

130 "can we create a user a second time???", 

131 ) 

132 

133 def test_set_password(self): 

134 """ 

135 Change the user password 

136 """ 

137 self.login() 

138 response = self.client.get(reverse("password_set")) 

139 self.assertEqual(response.status_code, 200, "no 200 ok?") 

140 self.password = random_string() 

141 response = self.client.post( 

142 reverse("password_set"), 

143 { 

144 "new_password1": self.password, 

145 "new_password2": self.password, 

146 }, 

147 follow=True, 

148 ) 

149 

150 self.assertEqual(response.status_code, 200, "cannot change password?") 

151 self.assertEqual( 

152 str(list(response.context[0]["messages"])[0]), 

153 "password changed successfully - please login again", 

154 "password change not successful?", 

155 ) 

156 

157 self.assertIsNotNone( 

158 authenticate( 

159 username=self.username, 

160 password=self.password, 

161 ), 

162 "cannot authenticate with new password!?", 

163 ) 

164 

165 self.login() 

166 response = self.client.get(reverse("profile")) 

167 self.assertEqual(response.context[0]["user"].is_anonymous, False) 

168 

169 def test_add_email(self): 

170 """ 

171 Add e-mail address 

172 """ 

173 self.login() 

174 response = self.client.get(reverse("add_email")) 

175 self.assertEqual(response.status_code, 200, "no 200 ok?") 

176 # Avoid sending out mails 

177 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

178 response = self.client.post( 

179 reverse("add_email"), 

180 { 

181 "email": self.email, 

182 }, 

183 follow=True, 

184 ) 

185 self.assertEqual(response.status_code, 200, "cannot add email?") 

186 self.assertEqual( 

187 len(response.context[0]["messages"]), 

188 1, 

189 "there must not be more or less than ONE (1) message", 

190 ) 

191 self.assertEqual( 

192 str(list(response.context[0]["messages"])[0]), 

193 "Address added successfully", 

194 "unable to add mail address?", 

195 ) 

196 

197 def test_confirm_email(self): 

198 """ 

199 Confirm unconfirmed email 

200 """ 

201 self.login() 

202 # Avoid sending out mails 

203 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

204 response = self.client.post( 

205 reverse("add_email"), 

206 { 

207 "email": self.email, 

208 }, 

209 follow=True, 

210 ) 

211 unconfirmed = self.user.unconfirmedemail_set.first() 

212 verification_key = unconfirmed.verification_key 

213 url = reverse("confirm_email", args=[verification_key]) 

214 response = self.client.get(url) 

215 self.assertEqual(response.status_code, 200, "unable to confirm mail address?") 

216 

217 self.assertEqual( 

218 self.user.unconfirmedemail_set.count(), 

219 0, 

220 "there must not be any unconfirmed address, after confirming it", 

221 ) 

222 self.assertEqual( 

223 self.user.confirmedemail_set.count(), 

224 1, 

225 "there must not be more or less than ONE (1) confirmed address!", 

226 ) 

227 

228 def test_confirm_email_w_invalid_auth_key(self): # pylint: disable=invalid-name 

229 """ 

230 Test confirmation with invalid auth key 

231 """ 

232 self.login() 

233 # Avoid sending out mails 

234 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

235 response = self.client.post( 

236 reverse("add_email"), 

237 { 

238 "email": self.email, 

239 }, 

240 follow=True, 

241 ) 

242 url = reverse("confirm_email", args=["x"]) 

243 response = self.client.get(url, follow=True) 

244 self.assertEqual( 

245 response.status_code, 

246 200, 

247 "Not able to request confirmation - without verification key?", 

248 ) 

249 self.assertEqual( 

250 str(list(response.context[0]["messages"])[-1]), 

251 "Verification key incorrect", 

252 "Confirm w/o verification key does not produce error message?", 

253 ) 

254 

255 def test_confirm_email_w_non_existing_auth_key( 

256 self, 

257 ): # pylint: disable=invalid-name 

258 """ 

259 Test confirmation with non existing auth key 

260 """ 

261 self.login() 

262 # Avoid sending out mails 

263 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

264 response = self.client.post( 

265 reverse("add_email"), 

266 { 

267 "email": self.email, 

268 }, 

269 follow=True, 

270 ) 

271 url = reverse("confirm_email", args=["x" * 64]) 

272 response = self.client.get(url, follow=True) 

273 self.assertEqual( 

274 response.status_code, 

275 200, 

276 "Not able to request confirmation - without verification key?", 

277 ) 

278 self.assertEqual( 

279 str(list(response.context[0]["messages"])[-1]), 

280 "Verification key does not exist", 

281 "Confirm w/o non existing key does not produce error message?", 

282 ) 

283 

284 def test_remove_confirmed_email(self): 

285 """ 

286 Remove confirmed email 

287 """ 

288 self.login() 

289 # Avoid sending out mails 

290 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

291 response = self.client.post( 

292 reverse("add_email"), 

293 { 

294 "email": self.email, 

295 }, 

296 ) # Create test address 

297 unconfirmed = self.user.unconfirmedemail_set.first() 

298 verification_key = unconfirmed.verification_key 

299 url = reverse("confirm_email", args=[verification_key]) 

300 self.client.get(url) # Confirm 

301 url = reverse( 

302 "remove_confirmed_email", args=[self.user.confirmedemail_set.first().id] 

303 ) 

304 response = self.client.post(url, follow=True) 

305 self.assertEqual( 

306 response.status_code, 200, "unable to remove confirmed address?" 

307 ) 

308 self.assertEqual( 

309 str(list(response.context[0]["messages"])[-1]), 

310 "Address removed", 

311 "Removing confirmed mail does not work?", 

312 ) 

313 

314 def test_remove_not_existing_confirmed_email(self): # pylint: disable=invalid-name 

315 """ 

316 Try removing confirmed mail that doesn't exist 

317 """ 

318 self.login() 

319 url = reverse("remove_confirmed_email", args=[1234]) 

320 response = self.client.post(url, follow=True) 

321 self.assertEqual( 

322 response.status_code, 200, "removing email does not redirect to profile?" 

323 ) 

324 self.assertEqual( 

325 str(list(response.context[0]["messages"])[0]), 

326 "Address does not exist", 

327 "Removing not existing (confirmed) address, should produce an\ 

328 error message!", 

329 ) 

330 

331 def test_remove_unconfirmed_email(self): 

332 """ 

333 Remove unconfirmed email 

334 """ 

335 self.login() 

336 # Avoid sending out mails 

337 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

338 response = self.client.post( 

339 reverse("add_email"), 

340 { 

341 "email": self.email, 

342 }, 

343 ) # Create test address 

344 url = reverse( 

345 "remove_unconfirmed_email", args=[self.user.unconfirmedemail_set.first().id] 

346 ) 

347 response = self.client.post(url, follow=True) 

348 self.assertEqual( 

349 response.status_code, 200, "unable to remove unconfirmed address?" 

350 ) 

351 # Take care, since we do not fetch any page now, the message we need 

352 # to check is the _second_ (aka [1], since first is [0]) 

353 self.assertEqual( 

354 str(list(response.context[0]["messages"])[1]), 

355 "Address removed", 

356 "Removing unconfirmed mail does not work?", 

357 ) 

358 

359 def test_gravatar_photo_import(self): 

360 """ 

361 import photo from Gravatar (with known mail address) 

362 """ 

363 self.login() 

364 # Avoid sending out mails 

365 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

366 response = self.client.post( 

367 reverse("add_email"), 

368 { 

369 "email": "oliver@linux-kernel.at", # Wow, static :-[ 

370 }, 

371 ) # Create test address 

372 unconfirmed = self.user.unconfirmedemail_set.first() 

373 verification_key = unconfirmed.verification_key 

374 url = reverse("confirm_email", args=[verification_key]) 

375 self.client.get(url) # Confirm 

376 

377 url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id]) 

378 response = self.client.post( 

379 url, 

380 { 

381 "photo_Gravatar": 1, 

382 }, 

383 follow=True, 

384 ) 

385 self.assertEqual( 

386 response.status_code, 200, "unable to import photo from Gravatar?" 

387 ) 

388 self.assertEqual( 

389 str(list(response.context[0]["messages"])[-1]), 

390 "Gravatar image successfully imported", 

391 "Importing gravatar photo did not work?", 

392 ) 

393 self.assertIsInstance( 

394 self.user.photo_set.first(), Photo, "why is there no Photo (instance)?" 

395 ) 

396 

397 def test_raw_image(self): 

398 """ 

399 test raw image view (as seen in profile <img src= 

400 """ 

401 

402 # Ensure we have a photo 

403 self.test_gravatar_photo_import() 

404 photo_id = self.user.photo_set.first().id 

405 response = self.client.get( 

406 reverse("raw_image", args=[photo_id]) 

407 ) 

408 self.assertEqual(response.status_code, 200, "cannot fetch photo?") 

409 # Probably not the best way to access the content type 

410 self.assertEqual(response["Content-Type"], "image/jpg", "Content type wrong!?") 

411 self.assertIn("ETag", response) 

412 self.assertIn("Cache-Control", response) 

413 self.assertIn("private", response["Cache-Control"]) 

414 

415 self.assertEqual( 

416 bytes(response.content), 

417 bytes(self.user.photo_set.first().data), 

418 "raw_image should return the same content as if we\ 

419 read it directly from the DB", 

420 ) 

421 

422 def test_raw_image_resized(self): 

423 """ 

424 Test raw image view with size query parameter returns resized thumbnail 

425 """ 

426 self.test_gravatar_photo_import() 

427 photo_id = self.user.photo_set.first().id 

428 

429 # Test with size parameter 

430 response = self.client.get( 

431 reverse("raw_image", args=[photo_id]) + "?size=100" 

432 ) 

433 self.assertEqual(response.status_code, 200) 

434 img = Image.open(BytesIO(response.content)) 

435 self.assertTrue(max(img.size) <= 100) 

436 self.assertIn("ETag", response) 

437 

438 # Test with s parameter 

439 response_s = self.client.get( 

440 reverse("raw_image", args=[photo_id]) + "?s=48" 

441 ) 

442 self.assertEqual(response_s.status_code, 200) 

443 img_s = Image.open(BytesIO(response_s.content)) 

444 self.assertTrue(max(img_s.size) <= 48) 

445 

446 def test_raw_image_conditional_request(self): 

447 """ 

448 Test raw image view handles If-None-Match (304 Not Modified) 

449 """ 

450 self.test_gravatar_photo_import() 

451 photo_id = self.user.photo_set.first().id 

452 

453 response = self.client.get( 

454 reverse("raw_image", args=[photo_id]) + "?size=120" 

455 ) 

456 self.assertEqual(response.status_code, 200) 

457 etag = response["ETag"] 

458 

459 # Conditional request with matching ETag 

460 cond_response = self.client.get( 

461 reverse("raw_image", args=[photo_id]) + "?size=120", 

462 HTTP_IF_NONE_MATCH=etag, 

463 ) 

464 self.assertEqual(cond_response.status_code, 304) 

465 self.assertEqual(cond_response["ETag"], etag) 

466 

467 def test_raw_image_invalid_size(self): 

468 """ 

469 Test raw image view handles invalid size parameter gracefully 

470 """ 

471 self.test_gravatar_photo_import() 

472 photo_id = self.user.photo_set.first().id 

473 

474 # Non-integer size falls back to full image 

475 response = self.client.get( 

476 reverse("raw_image", args=[photo_id]) + "?size=invalid" 

477 ) 

478 self.assertEqual(response.status_code, 200) 

479 self.assertEqual(bytes(response.content), bytes(self.user.photo_set.first().data)) 

480 

481 def test_delete_photo(self): 

482 """ 

483 test deleting the photo 

484 """ 

485 

486 # Ensure we have a photo 

487 self.test_gravatar_photo_import() 

488 

489 url = reverse("delete_photo", args=[self.user.photo_set.first().id]) 

490 response = self.client.get(url, follow=True) 

491 self.assertEqual(response.status_code, 200, "deleting photo does not work?") 

492 self.assertEqual( 

493 str(list(response.context[0]["messages"])[-1]), 

494 "Photo deleted successfully", 

495 "Photo deletion did not work?", 

496 ) 

497 

498 def test_delete_non_existing_photo(self): 

499 """ 

500 test deleting the photo 

501 """ 

502 

503 # Ensure we have a photo 

504 self.test_gravatar_photo_import() 

505 

506 url = reverse("delete_photo", args=[1234]) 

507 response = self.client.get(url, follow=True) 

508 self.assertEqual(response.status_code, 200, "post to delete does not work?") 

509 self.assertEqual( 

510 str(list(response.context[0]["messages"])[-1]), 

511 "No such image or no permission to delete it", 

512 "Deleting photo that does not exist, should return error message", 

513 ) 

514 

515 def test_too_many_unconfirmed_email(self): 

516 """ 

517 Request too many unconfirmed email addresses, make sure we 

518 cannot add more 

519 """ 

520 self.login() 

521 # Avoid sending out mails 

522 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

523 

524 max_num_unconfirmed = getattr( 

525 settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT 

526 ) 

527 

528 for i in range(max_num_unconfirmed + 1): 

529 response = self.client.post( # noqa: F841 

530 reverse("add_email"), 

531 { 

532 "email": "%i.%s" % (i, self.email), 

533 }, 

534 follow=True, 

535 ) # Create test addresses + 1 too much 

536 return self._check_form_validity( 

537 response, "Too many unconfirmed mail addresses!", "__all__" 

538 ) 

539 

540 def test_add_mail_address_twice(self): 

541 """ 

542 Request the same mail address two times, should not lead to 

543 having the same address twice 

544 """ 

545 self.login() 

546 # Avoid sending out mails 

547 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" 

548 

549 for _ in range(2): 

550 response = self.client.post( # noqa: F841 

551 reverse("add_email"), 

552 { 

553 "email": self.email, 

554 }, 

555 follow=True, 

556 ) 

557 return self._check_form_validity( 

558 response, "Address already added, currently unconfirmed", "email" 

559 ) 

560 

561 def test_add_already_confirmed_email_self(self): # pylint: disable=invalid-name 

562 """ 

563 Request adding mail address that is already confirmed (by someone) 

564 """ 

565 # Create test mail and confirm it, reuse test code 

566 # Should set EMAIL_BACKEND, so no need to do it here 

567 self.test_confirm_email() 

568 

569 response = self.client.post( # noqa: F841 

570 reverse("add_email"), 

571 { 

572 "email": self.email, 

573 }, 

574 follow=True, 

575 ) 

576 

577 return self._check_form_validity( 

578 response, "Address already confirmed (by you)", "email" 

579 ) 

580 

581 def test_add_already_confirmed_email_other(self): # pylint: disable=invalid-name 

582 """ 

583 Request adding mail address that is already confirmed (by someone) 

584 """ 

585 # Create test mail and confirm it, reuse test code 

586 # Should set EMAIL_BACKEND, so no need to do it here 

587 self.test_confirm_email() 

588 

589 # Create another user and assign the mail address to that one 

590 # in order to test the correct error message 

591 otheruser = User.objects.create(username="otheruser") 

592 confirmedemail = ConfirmedEmail.objects.last() 

593 confirmedemail.user = otheruser 

594 confirmedemail.save() 

595 

596 response = self.client.post( # noqa: F841 

597 reverse("add_email"), 

598 { 

599 "email": self.email, 

600 }, 

601 follow=True, 

602 ) 

603 

604 return self._check_form_validity( 

605 response, "Address already confirmed (by someone else)", "email" 

606 ) 

607 

608 def test_remove_unconfirmed_non_existing_email( 

609 self, 

610 ): # pylint: disable=invalid-name 

611 """ 

612 Remove unconfirmed email that doesn't exist 

613 """ 

614 self.login() 

615 url = reverse("remove_unconfirmed_email", args=[1234]) 

616 response = self.client.post(url, follow=True) 

617 self.assertEqual( 

618 response.status_code, 200, "unable to remove non existing address?" 

619 ) 

620 self.assertEqual( 

621 str(list(response.context[0]["messages"])[0]), 

622 "Address does not exist", 

623 "Removing address that does not\ 

624 exist, should return error message!", 

625 ) 

626 

627 def test_upload_image( 

628 self, test_only_one=True 

629 ): # pylint: disable=inconsistent-return-statements 

630 """ 

631 Test uploading image 

632 """ 

633 self.login() 

634 url = reverse("upload_photo") 

635 # rb => Read binary 

636 with open(TEST_IMAGE_FILE, "rb") as photo_file: 

637 photo_data = photo_file.read() 

638 

639 from django.core.files.uploadedfile import SimpleUploadedFile 

640 

641 uploaded_file = SimpleUploadedFile( 

642 "deadbeef.png", photo_data, content_type="image/png" 

643 ) 

644 response = self.client.post( 

645 url, 

646 { 

647 "photo": uploaded_file, 

648 "not_porn": True, 

649 "can_distribute": True, 

650 }, 

651 follow=True, 

652 ) 

653 if not test_only_one: 

654 return response 

655 self.assertEqual( 

656 self.user.photo_set.count(), 1, "there must be exactly one photo now!" 

657 ) 

658 self.assertEqual( 

659 str(list(response.context[0]["messages"])[-1]), 

660 "Successfully uploaded", 

661 "A valid image should return a success message!", 

662 ) 

663 self.assertEqual( 

664 self.user.photo_set.first().format, 

665 "png", 

666 "Format must be png, since we uploaded a png!", 

667 ) 

668 

669 def test_upload_too_many_images(self): 

670 """ 

671 Test uploading more images than we are allowed 

672 """ 

673 for _ in range(settings.MAX_NUM_PHOTOS + 1): 

674 response = self.test_upload_image(test_only_one=False) 

675 self.assertEqual( 

676 self.user.photo_set.count(), 

677 settings.MAX_NUM_PHOTOS, 

678 "there may not be more photos than allowed!", 

679 ) 

680 # Take care we need to check the last message 

681 self.assertEqual( 

682 str(list(response.context[0]["messages"])[-1]), 

683 "Maximum number of photos (%i) reached" % settings.MAX_NUM_PHOTOS, 

684 "Adding more than allowed images, should return error message!", 

685 ) 

686 

687 def test_upload_too_big_image(self): 

688 """ 

689 Test uploading image that is too big 

690 """ 

691 self.login() 

692 url = reverse("upload_photo") 

693 # rb => Read binary 

694 response = self.client.post( 

695 url, 

696 { 

697 "photo": io.StringIO("x" * (settings.MAX_PHOTO_SIZE + 1)), 

698 "not_porn": True, 

699 "can_distribute": True, 

700 }, 

701 follow=True, 

702 ) 

703 self.assertEqual( 

704 str(list(response.context[0]["messages"])[0]), 

705 "Image too big", 

706 "Uploading too big image, should return error message!", 

707 ) 

708 

709 def test_upload_invalid_image(self): 

710 """ 

711 Test invalid image data 

712 """ 

713 self.login() 

714 url = reverse("upload_photo") 

715 # rb => Read binary 

716 response = self.client.post( 

717 url, 

718 { 

719 "photo": io.StringIO("x"), 

720 "not_porn": True, 

721 "can_distribute": True, 

722 }, 

723 follow=True, 

724 ) 

725 self.assertEqual( 

726 str(list(response.context[0]["messages"])[0]), 

727 "Invalid Format", 

728 "Invalid img data should return error message!", 

729 ) 

730 

731 def test_upload_invalid_image_format(self): # pylint: disable=invalid-name 

732 """ 

733 Test if invalid format is correctly detected 

734 """ 

735 self.login() 

736 url = reverse("upload_photo") 

737 # rb => Read binary 

738 with open(os.path.join(settings.STATIC_ROOT, "img", "mm.svg"), "rb") as photo: 

739 response = self.client.post( 

740 url, 

741 { 

742 "photo": photo, 

743 "not_porn": True, 

744 "can_distribute": True, 

745 }, 

746 follow=True, 

747 ) 

748 self.assertEqual( 

749 str(list(response.context[0]["messages"])[0]), 

750 "Invalid Format", 

751 "Invalid img data should return error message!", 

752 ) 

753 

754 def test_upload_gif_image(self): 

755 """ 

756 Test if gif is correctly detected and can be viewed 

757 """ 

758 self._extracted_from_test_upload_webp_image_5( 

759 "broken.gif", 

760 "GIF upload failed?!", 

761 "gif", 

762 "Format must be gif, since we uploaded a GIF!", 

763 ) 

764 

765 def test_upload_jpg_image(self): 

766 """ 

767 Test if jpg is correctly detected and can be viewed 

768 """ 

769 self._extracted_from_test_upload_webp_image_5( 

770 "broken.jpg", 

771 "JPEG upload failed?!", 

772 "jpg", 

773 "Format must be jpeg, since we uploaded a jpeg!", 

774 ) 

775 

776 def test_upload_webp_image(self): 

777 """ 

778 Test if webp is correctly detected and can be viewed 

779 """ 

780 self._extracted_from_test_upload_webp_image_5( 

781 "broken.webp", 

782 "WEBP upload failed?!", 

783 "webp", 

784 "Format must be webp, since we uploaded a webp!", 

785 ) 

786 

787 def _extracted_from_test_upload_webp_image_5( 

788 self, filename, message1, format, message2 

789 ): 

790 """ 

791 Helper function for common checks for gif, jpg, webp 

792 """ 

793 self.login() 

794 url = reverse("upload_photo") 

795 with open(os.path.join(settings.STATIC_ROOT, "img", filename), "rb") as photo: 

796 response = self.client.post( 

797 url, 

798 {"photo": photo, "not_porn": True, "can_distribute": True}, 

799 follow=True, 

800 ) 

801 self.assertEqual( 

802 str(list(response.context[0]["messages"])[0]), 

803 "Successfully uploaded", 

804 message1, 

805 ) 

806 self.assertEqual(self.user.photo_set.first().format, format, message2) 

807 self.test_confirm_email() 

808 self.user.confirmedemail_set.first().photo = self.user.photo_set.first() 

809 urlobj = urlsplit( 

810 libravatar_url(email=self.user.confirmedemail_set.first().email) 

811 ) 

812 url = f"{urlobj.path}?{urlobj.query}" 

813 response = self.client.get(url, follow=True) 

814 self.assertEqual(response.status_code, 200, "unable to fetch avatar?") 

815 

816 def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name 

817 """ 

818 Test if unsupported format is correctly detected 

819 """ 

820 self.login() 

821 url = reverse("upload_photo") 

822 # rb => Read binary 

823 with open( 

824 os.path.join(settings.STATIC_ROOT, "img", "broken.tif"), "rb" 

825 ) as photo: 

826 response = self.client.post( 

827 url, 

828 { 

829 "photo": photo, 

830 "not_porn": True, 

831 "can_distribute": True, 

832 }, 

833 follow=True, 

834 ) 

835 self.assertEqual( 

836 str(list(response.context[0]["messages"])[0]), 

837 "Invalid Format", 

838 "Invalid img data should return error message!", 

839 ) 

840 

841 def test_automatic_photo_assign_to_confirmed_mail( 

842 self, 

843 ): # pylint: disable=invalid-name 

844 """ 

845 Test if automatic assignment of photo works 

846 """ 

847 self.test_upload_image() 

848 self.test_confirm_email() 

849 confirmed = self.user.confirmedemail_set.first() 

850 self.assertEqual(confirmed.photo, self.user.photo_set.first()) 

851 

852 def test_assign_photo_to_email(self): 

853 """ 

854 Test assigning photo to mail address 

855 """ 

856 self.test_confirm_email() 

857 self.test_upload_image() 

858 self.assertIsNone(self.user.confirmedemail_set.first().photo) 

859 url = reverse( 

860 "assign_photo_email", args=[self.user.confirmedemail_set.first().id] 

861 ) 

862 # The get is for the view - test context data 

863 self.client.get( 

864 url, 

865 { 

866 "photo_id": self.user.photo_set.first().id, 

867 }, 

868 ) 

869 # The post is for the actual assigning 

870 response = self.client.post( 

871 url, 

872 { 

873 "photo_id": self.user.photo_set.first().id, 

874 }, 

875 follow=True, 

876 ) 

877 self.assertEqual(response.status_code, 200, "cannot assign photo?") 

878 self.assertEqual( 

879 self.user.confirmedemail_set.first().photo, self.user.photo_set.first() 

880 ) 

881 

882 def test_no_photo_to_email(self): 

883 """ 

884 Test assigning photo to mail address 

885 """ 

886 self.test_confirm_email() 

887 url = reverse( 

888 "assign_photo_email", args=[self.user.confirmedemail_set.first().id] 

889 ) 

890 response = self.client.post( 

891 url, 

892 { 

893 "photoNone": True, 

894 }, 

895 follow=True, 

896 ) 

897 self.assertEqual(response.status_code, 200, "cannot un-assign photo?") 

898 self.assertEqual(self.user.confirmedemail_set.first().photo, None) 

899 

900 def test_assign_photo_to_email_wo_photo_for_testing_template( 

901 self, 

902 ): # pylint: disable=invalid-name 

903 """ 

904 Test assign photo template 

905 """ 

906 self.test_confirm_email() 

907 url = reverse( 

908 "assign_photo_email", args=[self.user.confirmedemail_set.first().id] 

909 ) 

910 # The get is for the view - test context data 

911 response = self.client.get(url) 

912 self.assertEqual(response.status_code, 200, "cannot fetch page?") 

913 

914 def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name 

915 """ 

916 Test if assigning an invalid photo id returns the correct error message 

917 """ 

918 self.test_confirm_email() 

919 self.test_upload_image() 

920 self.assertIsNone(self.user.confirmedemail_set.first().photo) 

921 url = reverse( 

922 "assign_photo_email", args=[self.user.confirmedemail_set.first().id] 

923 ) 

924 response = self.client.post( 

925 url, 

926 { 

927 "photo_id": 1234, 

928 }, 

929 follow=True, 

930 ) 

931 self.assertEqual(response.status_code, 200, "cannot post assign photo request?") 

932 self.assertEqual( 

933 str(list(response.context[0]["messages"])[-1]), 

934 "Photo does not exist", 

935 "Assign non existing photo, does not return error message?", 

936 ) 

937 

938 def test_post_to_assign_photo_without_photo_id( 

939 self, 

940 ): # pylint: disable=invalid-name 

941 """ 

942 Test if assigning photo without id returns the correct error message 

943 """ 

944 self.test_confirm_email() 

945 self.test_upload_image() 

946 self.assertIsNone(self.user.confirmedemail_set.first().photo) 

947 url = reverse( 

948 "assign_photo_email", args=[self.user.confirmedemail_set.first().id] 

949 ) 

950 response = self.client.post(url, {}, follow=True) 

951 self.assertEqual(response.status_code, 200, "cannot post assign photo request?") 

952 self.assertEqual( 

953 str(list(response.context[0]["messages"])[-1]), 

954 "Invalid request [photo_id] missing", 

955 "Assign non existing photo, does not return error message?", 

956 ) 

957 

958 def test_assign_photo_to_non_existing_mail(self): # pylint: disable=invalid-name 

959 """ 

960 Test if assigning photo to mail address that doesn't exist returns 

961 the correct error message 

962 """ 

963 self.test_upload_image() 

964 url = reverse("assign_photo_email", args=[1234]) 

965 response = self.client.post( 

966 url, 

967 { 

968 "photo_id": self.user.photo_set.first().id, 

969 }, 

970 follow=True, 

971 ) 

972 self.assertEqual(response.status_code, 200, "cannot post assign photo request?") 

973 self.assertEqual( 

974 str(list(response.context[0]["messages"])[-1]), 

975 "Invalid request", 

976 "Assign non existing photo, does not return error message?", 

977 ) 

978 

979 def test_import_photo_with_non_existing_email(self): # pylint: disable=invalid-name 

980 """ 

981 Test if import with non existing mail address returns 

982 the correct error message 

983 """ 

984 self.login() 

985 url = reverse("import_photo", args=[1234]) 

986 response = self.client.post(url, {}, follow=True) 

987 self.assertEqual(response.status_code, 200, "cannot post import photo request?") 

988 self.assertEqual( 

989 str(list(response.context[0]["messages"])[0]), 

990 "Address does not exist", 

991 "Import photo with non existing mail id,\ 

992 does not return error message?", 

993 ) 

994 

995 def test_import_nothing(self): 

996 """ 

997 Test if importing nothing causes the correct 

998 error message to be returned 

999 """ 

1000 self.test_confirm_email() 

1001 url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id]) 

1002 response = self.client.post(url, {}, follow=True) 

1003 self.assertEqual(response.status_code, 200, "cannot post import photo request?") 

1004 self.assertEqual( 

1005 str(list(response.context[0]["messages"])[-1]), 

1006 "Nothing importable", 

1007 "Importing with email that does not exist in Gravatar,\ 

1008 should return an error message!", 

1009 ) 

1010 

1011 def _manual_confirm(self): 

1012 """ 

1013 Helper method to confirm manually, because testing is really hard 

1014 """ 

1015 # Manual confirm, since testing is _really_ hard! 

1016 unconfirmed = self.user.unconfirmedopenid_set.first() 

1017 confirmed = ConfirmedOpenId() 

1018 confirmed.user = unconfirmed.user 

1019 confirmed.ip_address = "127.0.0.1" 

1020 confirmed.openid = unconfirmed.openid 

1021 confirmed.save() 

1022 unconfirmed.delete() 

1023 

1024 def test_add_openid(self, confirm=True): 

1025 """ 

1026 Test if adding an OpenID works 

1027 """ 

1028 

1029 self.login() 

1030 # Get page 

1031 response = self.client.get(reverse("add_openid")) 

1032 self.assertEqual( 

1033 response.status_code, 200, "Fetching page to add OpenID fails?" 

1034 ) 

1035 

1036 response = self.client.post( 

1037 reverse("add_openid"), 

1038 { 

1039 "openid": self.openid, 

1040 }, 

1041 ) 

1042 self.assertEqual(response.status_code, 302, "OpenID must redirect") 

1043 

1044 if confirm: 

1045 self._manual_confirm() 

1046 

1047 def test_add_openid_twice(self): 

1048 """ 

1049 Test if adding OpenID a second time works - it shouldn't 

1050 """ 

1051 self.login() 

1052 # Get page 

1053 response = self.client.get(reverse("add_openid")) 

1054 self.assertEqual( 

1055 response.status_code, 200, "Fetching page to add OpenID fails?" 

1056 ) 

1057 

1058 response = self.client.post( 

1059 reverse("add_openid"), 

1060 { 

1061 "openid": self.openid, 

1062 }, 

1063 ) 

1064 self.assertEqual(response.status_code, 302, "OpenID must redirect") 

1065 response = self.client.post( 

1066 reverse("add_openid"), 

1067 { 

1068 "openid": self.openid, 

1069 }, 

1070 follow=True, 

1071 ) 

1072 self.assertEqual( 

1073 self.user.unconfirmedopenid_set.count(), 

1074 1, 

1075 "There must only be one unconfirmed ID!", 

1076 ) 

1077 

1078 self._check_form_validity( 

1079 response, "OpenID already added, but not confirmed yet!", "openid" 

1080 ) 

1081 # Manual confirm, since testing is _really_ hard! 

1082 unconfirmed = self.user.unconfirmedopenid_set.first() 

1083 confirmed = ConfirmedOpenId() 

1084 confirmed.user = unconfirmed.user 

1085 confirmed.ip_address = "127.0.0.1" 

1086 confirmed.openid = unconfirmed.openid 

1087 confirmed.save() 

1088 unconfirmed.delete() 

1089 

1090 # Try adding it again - although already confirmed 

1091 response = self.client.post( 

1092 reverse("add_openid"), 

1093 { 

1094 "openid": self.openid, 

1095 }, 

1096 follow=True, 

1097 ) 

1098 

1099 return self._check_form_validity( 

1100 response, "OpenID already added and confirmed!", "openid" 

1101 ) 

1102 

1103 def _check_form_validity(self, response, message, field): 

1104 """ 

1105 Helper method to check form, used in several test functions, 

1106 deduplicating code 

1107 """ 

1108 

1109 self.assertTrue( 

1110 hasattr(response, "context"), "Response does not have a context" 

1111 ) 

1112 result = response.context.get("form") 

1113 self.assertIsNotNone(result, "No form found in response context") 

1114 self.assertFalse(result.is_valid(), "Form should not be valid") 

1115 self.assertIn(message, result.errors.get(field, [])) 

1116 return result 

1117 

1118 def test_assign_photo_to_openid(self): 

1119 """ 

1120 Test assignment of photo to openid 

1121 """ 

1122 self.test_add_openid() 

1123 self.test_upload_image() 

1124 self.assertIsNone(self.user.confirmedopenid_set.first().photo) 

1125 url = reverse( 

1126 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] 

1127 ) 

1128 # The get is for the view - test context data 

1129 self.client.get( 

1130 url, 

1131 { 

1132 "photo_id": self.user.photo_set.first().id, 

1133 }, 

1134 ) 

1135 # The post is for the actual assigning 

1136 response = self.client.post( 

1137 url, 

1138 { 

1139 "photo_id": self.user.photo_set.first().id, 

1140 }, 

1141 follow=True, 

1142 ) 

1143 self.assertEqual(response.status_code, 200, "cannot assign photo?") 

1144 self.assertEqual( 

1145 self.user.confirmedopenid_set.first().photo, self.user.photo_set.first() 

1146 ) 

1147 

1148 def test_assign_photo_to_openid_wo_photo_for_testing_template( 

1149 self, 

1150 ): # pylint: disable=invalid-name 

1151 """ 

1152 Test openid/photo assignment template 

1153 """ 

1154 self.test_add_openid() 

1155 url = reverse( 

1156 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] 

1157 ) 

1158 response = self.client.get(url) 

1159 self.assertEqual(response.status_code, 200, "cannot fetch page?") 

1160 

1161 def test_assign_invalid_photo_id_to_openid(self): # pylint: disable=invalid-name 

1162 """ 

1163 Test assigning invalid photo to openid returns 

1164 the correct error message 

1165 """ 

1166 self.test_add_openid() 

1167 self.assertIsNone(self.user.confirmedopenid_set.first().photo) 

1168 url = reverse( 

1169 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] 

1170 ) 

1171 response = self.client.post( 

1172 url, 

1173 { 

1174 "photo_id": 1234, 

1175 }, 

1176 follow=True, 

1177 ) 

1178 self.assertEqual(response.status_code, 200, "cannot post assign photo request?") 

1179 self.assertEqual( 

1180 str(list(response.context[0]["messages"])[-1]), 

1181 "Photo does not exist", 

1182 "Assign non existing photo, does not return error message?", 

1183 ) 

1184 

1185 def test_post_to_assign_photo_openid_without_photo_id( 

1186 self, 

1187 ): # pylint: disable=invalid-name 

1188 """ 

1189 Test POST assign photo to openid without photo id 

1190 returns the correct error message 

1191 """ 

1192 self.test_add_openid() 

1193 self.test_upload_image() 

1194 self.assertIsNone(self.user.confirmedopenid_set.first().photo) 

1195 url = reverse( 

1196 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] 

1197 ) 

1198 response = self.client.post(url, {}, follow=True) 

1199 self.assertEqual(response.status_code, 200, "cannot post assign photo request?") 

1200 self.assertEqual( 

1201 str(list(response.context[0]["messages"])[-1]), 

1202 "Invalid request [photo_id] missing", 

1203 "Assign non existing photo, does not return error message?", 

1204 ) 

1205 

1206 def test_assign_photo_to_openid_non_existing_openid( 

1207 self, 

1208 ): # pylint: disable=invalid-name 

1209 """ 

1210 Test assigning photo to openid that doesn't exist 

1211 returns the correct error message. 

1212 """ 

1213 self.test_upload_image() 

1214 url = reverse("assign_photo_openid", args=[1234]) 

1215 response = self.client.post( 

1216 url, 

1217 { 

1218 "photo_id": self.user.photo_set.first().id, 

1219 }, 

1220 follow=True, 

1221 ) 

1222 self.assertEqual(response.status_code, 200, "cannot post assign photo request?") 

1223 self.assertEqual( 

1224 str(list(response.context[0]["messages"])[-1]), 

1225 "Invalid request", 

1226 "Assign non existing photo, does not return error message?", 

1227 ) 

1228 

1229 def test_remove_confirmed_openid(self): # pylint: disable=invalid-name 

1230 """ 

1231 Remove confirmed openid 

1232 """ 

1233 self.test_add_openid() 

1234 url = reverse( 

1235 "remove_confirmed_openid", args=[self.user.confirmedopenid_set.first().id] 

1236 ) 

1237 response = self.client.post(url, follow=True) 

1238 self.assertEqual( 

1239 response.status_code, 200, "unable to remove confirmed openid?" 

1240 ) 

1241 self.assertEqual( 

1242 str(list(response.context[0]["messages"])[-1]), 

1243 "ID removed", 

1244 "Removing confirmed openid does not work?", 

1245 ) 

1246 

1247 def test_remove_not_existing_confirmed_openid(self): # pylint: disable=invalid-name 

1248 """ 

1249 Try removing confirmed openid that doesn't exist 

1250 """ 

1251 self.login() 

1252 url = reverse("remove_confirmed_openid", args=[1234]) 

1253 response = self.client.post(url, follow=True) 

1254 self.assertEqual( 

1255 response.status_code, 200, "removing id does not redirect to profile?" 

1256 ) 

1257 self.assertEqual( 

1258 str(list(response.context[0]["messages"])[0]), 

1259 "ID does not exist", 

1260 "Removing not existing (confirmed) address, should produce an\ 

1261 error message!", 

1262 ) 

1263 

1264 def test_remove_unconfirmed_openid(self): 

1265 """ 

1266 Remove unconfirmed openid 

1267 """ 

1268 self.test_add_openid(confirm=False) 

1269 url = reverse( 

1270 "remove_unconfirmed_openid", 

1271 args=[self.user.unconfirmedopenid_set.first().id], 

1272 ) 

1273 response = self.client.post(url, follow=True) 

1274 self.assertEqual( 

1275 response.status_code, 200, "unable to remove unconfirmed address?" 

1276 ) 

1277 self.assertEqual( 

1278 str(list(response.context[0]["messages"])[-1]), 

1279 "ID removed", 

1280 "Removing unconfirmed mail does not work?", 

1281 ) 

1282 

1283 def test_remove_unconfirmed_non_existing_openid( 

1284 self, 

1285 ): # pylint: disable=invalid-name 

1286 """ 

1287 Remove unconfirmed openid that doesn't exist 

1288 """ 

1289 self.login() 

1290 url = reverse("remove_unconfirmed_openid", args=[1234]) 

1291 response = self.client.post(url, follow=True) 

1292 self.assertEqual( 

1293 response.status_code, 200, "unable to remove unconfirmed address?" 

1294 ) 

1295 self.assertEqual( 

1296 str(list(response.context[0]["messages"])[0]), 

1297 "ID does not exist", 

1298 "Removing an non existing openid should return an error message", 

1299 ) 

1300 

1301 def test_openid_redirect_view(self): 

1302 """ 

1303 Test redirect view 

1304 """ 

1305 self.test_add_openid(confirm=False) 

1306 url = reverse( 

1307 "openid_redirection", args=[self.user.unconfirmedopenid_set.first().id] 

1308 ) 

1309 response = self.client.get(url, follow=True) 

1310 self.assertEqual( 

1311 response.status_code, 200, "unable to remove unconfirmed address?" 

1312 ) 

1313 # self.assertContains( 

1314 # response, 

1315 # 'OpenID discovery failed: ', 1, 200, 

1316 # 'This request must return an error in test mode' 

1317 # ) 

1318 

1319 def test_set_photo_on_openid(self): 

1320 """ 

1321 Test the set_photo function on our ConfirmedOpenId model. 

1322 """ 

1323 self.test_add_openid() 

1324 self.test_upload_image() 

1325 self.assertIsNone(self.user.confirmedopenid_set.first().photo) 

1326 self.user.confirmedopenid_set.first().set_photo(self.user.photo_set.first()) 

1327 self.assertEqual( 

1328 self.user.confirmedopenid_set.first().photo, 

1329 self.user.photo_set.first(), 

1330 "set_photo did not work!?", 

1331 ) 

1332 

1333 def test_avatar_url_mail(self, do_upload_and_confirm=True, size=(80, 80)): 

1334 """ 

1335 Test fetching avatar via mail 

1336 """ 

1337 if do_upload_and_confirm: 

1338 self.test_upload_image() 

1339 self.test_confirm_email() 

1340 urlobj = urlsplit( 

1341 libravatar_url( 

1342 email=self.user.confirmedemail_set.first().email, 

1343 size=size[0], 

1344 ) 

1345 ) 

1346 url = f"{urlobj.path}?{urlobj.query}" 

1347 response = self.client.get(url, follow=True) 

1348 self.assertEqual(response.status_code, 200, "unable to fetch avatar?") 

1349 photodata = Image.open(BytesIO(response.content)) 

1350 self.assertEqual(photodata.size, size, "Why is this not the correct size?") 

1351 

1352 def test_avatar_url_openid(self): 

1353 """ 

1354 Test fetching avatar via openid 

1355 """ 

1356 self.test_assign_photo_to_openid() 

1357 urlobj = urlsplit( 

1358 libravatar_url( 

1359 openid=self.user.confirmedopenid_set.first().openid, 

1360 size=80, 

1361 ) 

1362 ) 

1363 url = f"{urlobj.path}?{urlobj.query}" 

1364 response = self.client.get(url, follow=True) 

1365 self.assertEqual(response.status_code, 200, "unable to fetch avatar?") 

1366 photodata = Image.open(BytesIO(response.content)) 

1367 self.assertEqual(photodata.size, (80, 80), "Why is this not the correct size?") 

1368 

1369 def test_avatar_url_non_existing_mail_digest(self): # pylint: disable=invalid-name 

1370 """ 

1371 Test fetching avatar via non existing mail digest 

1372 """ 

1373 self.test_upload_image() 

1374 self.test_confirm_email() 

1375 urlobj = urlsplit( 

1376 libravatar_url( 

1377 email=self.user.confirmedemail_set.first().email, 

1378 size=80, 

1379 ) 

1380 ) 

1381 # Simply delete it, then it's digest is 'correct', but 

1382 # the hash is no longer there 

1383 addr = self.user.confirmedemail_set.first().email 

1384 hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest() 

1385 

1386 self.user.confirmedemail_set.first().delete() 

1387 url = f"{urlobj.path}?{urlobj.query}" 

1388 self.client.get(url, follow=True) 

1389 # TODO: All these tests still fails under some circumstances - it needs further investigation 

1390 # self.assertEqual( 

1391 # response.redirect_chain[0][0], 

1392 # f"/gravatarproxy/{digest}?s=80", 

1393 # "Doesn't redirect to Gravatar?", 

1394 # ) 

1395 # self.assertEqual( 

1396 # response.redirect_chain[0][1], 302, "Doesn't redirect with 302?" 

1397 # ) 

1398 # self.assertEqual( 

1399 # response.redirect_chain[1][0], 

1400 # f"/avatar/{digest}?s=80&forcedefault=y", 

1401 # "Doesn't redirect with default forced on?", 

1402 # ) 

1403 # self.assertEqual( 

1404 # response.redirect_chain[1][1], 302, "Doesn't redirect with 302?" 

1405 # ) 

1406 # self.assertEqual( 

1407 # response.redirect_chain[2][0], 

1408 # "/static/img/nobody/80.png", 

1409 # "Doesn't redirect to static?", 

1410 # ) 

1411 # self.assertRedirects( 

1412 # response=response, 

1413 # expected_url="/static/img/nobody/80.png", 

1414 # msg_prefix="Why does this not redirect to Gravatar?", 

1415 # ) 

1416 # Eventually one should check if the data is the same 

1417 

1418 def test_avatar_url_non_existing_mail_digest_gravatarproxy_disabled( 

1419 self, 

1420 ): # pylint: disable=invalid-name 

1421 """ 

1422 Test fetching avatar via non existing mail digest 

1423 """ 

1424 self.test_upload_image() 

1425 self.test_confirm_email() 

1426 urlobj = urlsplit( 

1427 libravatar_url( 

1428 email=self.user.confirmedemail_set.first().email, 

1429 size=80, 

1430 ) 

1431 ) 

1432 # Simply delete it, then it digest is 'correct', but 

1433 # the hash is no longer there 

1434 self.user.confirmedemail_set.first().delete() 

1435 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n" 

1436 response = self.client.get(url, follow=True) 

1437 self.assertEqual( 

1438 response.redirect_chain[0][0], 

1439 "/static/img/nobody/80.png", 

1440 "Doesn't redirect to static?", 

1441 ) 

1442 

1443 # self.assertRedirects( 

1444 # response=response, 

1445 # expected_url="/static/img/nobody/80.png", 

1446 # msg_prefix="Why does this not redirect to the default img?", 

1447 # ) 

1448 # Eventually one should check if the data is the same 

1449 

1450 def test_avatar_url_non_existing_mail_digest_w_default_mm( 

1451 self, 

1452 ): # pylint: disable=invalid-name 

1453 """ 

1454 Test fetching avatar via non existing mail digest and default 'mm' 

1455 """ 

1456 urlobj = urlsplit( 

1457 libravatar_url( 

1458 email="asdf@company.local", 

1459 size=80, 

1460 default="mm", 

1461 ) 

1462 ) 

1463 url = f"{urlobj.path}?{urlobj.query}" 

1464 self.client.get(url, follow=False) 

1465 

1466 def test_avatar_url_non_existing_mail_digest_w_default_mm_gravatarproxy_disabled( 

1467 self, 

1468 ): # pylint: disable=invalid-name 

1469 """ 

1470 Test fetching avatar via non existing mail digest and default 'mm' 

1471 """ 

1472 urlobj = urlsplit( 

1473 libravatar_url( 

1474 email="asdf@company.local", 

1475 size=80, 

1476 default="mm", 

1477 ) 

1478 ) 

1479 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n" 

1480 response = self.client.get(url, follow=True) 

1481 self.assertEqual( 

1482 response.redirect_chain[0][0], 

1483 "/static/img/mm/80.png", 

1484 "Doesn't redirect to static?", 

1485 ) 

1486 

1487 # self.assertRedirects( 

1488 # response=response, 

1489 # expected_url="/static/img/mm/80.png", 

1490 # msg_prefix="Why does this not redirect to the default img?", 

1491 # ) 

1492 # Eventually one should check if the data is the same 

1493 

1494 def test_avatar_url_non_existing_mail_digest_wo_default( 

1495 self, 

1496 ): # pylint: disable=invalid-name 

1497 """ 

1498 Test fetching avatar via non existing mail digest and default 'mm' 

1499 """ 

1500 urlobj = urlsplit( 

1501 libravatar_url( 

1502 email="asdf@company.local", 

1503 size=80, 

1504 ) 

1505 ) 

1506 digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest() 

1507 url = f"{urlobj.path}?{urlobj.query}" 

1508 response = self.client.get(url, follow=True) 

1509 self.assertEqual( 

1510 response.redirect_chain[0][0], 

1511 f"/gravatarproxy/{digest}?s=80", 

1512 "Doesn't redirect to Gravatar?", 

1513 ) 

1514 self.assertEqual( 

1515 response.redirect_chain[0][1], 302, "Doesn't redirect with 302?" 

1516 ) 

1517 self.assertEqual( 

1518 response.redirect_chain[1][0], 

1519 f"/avatar/{digest}?s=80&forcedefault=y", 

1520 "Doesn't redirect with default forced on?", 

1521 ) 

1522 self.assertEqual( 

1523 response.redirect_chain[1][1], 302, "Doesn't redirect with 302?" 

1524 ) 

1525 self.assertEqual( 

1526 response.redirect_chain[2][0], 

1527 "/static/img/nobody/80.png", 

1528 "Doesn't redirect to static?", 

1529 ) 

1530 

1531 # self.assertRedirects( 

1532 # response=response, 

1533 # expected_url="/static/img/nobody/80.png", 

1534 # msg_prefix="Why does this not redirect to the default img?", 

1535 # ) 

1536 # Eventually one should check if the data is the same 

1537 

1538 def test_avatar_url_non_existing_mail_digest_wo_default_gravatarproxy_disabled( 

1539 self, 

1540 ): # pylint: disable=invalid-name 

1541 """ 

1542 Test fetching avatar via non existing mail digest and default 'mm' 

1543 """ 

1544 urlobj = urlsplit( 

1545 libravatar_url( 

1546 email="asdf@company.local", 

1547 size=80, 

1548 ) 

1549 ) 

1550 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n" 

1551 response = self.client.get(url, follow=True) 

1552 self.assertEqual( 

1553 response.redirect_chain[0][0], 

1554 "/static/img/nobody/80.png", 

1555 "Doesn't redirect to static?", 

1556 ) 

1557 

1558 # self.assertRedirects( 

1559 # response=response, 

1560 # expected_url="/static/img/nobody/80.png", 

1561 # msg_prefix="Why does this not redirect to the default img?", 

1562 # ) 

1563 # Eventually one should check if the data is the same 

1564 

1565 def test_avatar_url_default(self): # pylint: disable=invalid-name 

1566 """ 

1567 Test fetching avatar for not existing mail with default specified 

1568 """ 

1569 urlobj = urlsplit( 

1570 libravatar_url( 

1571 "xxx@xxx.xxx", 

1572 size=80, 

1573 default="/static/img/nobody.png", 

1574 ) 

1575 ) 

1576 url = f"{urlobj.path}?{urlobj.query}" 

1577 url += "&gravatarproxy=n" 

1578 response = self.client.get(url, follow=False) 

1579 self.assertEqual(response.status_code, 302, "Doesn't redirect with 302?") 

1580 self.assertEqual( 

1581 response["Location"], 

1582 "/static/img/nobody.png", 

1583 "Doesn't redirect to static img?", 

1584 ) 

1585 

1586 def test_avatar_url_default_gravatarproxy_disabled( 

1587 self, 

1588 ): # pylint: disable=invalid-name 

1589 """ 

1590 Test fetching avatar for not existing mail with default specified 

1591 """ 

1592 urlobj = urlsplit( 

1593 libravatar_url( 

1594 "xxx@xxx.xxx", 

1595 size=80, 

1596 default="/static/img/nobody.png", 

1597 ) 

1598 ) 

1599 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n" 

1600 response = self.client.get(url, follow=True) 

1601 self.assertEqual( 

1602 response.redirect_chain[0][0], 

1603 "/static/img/nobody.png", 

1604 "Doesn't redirect to static?", 

1605 ) 

1606 

1607 def test_avatar_url_default_external(self): # pylint: disable=invalid-name 

1608 """ 

1609 Test fetching avatar for not existing mail with external default specified 

1610 This shall *not* redirect to the external site (CWE-601)! 

1611 """ 

1612 default = "http://host.tld/img.png" 

1613 size = 80 

1614 urlobj = urlsplit( 

1615 libravatar_url( 

1616 "xxx@xxx.xxx", 

1617 size=size, 

1618 default=default, 

1619 ) 

1620 ) 

1621 url = f"{urlobj.path}?{urlobj.query}" 

1622 response = self.client.get(url, follow=False) 

1623 self.assertRedirects( 

1624 response=response, 

1625 expected_url=f"/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s={size}", 

1626 fetch_redirect_response=False, 

1627 msg_prefix="Why does this not redirect to the default img?", 

1628 ) 

1629 

1630 def test_avatar_url_default_external_trusted(self): # pylint: disable=invalid-name 

1631 """ 

1632 Test fetching avatar for not existing mail with external default specified 

1633 """ 

1634 default = "https://ui-avatars.com/api/blah" 

1635 urlobj = urlsplit( 

1636 libravatar_url( 

1637 "xxx@xxx.xxx", 

1638 size=80, 

1639 default=default, 

1640 ) 

1641 ) 

1642 url = f"{urlobj.path}?{urlobj.query}" 

1643 response = self.client.get(url, follow=False) 

1644 self.assertRedirects( 

1645 response=response, 

1646 expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=https://ui-avatars.com/api/blah", 

1647 fetch_redirect_response=False, 

1648 msg_prefix="Why does this not redirect to the default img?", 

1649 ) 

1650 

1651 def test_avatar_url_default_external_gravatarproxy_disabled( 

1652 self, 

1653 ): # pylint: disable=invalid-name 

1654 """ 

1655 Test fetching avatar for not existing mail with external default specified 

1656 This shall *not* redirect to the external site (CWE-601)! 

1657 """ 

1658 default = "http://host.tld/img.png" 

1659 urlobj = urlsplit( 

1660 libravatar_url( 

1661 "xxx@xxx.xxx", 

1662 size=80, 

1663 default=default, 

1664 ) 

1665 ) 

1666 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n" 

1667 response = self.client.get(url, follow=False) 

1668 self.assertRedirects( 

1669 response=response, 

1670 expected_url="/static/img/nobody/80.png", 

1671 fetch_redirect_response=False, 

1672 msg_prefix="Why does this not redirect to the default img?", 

1673 ) 

1674 

1675 def test_crop_photo(self): 

1676 """ 

1677 Test cropping photo 

1678 """ 

1679 self.test_upload_image() 

1680 self.test_confirm_email() 

1681 url = reverse("crop_photo", args=[self.user.photo_set.first().pk]) 

1682 response = self.client.post( 

1683 url, 

1684 { 

1685 "x": 10, 

1686 "y": 10, 

1687 "w": 20, 

1688 "h": 20, 

1689 }, 

1690 follow=True, 

1691 ) 

1692 self.assertEqual(response.status_code, 200, "unable to crop?") 

1693 self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20)) 

1694 img = Image.open(BytesIO(self.user.photo_set.first().data)) 

1695 self.assertEqual( 

1696 img.size, (20, 20), "cropped to 20x20, but resulting image isn't 20x20!?" 

1697 ) 

1698 

1699 def test_password_change_view(self): 

1700 """ 

1701 Test password change view 

1702 """ 

1703 self.login() 

1704 url = reverse("password_change") 

1705 response = self.client.get(url) 

1706 self.assertEqual( 

1707 response.status_code, 200, "unable to view password change view?" 

1708 ) 

1709 

1710 def test_password_change_view_post_wrong_old_pw(self): 

1711 """ 

1712 Test password change view post 

1713 """ 

1714 self.login() 

1715 response = self.client.post( 

1716 reverse("password_change"), 

1717 { 

1718 "old_password": "xxx", 

1719 "new_password1": self.password, 

1720 "new_password2": self.password, 

1721 }, 

1722 follow=True, 

1723 ) 

1724 

1725 self.assertContains( 

1726 response, 

1727 "Your old password was entered incorrectly. Please enter it again.", 

1728 1, 

1729 200, 

1730 "Old password as entered incorrectly, site should raise an error", 

1731 ) 

1732 

1733 def test_password_change_view_post_wrong_new_password1(self): 

1734 """ 

1735 Test password change view post 

1736 """ 

1737 self.login() 

1738 response = self.client.post( 

1739 reverse("password_change"), 

1740 { 

1741 "old_password": self.password, 

1742 "new_password1": f"{self.password}.", 

1743 "new_password2": self.password, 

1744 }, 

1745 follow=True, 

1746 ) 

1747 self.assertContains( 

1748 response, 

1749 "The two password fields did", 

1750 1, 

1751 200, 

1752 "Old password was entered incorrectly, site should raise an error", 

1753 ) 

1754 

1755 def test_password_change_view_post_wrong_new_password2(self): 

1756 """ 

1757 Test password change view post 

1758 """ 

1759 self.login() 

1760 response = self.client.post( 

1761 reverse("password_change"), 

1762 { 

1763 "old_password": self.password, 

1764 "new_password1": self.password, 

1765 "new_password2": f"{self.password}.", 

1766 }, 

1767 follow=True, 

1768 ) 

1769 

1770 self.assertContains( 

1771 response, 

1772 "The two password fields did", 

1773 1, 

1774 200, 

1775 "Old password as entered incorrectly, site should raise an error", 

1776 ) 

1777 

1778 def test_password_change_view_post_common_password(self): 

1779 """ 

1780 Test password change view post 

1781 """ 

1782 self.login() 

1783 response = self.client.post( 

1784 reverse("password_change"), 

1785 { 

1786 "old_password": self.password, 

1787 "new_password1": "Hallo", 

1788 "new_password2": "Hallo", 

1789 }, 

1790 follow=True, 

1791 ) 

1792 

1793 self.assertContains( 

1794 response, 

1795 "This password is too common.", 

1796 1, 

1797 200, 

1798 "Common password, site should raise an error", 

1799 ) 

1800 

1801 def test_profile_must_list_first_and_lastname(self): 

1802 """ 

1803 Test if profile view correctly lists first -/last name 

1804 """ 

1805 self.login() 

1806 response = self.client.get(reverse("profile")) 

1807 self.assertContains( 

1808 response, 

1809 self.first_name, 

1810 1, 

1811 200, 

1812 "First name not listed in profile page", 

1813 ) 

1814 self.assertContains( 

1815 response, 

1816 self.last_name, 

1817 1, 

1818 200, 

1819 "Last name not listed in profile page", 

1820 ) 

1821 self.assertContains( 

1822 response, 

1823 f"{self.first_name} {self.last_name}", 

1824 1, 

1825 200, 

1826 "First and last name not correctly listed in profile page", 

1827 ) 

1828 

1829 def test_password_reset_page(self): 

1830 """ 

1831 Just test if the password reset page come up correctly 

1832 """ 

1833 response = self.client.get(reverse("password_reset")) 

1834 self.assertEqual(response.status_code, 200, "no 200 ok?") 

1835 

1836 def test_password_reset_wo_mail(self): 

1837 """ 

1838 Test if the password reset doesn't error out 

1839 if the mail address doesn't exist 

1840 """ 

1841 # Avoid sending out mails 

1842 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 

1843 

1844 # Empty database / eliminate existing users 

1845 User.objects.all().delete() 

1846 url = reverse("password_reset") 

1847 response = self.client.post( 

1848 url, 

1849 { 

1850 "email": "asdf@asdf.local", 

1851 }, 

1852 follow=True, 

1853 ) 

1854 self.assertEqual(response.status_code, 200, "password reset page not working?") 

1855 self.assertEqual( 

1856 len(mail.outbox), 

1857 0, 

1858 "user does not exist, there should be no mail in the outbox!", 

1859 ) 

1860 

1861 def test_password_reset_w_mail(self): 

1862 """ 

1863 Test if the password reset works correctly with email in 

1864 User object 

1865 """ 

1866 # Avoid sending out mails 

1867 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 

1868 

1869 url = reverse("password_reset") 

1870 # Our test user doesn't have an email address by default - but we need one set 

1871 self.user.email = "asdf@asdf.local" 

1872 self.user.save() 

1873 response = self.client.post( 

1874 url, 

1875 { 

1876 "email": self.user.email, 

1877 }, 

1878 follow=True, 

1879 ) 

1880 self.assertEqual(response.status_code, 200, "password reset page not working?") 

1881 self.assertEqual( 

1882 len(mail.outbox), 1, "User exists, there should be a mail in the outbox!" 

1883 ) 

1884 self.assertEqual( 

1885 mail.outbox[0].to[0], 

1886 self.user.email, 

1887 "Sending mails to the wrong \ 

1888 mail address?", 

1889 ) 

1890 

1891 def test_password_reset_w_confirmed_mail(self): 

1892 """ 

1893 Test if the password reset works correctly with confirmed 

1894 mail 

1895 """ 

1896 # Avoid sending out mails 

1897 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 

1898 

1899 url = reverse("password_reset") 

1900 # Our test user doesn't have a confirmed mail identity - add one 

1901 self.user.confirmedemail_set.create(email="asdf@asdf.local") 

1902 self.user.save() 

1903 

1904 response = self.client.post( 

1905 url, 

1906 { 

1907 "email": self.user.confirmedemail_set.first().email, 

1908 }, 

1909 follow=True, 

1910 ) 

1911 # Since the object is touched in another process, we need to refresh it 

1912 self.user.refresh_from_db() 

1913 self.assertEqual(response.status_code, 200, "password reset page not working?") 

1914 self.assertEqual( 

1915 self.user.email, 

1916 self.user.confirmedemail_set.first().email, 

1917 "The password reset view, should have corrected this!", 

1918 ) 

1919 self.assertEqual( 

1920 len(mail.outbox), 1, "user exists, there should be a mail in the outbox!" 

1921 ) 

1922 self.assertEqual( 

1923 mail.outbox[0].to[0], 

1924 self.user.email, 

1925 "why are we sending mails to the wrong mail address?", 

1926 ) 

1927 

1928 def test_password_reset_w_confirmed_mail_no_password(self): 

1929 """ 

1930 Test password reset for user with confirmed email but no password set. 

1931 This tests the specific case that was failing with Django 4.2+ where 

1932 User.objects.make_random_password() was deprecated. 

1933 

1934 Reproduces the scenario where a user has a confirmed email address 

1935 but their password field is empty or starts with "!" (unusable password). 

1936 """ 

1937 # Avoid sending out mails 

1938 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 

1939 

1940 # Create a user with no usable password (starts with "!") 

1941 test_user = User.objects.create_user( 

1942 username="testuser_no_pass", 

1943 email="", # No email in User model 

1944 ) 

1945 # Set an unusable password (starts with "!") 

1946 test_user.set_unusable_password() 

1947 test_user.save() 

1948 

1949 # Add a confirmed email (this is the key scenario) 

1950 confirmed_email = test_user.confirmedemail_set.create(email="test@example.com") 

1951 

1952 # Verify the user has no usable password 

1953 self.assertTrue( 

1954 test_user.password.startswith("!"), 

1955 "Test user should have unusable password starting with '!'", 

1956 ) 

1957 

1958 url = reverse("password_reset") 

1959 

1960 # Attempt password reset - this should work without AttributeError 

1961 response = self.client.post( 

1962 url, 

1963 { 

1964 "email": confirmed_email.email, 

1965 }, 

1966 follow=True, 

1967 ) 

1968 

1969 # Refresh user from database to see changes 

1970 test_user.refresh_from_db() 

1971 

1972 # Verify the request succeeded 

1973 self.assertEqual(response.status_code, 200, "password reset page not working?") 

1974 

1975 # Verify that the user now has a usable password (no longer starts with "!") 

1976 self.assertFalse( 

1977 test_user.password.startswith("!"), 

1978 "User should now have a usable password after reset", 

1979 ) 

1980 

1981 # Verify the email was set on the user object 

1982 self.assertEqual( 

1983 test_user.email, 

1984 confirmed_email.email, 

1985 "The password reset view should have set the email on user object", 

1986 ) 

1987 

1988 # Verify a reset email was sent 

1989 self.assertEqual( 

1990 len(mail.outbox), 1, "user exists, there should be a mail in the outbox!" 

1991 ) 

1992 self.assertEqual( 

1993 mail.outbox[0].to[0], 

1994 test_user.email, 

1995 "reset email should be sent to the correct address", 

1996 ) 

1997 

1998 # Clean up 

1999 test_user.delete() 

2000 

2001 def test_export(self): 

2002 """ 

2003 Test if export works 

2004 """ 

2005 

2006 # Create well known strings to check if export 

2007 # works as expected 

2008 self.user.confirmedemail_set.create(email="asdf@asdf.local") 

2009 self.user.confirmedopenid_set.create(openid="http://asdf.asdf.local") 

2010 self.user.save() 

2011 

2012 # Ensure we have a photo uploaded 

2013 self.test_upload_image() 

2014 

2015 self.login() 

2016 self.client.get(reverse("export")) 

2017 response = self.client.post( 

2018 reverse("export"), 

2019 {}, 

2020 follow=False, 

2021 ) 

2022 self.assertIsInstance(response.content, bytes) 

2023 fh = gzip.open(BytesIO(response.content), "rb") 

2024 content = fh.read() 

2025 fh.close() 

2026 root = xml.etree.ElementTree.fromstring(content) 

2027 self.assertEqual(root.tag, "{%s}user" % settings.SCHEMAROOT) 

2028 self.assertEqual( 

2029 root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[0][1], 

2030 self.user.username, 

2031 ) 

2032 self.assertEqual( 

2033 root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[1][1], 

2034 self.user.password, 

2035 ) 

2036 

2037 self.assertEqual( 

2038 root.findall("{%s}emails" % settings.SCHEMAROOT)[0][0].text, 

2039 self.user.confirmedemail_set.first().email, 

2040 ) 

2041 self.assertEqual( 

2042 root.findall("{%s}openids" % settings.SCHEMAROOT)[0][0].text, 

2043 self.user.confirmedopenid_set.first().openid, 

2044 ) 

2045 

2046 data = root.findall("{%s}photos" % settings.SCHEMAROOT)[0][0].text 

2047 

2048 data = data.strip("'") 

2049 data = data.strip("\\n") 

2050 data = data.lstrip("b'") 

2051 bindata = base64.decodebytes(bytes(data, "utf-8")) 

2052 image = Image.open(BytesIO(bindata)) 

2053 self.assertTrue(hasattr(image, "png")) 

2054 

2055 def test_upload_export(self): 

2056 """ 

2057 Test if uploading export works 

2058 """ 

2059 

2060 # Ensure we have data in place 

2061 self.test_export() 

2062 

2063 self.login() 

2064 self.client.get(reverse("export")) 

2065 response = self.client.post( 

2066 reverse("export"), 

2067 {}, 

2068 follow=False, 

2069 ) 

2070 self.assertIsInstance(response.content, bytes) 

2071 

2072 fh_gzip = gzip.open(BytesIO(response.content), "rb") 

2073 fh = BytesIO(response.content) 

2074 

2075 response = self._uploading_export_check( 

2076 fh_gzip, "Unable to parse file: Not a gzipped file" 

2077 ) 

2078 response = self._uploading_export_check(fh, "Choose items to be imported") 

2079 self.assertContains( 

2080 response, 

2081 "asdf@asdf.local", 

2082 2, 

2083 200, 

2084 "Upload didn't work?", 

2085 ) 

2086 

2087 def _uploading_export_check(self, fh, message): 

2088 """ 

2089 Helper function to upload an export 

2090 """ 

2091 result = self.client.post( 

2092 reverse("upload_export"), 

2093 data={"not_porn": "on", "can_distribute": "on", "export_file": fh}, 

2094 follow=True, 

2095 ) 

2096 fh.close() 

2097 self.assertEqual(result.status_code, 200, "Upload worked") 

2098 self.assertContains(result, message, 1, 200, "Upload didn't work?") 

2099 

2100 return result 

2101 

2102 def test_preferences_page(self): 

2103 """ 

2104 Test if preferences page works 

2105 """ 

2106 

2107 self.login() 

2108 self.client.get(reverse("user_preference")) 

2109 

2110 def test_delete_user(self): 

2111 """ 

2112 Test if deleting user profile works 

2113 """ 

2114 

2115 self.login() 

2116 self.client.get(reverse("delete")) 

2117 response = self.client.post( 

2118 reverse("delete"), 

2119 data={"password": self.password}, 

2120 follow=True, 

2121 ) 

2122 self.assertEqual(response.status_code, 200, "Deletion worked") 

2123 self.assertEqual(User.objects.count(), 0, "No user there any more") 

2124 

2125 def test_confirm_already_confirmed(self): 

2126 """ 

2127 Try to confirm a mail address that has been confirmed (by another user) 

2128 """ 

2129 

2130 # Add mail address (stays unconfirmed) 

2131 self.test_add_email() 

2132 

2133 # Create a second user that will conflict 

2134 user2 = User.objects.create_user( 

2135 username=f"{self.username}1", 

2136 password=self.password, 

2137 first_name=self.first_name, 

2138 last_name=self.last_name, 

2139 ) 

2140 ConfirmedEmail.objects.create( 

2141 email=self.email, 

2142 user=user2, 

2143 ) 

2144 

2145 # Just to be sure 

2146 self.assertEqual( 

2147 self.user.unconfirmedemail_set.first().email, 

2148 user2.confirmedemail_set.first().email, 

2149 "Mail not the same?", 

2150 ) 

2151 

2152 # This needs to be caught 

2153 with contextlib.suppress(AssertionError): 

2154 self.test_confirm_email() 

2155 # Request a random page, so we can access the messages 

2156 response = self.client.get(reverse("profile")) 

2157 self.assertEqual( 

2158 str(list(response.context[0]["messages"])[0]), 

2159 "This mail address has been taken already and cannot be confirmed", 

2160 "This should return an error message!", 

2161 ) 

2162 

2163 

2164class OpenIDErrorHandlingTestCase(TestCase): 

2165 """ 

2166 Test cases for OpenID error handling and error.html template coverage 

2167 """ 

2168 

2169 def setUp(self): 

2170 """Set up test user and client""" 

2171 self.username = random_string() 

2172 self.password = random_string() 

2173 self.user = User.objects.create_user( 

2174 username=self.username, 

2175 password=self.password, 

2176 ) 

2177 self.client = Client() 

2178 

2179 def login(self): 

2180 """Login as test user""" 

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

2182 

2183 def test_openid_discovery_failure_renders_error_template(self): 

2184 """ 

2185 Test that OpenID discovery failure renders error.html template 

2186 """ 

2187 from unittest.mock import patch, MagicMock 

2188 from openid.consumer import consumer 

2189 from ivatar.ivataraccount.models import UnconfirmedOpenId 

2190 

2191 self.login() 

2192 

2193 # Create an unconfirmed OpenID 

2194 unconfirmed = UnconfirmedOpenId.objects.create( 

2195 user=self.user, 

2196 openid="http://invalid-openid-provider.example.com/", 

2197 ) 

2198 

2199 # Mock the OpenID consumer to raise DiscoveryFailure 

2200 with patch( 

2201 "ivatar.ivataraccount.views.consumer.Consumer" 

2202 ) as mock_consumer_class: 

2203 mock_consumer = MagicMock() 

2204 mock_consumer_class.return_value = mock_consumer 

2205 # Create a proper DiscoveryFailure with required http_response parameter 

2206 mock_response = MagicMock() 

2207 mock_response.status_code = 404 

2208 discovery_failure = consumer.DiscoveryFailure( 

2209 "Invalid provider", mock_response 

2210 ) 

2211 mock_consumer.begin.side_effect = discovery_failure 

2212 

2213 # Make request to openid_redirection view 

2214 response = self.client.get( 

2215 reverse("openid_redirection", args=[unconfirmed.id]), follow=True 

2216 ) 

2217 

2218 # Verify we get redirected to profile with error message 

2219 self.assertEqual(response.status_code, 200) 

2220 self.assertRedirects(response, reverse("profile")) 

2221 

2222 # Check that error message is in the response 

2223 messages = list(response.context[0]["messages"]) 

2224 self.assertTrue( 

2225 any("OpenID discovery failed" in str(msg) for msg in messages) 

2226 ) 

2227 

2228 def test_openid_confirmation_failure_renders_error_template(self): 

2229 """ 

2230 Test that OpenID confirmation failure renders error.html template 

2231 """ 

2232 from unittest.mock import patch, MagicMock 

2233 from openid.consumer import consumer 

2234 from ivatar.ivataraccount.models import UnconfirmedOpenId 

2235 

2236 self.login() 

2237 

2238 # Create an unconfirmed OpenID 

2239 unconfirmed = UnconfirmedOpenId.objects.create( 

2240 user=self.user, 

2241 openid="http://test-provider.example.com/", 

2242 ) 

2243 

2244 # Mock the OpenID consumer to return FAILURE status 

2245 with patch( 

2246 "ivatar.ivataraccount.views.consumer.Consumer" 

2247 ) as mock_consumer_class: 

2248 mock_consumer = MagicMock() 

2249 mock_consumer_class.return_value = mock_consumer 

2250 

2251 # Create a mock response with FAILURE status 

2252 mock_response = MagicMock() 

2253 mock_response.status = consumer.FAILURE 

2254 mock_response.message = "Authentication failed" 

2255 mock_consumer.complete.return_value = mock_response 

2256 

2257 # Make request to confirm_openid view 

2258 response = self.client.get( 

2259 reverse("confirm_openid", args=[unconfirmed.id]), follow=True 

2260 ) 

2261 

2262 # Verify we get redirected to profile with error message 

2263 self.assertEqual(response.status_code, 200) 

2264 self.assertRedirects(response, reverse("profile")) 

2265 

2266 # Check that error message is in the response 

2267 messages = list(response.context[0]["messages"]) 

2268 self.assertTrue(any("Confirmation failed" in str(msg) for msg in messages)) 

2269 

2270 def test_openid_cancellation_renders_error_template(self): 

2271 """ 

2272 Test that OpenID cancellation renders error.html template 

2273 """ 

2274 from unittest.mock import patch, MagicMock 

2275 from openid.consumer import consumer 

2276 from ivatar.ivataraccount.models import UnconfirmedOpenId 

2277 

2278 self.login() 

2279 

2280 # Create an unconfirmed OpenID 

2281 unconfirmed = UnconfirmedOpenId.objects.create( 

2282 user=self.user, 

2283 openid="http://test-provider.example.com/", 

2284 ) 

2285 

2286 # Mock the OpenID consumer to return CANCEL status 

2287 with patch( 

2288 "ivatar.ivataraccount.views.consumer.Consumer" 

2289 ) as mock_consumer_class: 

2290 mock_consumer = MagicMock() 

2291 mock_consumer_class.return_value = mock_consumer 

2292 

2293 # Create a mock response with CANCEL status 

2294 mock_response = MagicMock() 

2295 mock_response.status = consumer.CANCEL 

2296 mock_consumer.complete.return_value = mock_response 

2297 

2298 # Make request to confirm_openid view 

2299 response = self.client.get( 

2300 reverse("confirm_openid", args=[unconfirmed.id]), follow=True 

2301 ) 

2302 

2303 # Verify we get redirected to profile with error message 

2304 self.assertEqual(response.status_code, 200) 

2305 self.assertRedirects(response, reverse("profile")) 

2306 

2307 # Check that error message is in the response 

2308 messages = list(response.context[0]["messages"]) 

2309 self.assertTrue(any("Cancelled by user" in str(msg) for msg in messages)) 

2310 

2311 def test_openid_unknown_error_renders_error_template(self): 

2312 """ 

2313 Test that unknown OpenID verification error renders error.html template 

2314 """ 

2315 from unittest.mock import patch, MagicMock 

2316 from ivatar.ivataraccount.models import UnconfirmedOpenId 

2317 

2318 self.login() 

2319 

2320 # Create an unconfirmed OpenID 

2321 unconfirmed = UnconfirmedOpenId.objects.create( 

2322 user=self.user, 

2323 openid="http://test-provider.example.com/", 

2324 ) 

2325 

2326 # Mock the OpenID consumer to return unknown status 

2327 with patch( 

2328 "ivatar.ivataraccount.views.consumer.Consumer" 

2329 ) as mock_consumer_class: 

2330 mock_consumer = MagicMock() 

2331 mock_consumer_class.return_value = mock_consumer 

2332 

2333 # Create a mock response with unknown status 

2334 mock_response = MagicMock() 

2335 mock_response.status = "UNKNOWN_STATUS" 

2336 mock_consumer.complete.return_value = mock_response 

2337 

2338 # Make request to confirm_openid view 

2339 response = self.client.get( 

2340 reverse("confirm_openid", args=[unconfirmed.id]), follow=True 

2341 ) 

2342 

2343 # Verify we get redirected to profile with error message 

2344 self.assertEqual(response.status_code, 200) 

2345 self.assertRedirects(response, reverse("profile")) 

2346 

2347 # Check that error message is in the response 

2348 messages = list(response.context[0]["messages"]) 

2349 self.assertTrue( 

2350 any("Unknown verification error" in str(msg) for msg in messages) 

2351 ) 

2352 

2353 def test_openid_nonexistent_id_error(self): 

2354 """ 

2355 Test that accessing non-existent OpenID ID shows error message 

2356 """ 

2357 self.login() 

2358 

2359 # Try to access a non-existent OpenID ID 

2360 response = self.client.get( 

2361 reverse("openid_redirection", args=[99999]), follow=True 

2362 ) 

2363 

2364 # Verify we get redirected to profile with error message 

2365 self.assertEqual(response.status_code, 200) 

2366 self.assertRedirects(response, reverse("profile")) 

2367 

2368 # Check that error message is in the response 

2369 messages = list(response.context[0]["messages"]) 

2370 self.assertTrue(any("ID does not exist" in str(msg) for msg in messages)) 

2371 

2372 def test_django_openid_auth_failure_template_coverage(self): 

2373 """ 

2374 Test that django-openid-auth failure template uses error.html 

2375 This test verifies the OpenID login page renders correctly 

2376 """ 

2377 # Try to access the OpenID login page 

2378 response = self.client.get(reverse("openid-login")) 

2379 self.assertEqual(response.status_code, 200) 

2380 

2381 # The login page should render successfully 

2382 self.assertContains(response, "OpenID Login") 

2383 

2384 def test_error_template_direct_rendering(self): 

2385 """ 

2386 Test error.html template directly to ensure it renders correctly 

2387 """ 

2388 from django.test import RequestFactory 

2389 from django.template import Context, Template 

2390 from django.contrib.auth.models import AnonymousUser 

2391 

2392 # Test with authenticated user 

2393 factory = RequestFactory() 

2394 request = factory.get("/") 

2395 request.user = self.user 

2396 

2397 # Test template with error message 

2398 template_content = """ 

2399 {% extends 'error.html' %} 

2400 {% load i18n %} 

2401 {% block errormessage %} 

2402 {% trans 'Test error message:' %} {{ errormessage }} 

2403 {% endblock errormessage %} 

2404 """ 

2405 

2406 template = Template(template_content) 

2407 context = Context( 

2408 { 

2409 "request": request, 

2410 "errormessage": "This is a test error", 

2411 "user": self.user, 

2412 } 

2413 ) 

2414 

2415 rendered = template.render(context) 

2416 

2417 # Verify the template renders without errors 

2418 self.assertIn("Error!", rendered) 

2419 self.assertIn("This is a test error", rendered) 

2420 # Check for the profile link in the navbar (not in the backlink block) 

2421 self.assertIn("/accounts/profile/", rendered) 

2422 

2423 # Test with anonymous user 

2424 request.user = AnonymousUser() 

2425 context = Context( 

2426 { 

2427 "request": request, 

2428 "errormessage": "This is a test error", 

2429 "user": AnonymousUser(), 

2430 } 

2431 ) 

2432 

2433 rendered = template.render(context) 

2434 

2435 # Verify the template renders without errors for anonymous users 

2436 self.assertIn("Error!", rendered) 

2437 self.assertIn("This is a test error", rendered) 

2438 # Should not contain profile link for anonymous users 

2439 self.assertNotIn("/accounts/profile/", rendered) 

2440 

2441 def test_openid_failure_template_inheritance(self): 

2442 """ 

2443 Test that openid/failure.html properly extends error.html 

2444 """ 

2445 from django.test import RequestFactory 

2446 from django.template import Context, Template 

2447 

2448 factory = RequestFactory() 

2449 request = factory.get("/") 

2450 request.user = self.user 

2451 

2452 # Test the openid/failure.html template 

2453 template_content = """ 

2454 {% extends 'error.html' %} 

2455 {% load i18n %} 

2456 {% block errormessage %} 

2457 {% trans 'OpenID error:' %} {{ message }} 

2458 {% endblock errormessage %} 

2459 """ 

2460 

2461 template = Template(template_content) 

2462 context = Context( 

2463 { 

2464 "request": request, 

2465 "message": "Authentication failed", 

2466 "user": self.user, 

2467 } 

2468 ) 

2469 

2470 rendered = template.render(context) 

2471 

2472 # Verify the template renders correctly 

2473 self.assertIn("Error!", rendered) 

2474 self.assertIn("OpenID error:", rendered) 

2475 self.assertIn("Authentication failed", rendered) 

2476 # Check for the profile link in the navbar (not in the backlink block) 

2477 self.assertIn("/accounts/profile/", rendered)