Coverage for ivatar/ivataraccount/test_views.py: 100%
765 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-24 23:06 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-24 23:06 +0000
1"""
2Test our views in ivatar.ivataraccount.views and ivatar.views
3"""
5import contextlib
7# pylint: disable=too-many-lines
8from urllib.parse import urlsplit
9from io import BytesIO
10from contextlib import suppress
11import io
12import os
13import gzip
14import xml.etree.ElementTree
15import base64
16import django
17from django.test import TestCase
18from django.test import Client
19from django.test import override_settings
20from django.urls import reverse
21from django.core import mail
22from django.core.cache import caches
23from django.contrib.auth.models import User
24from django.contrib.auth import authenticate
25import hashlib
27from libravatar import libravatar_url
29from PIL import Image
31os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
32django.setup()
34# pylint: disable=wrong-import-position
35from ivatar import settings
36from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
37from ivatar.ivataraccount.models import Photo, ConfirmedOpenId, ConfirmedEmail
38from ivatar.utils import random_string
40# pylint: enable=wrong-import-position
42TEST_IMAGE_FILE = os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png")
45@override_settings()
46class Tester(TestCase): # pylint: disable=too-many-public-methods
47 """
48 Main test class
49 """
51 client = Client()
52 user = None
53 username = random_string()
54 password = random_string()
55 email = "{}@{}.org".format(username, random_string())
56 # Dunno why random tld doesn't work, but I'm too lazy now to investigate
57 openid = "http://{}.{}.{}/".format(username, random_string(), "org")
58 first_name = random_string()
59 last_name = random_string()
61 def login(self):
62 """
63 Login as user
64 """
65 self.client.login(username=self.username, password=self.password)
67 def setUp(self):
68 """
69 Prepare for tests.
70 - Create user
71 """
72 self.user = User.objects.create_user(
73 username=self.username,
74 password=self.password,
75 first_name=self.first_name,
76 last_name=self.last_name,
77 )
78 # Disable caching
79 settings.CACHES["default"] = {
80 "BACKEND": "django.core.cache.backends.dummy.DummyCache",
81 }
82 caches._settings = None
83 with suppress(AttributeError):
84 # clear the existing cache connection
85 delattr(caches._connections, "default")
87 def test_new_user(self):
88 """
89 Create a new user
90 """
91 response = self.client.get(reverse("new_account"))
92 self.assertEqual(response.status_code, 200, "no 200 ok?")
93 # Empty database / eliminate existing users
94 User.objects.all().delete()
95 url = reverse("new_account")
96 response = self.client.post(
97 url,
98 {
99 "username": self.username,
100 "password1": self.password,
101 "password2": self.password,
102 },
103 follow=True,
104 )
105 self.assertEqual(response.status_code, 200, "unable to create user?")
106 self.assertEqual(response.context[0]["user"].username, self.username)
108 def test_new_user_twice(self):
109 """
110 Try to create a user that already exists
111 """
112 response = self.client.get(reverse("new_account"))
113 self.assertEqual(response.status_code, 200, "no 200 ok?")
114 # Due to setUp(), we already have this user!
115 url = reverse("new_account")
116 response = self.client.post(
117 url,
118 {
119 "username": self.username,
120 "password1": self.password,
121 "password2": self.password,
122 },
123 follow=True,
124 )
125 self.assertEqual(response.status_code, 200, "unable to create user?")
126 self.assertEqual(response.context[0]["user"].username, "")
127 self.assertContains(
128 response,
129 "A user with that username already exists.",
130 1,
131 200,
132 "can we create a user a second time???",
133 )
135 def test_set_password(self):
136 """
137 Change the user password
138 """
139 self.login()
140 response = self.client.get(reverse("password_set"))
141 self.assertEqual(response.status_code, 200, "no 200 ok?")
142 self.password = random_string()
143 response = self.client.post(
144 reverse("password_set"),
145 {
146 "new_password1": self.password,
147 "new_password2": self.password,
148 },
149 follow=True,
150 )
152 self.assertEqual(response.status_code, 200, "cannot change password?")
153 self.assertEqual(
154 str(list(response.context[0]["messages"])[0]),
155 "password changed successfully - please login again",
156 "password change not successful?",
157 )
159 self.assertIsNotNone(
160 authenticate(
161 username=self.username,
162 password=self.password,
163 ),
164 "cannot authenticate with new password!?",
165 )
167 self.login()
168 response = self.client.get(reverse("profile"))
169 self.assertEqual(response.context[0]["user"].is_anonymous, False)
171 def test_add_email(self):
172 """
173 Add e-mail address
174 """
175 self.login()
176 response = self.client.get(reverse("add_email"))
177 self.assertEqual(response.status_code, 200, "no 200 ok?")
178 # Avoid sending out mails
179 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
180 response = self.client.post(
181 reverse("add_email"),
182 {
183 "email": self.email,
184 },
185 follow=True,
186 )
187 self.assertEqual(response.status_code, 200, "cannot add email?")
188 self.assertEqual(
189 len(response.context[0]["messages"]),
190 1,
191 "there must not be more or less than ONE (1) message",
192 )
193 self.assertEqual(
194 str(list(response.context[0]["messages"])[0]),
195 "Address added successfully",
196 "unable to add mail address?",
197 )
199 def test_confirm_email(self):
200 """
201 Confirm unconfirmed email
202 """
203 self.login()
204 # Avoid sending out mails
205 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
206 response = self.client.post(
207 reverse("add_email"),
208 {
209 "email": self.email,
210 },
211 follow=True,
212 )
213 unconfirmed = self.user.unconfirmedemail_set.first()
214 verification_key = unconfirmed.verification_key
215 url = reverse("confirm_email", args=[verification_key])
216 response = self.client.get(url)
217 self.assertEqual(response.status_code, 200, "unable to confirm mail address?")
219 self.assertEqual(
220 self.user.unconfirmedemail_set.count(),
221 0,
222 "there must not be any unconfirmed address, after confirming it",
223 )
224 self.assertEqual(
225 self.user.confirmedemail_set.count(),
226 1,
227 "there must not be more or less than ONE (1) confirmed address!",
228 )
230 def test_confirm_email_w_invalid_auth_key(self): # pylint: disable=invalid-name
231 """
232 Test confirmation with invalid auth key
233 """
234 self.login()
235 # Avoid sending out mails
236 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
237 response = self.client.post(
238 reverse("add_email"),
239 {
240 "email": self.email,
241 },
242 follow=True,
243 )
244 url = reverse("confirm_email", args=["x"])
245 response = self.client.get(url, follow=True)
246 self.assertEqual(
247 response.status_code,
248 200,
249 "Not able to request confirmation - without verification key?",
250 )
251 self.assertEqual(
252 str(list(response.context[0]["messages"])[-1]),
253 "Verification key incorrect",
254 "Confirm w/o verification key does not produce error message?",
255 )
257 def test_confirm_email_w_non_existing_auth_key(
258 self,
259 ): # pylint: disable=invalid-name
260 """
261 Test confirmation with non existing auth key
262 """
263 self.login()
264 # Avoid sending out mails
265 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
266 response = self.client.post(
267 reverse("add_email"),
268 {
269 "email": self.email,
270 },
271 follow=True,
272 )
273 url = reverse("confirm_email", args=["x" * 64])
274 response = self.client.get(url, follow=True)
275 self.assertEqual(
276 response.status_code,
277 200,
278 "Not able to request confirmation - without verification key?",
279 )
280 self.assertEqual(
281 str(list(response.context[0]["messages"])[-1]),
282 "Verification key does not exist",
283 "Confirm w/o non existing key does not produce error message?",
284 )
286 def test_remove_confirmed_email(self):
287 """
288 Remove confirmed email
289 """
290 self.login()
291 # Avoid sending out mails
292 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
293 response = self.client.post(
294 reverse("add_email"),
295 {
296 "email": self.email,
297 },
298 ) # Create test address
299 unconfirmed = self.user.unconfirmedemail_set.first()
300 verification_key = unconfirmed.verification_key
301 url = reverse("confirm_email", args=[verification_key])
302 self.client.get(url) # Confirm
303 url = reverse(
304 "remove_confirmed_email", args=[self.user.confirmedemail_set.first().id]
305 )
306 response = self.client.post(url, follow=True)
307 self.assertEqual(
308 response.status_code, 200, "unable to remove confirmed address?"
309 )
310 self.assertEqual(
311 str(list(response.context[0]["messages"])[-1]),
312 "Address removed",
313 "Removing confirmed mail does not work?",
314 )
316 def test_remove_not_existing_confirmed_email(self): # pylint: disable=invalid-name
317 """
318 Try removing confirmed mail that doesn't exist
319 """
320 self.login()
321 url = reverse("remove_confirmed_email", args=[1234])
322 response = self.client.post(url, follow=True)
323 self.assertEqual(
324 response.status_code, 200, "removing email does not redirect to profile?"
325 )
326 self.assertEqual(
327 str(list(response.context[0]["messages"])[0]),
328 "Address does not exist",
329 "Removing not existing (confirmed) address, should produce an\
330 error message!",
331 )
333 def test_remove_unconfirmed_email(self):
334 """
335 Remove unconfirmed email
336 """
337 self.login()
338 # Avoid sending out mails
339 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
340 response = self.client.post(
341 reverse("add_email"),
342 {
343 "email": self.email,
344 },
345 ) # Create test address
346 url = reverse(
347 "remove_unconfirmed_email", args=[self.user.unconfirmedemail_set.first().id]
348 )
349 response = self.client.post(url, follow=True)
350 self.assertEqual(
351 response.status_code, 200, "unable to remove unconfirmed address?"
352 )
353 # Take care, since we do not fetch any page now, the message we need
354 # to check is the _second_ (aka [1], since first is [0])
355 self.assertEqual(
356 str(list(response.context[0]["messages"])[1]),
357 "Address removed",
358 "Removing unconfirmed mail does not work?",
359 )
361 def test_gravatar_photo_import(self):
362 """
363 import photo from Gravatar (with known mail address)
364 """
365 self.login()
366 # Avoid sending out mails
367 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
368 response = self.client.post(
369 reverse("add_email"),
370 {
371 "email": "oliver@linux-kernel.at", # Wow, static :-[
372 },
373 ) # Create test address
374 unconfirmed = self.user.unconfirmedemail_set.first()
375 verification_key = unconfirmed.verification_key
376 url = reverse("confirm_email", args=[verification_key])
377 self.client.get(url) # Confirm
379 url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id])
380 response = self.client.post(
381 url,
382 {
383 "photo_Gravatar": 1,
384 },
385 follow=True,
386 )
387 self.assertEqual(
388 response.status_code, 200, "unable to import photo from Gravatar?"
389 )
390 self.assertEqual(
391 str(list(response.context[0]["messages"])[-1]),
392 "Gravatar image successfully imported",
393 "Importing gravatar photo did not work?",
394 )
395 self.assertIsInstance(
396 self.user.photo_set.first(), Photo, "why is there no Photo (instance)?"
397 )
399 def test_raw_image(self):
400 """
401 test raw image view (as seen in profile <img src=
402 """
404 # Ensure we have a photo
405 self.test_gravatar_photo_import()
406 response = self.client.get(
407 reverse("raw_image", args=[self.user.photo_set.first().id])
408 )
409 self.assertEqual(response.status_code, 200, "cannot fetch photo?")
410 # Probably not the best way to access the content type
411 self.assertEqual(response["Content-Type"], "image/jpg", "Content type wrong!?")
413 self.assertEqual(
414 bytes(response.content),
415 bytes(self.user.photo_set.first().data),
416 "raw_image should return the same content as if we\
417 read it directly from the DB",
418 )
420 def test_delete_photo(self):
421 """
422 test deleting the photo
423 """
425 # Ensure we have a photo
426 self.test_gravatar_photo_import()
428 url = reverse("delete_photo", args=[self.user.photo_set.first().id])
429 response = self.client.get(url, follow=True)
430 self.assertEqual(response.status_code, 200, "deleting photo does not work?")
431 self.assertEqual(
432 str(list(response.context[0]["messages"])[-1]),
433 "Photo deleted successfully",
434 "Photo deletion did not work?",
435 )
437 def test_delete_non_existing_photo(self):
438 """
439 test deleting the photo
440 """
442 # Ensure we have a photo
443 self.test_gravatar_photo_import()
445 url = reverse("delete_photo", args=[1234])
446 response = self.client.get(url, follow=True)
447 self.assertEqual(response.status_code, 200, "post to delete does not work?")
448 self.assertEqual(
449 str(list(response.context[0]["messages"])[-1]),
450 "No such image or no permission to delete it",
451 "Deleting photo that does not exist, should return error message",
452 )
454 def test_too_many_unconfirmed_email(self):
455 """
456 Request too many unconfirmed email addresses, make sure we
457 cannot add more
458 """
459 self.login()
460 # Avoid sending out mails
461 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
463 max_num_unconfirmed = getattr(
464 settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
465 )
467 for i in range(max_num_unconfirmed + 1):
468 response = self.client.post( # noqa: F841
469 reverse("add_email"),
470 {
471 "email": "%i.%s" % (i, self.email),
472 },
473 follow=True,
474 ) # Create test addresses + 1 too much
475 return self._check_form_validity(
476 response, "Too many unconfirmed mail addresses!", "__all__"
477 )
479 def test_add_mail_address_twice(self):
480 """
481 Request the same mail address two times, should not lead to
482 having the same address twice
483 """
484 self.login()
485 # Avoid sending out mails
486 settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
488 for _ in range(2):
489 response = self.client.post( # noqa: F841
490 reverse("add_email"),
491 {
492 "email": self.email,
493 },
494 follow=True,
495 )
496 return self._check_form_validity(
497 response, "Address already added, currently unconfirmed", "email"
498 )
500 def test_add_already_confirmed_email_self(self): # pylint: disable=invalid-name
501 """
502 Request adding mail address that is already confirmed (by someone)
503 """
504 # Create test mail and confirm it, reuse test code
505 # Should set EMAIL_BACKEND, so no need to do it here
506 self.test_confirm_email()
508 response = self.client.post( # noqa: F841
509 reverse("add_email"),
510 {
511 "email": self.email,
512 },
513 follow=True,
514 )
516 return self._check_form_validity(
517 response, "Address already confirmed (by you)", "email"
518 )
520 def test_add_already_confirmed_email_other(self): # pylint: disable=invalid-name
521 """
522 Request adding mail address that is already confirmed (by someone)
523 """
524 # Create test mail and confirm it, reuse test code
525 # Should set EMAIL_BACKEND, so no need to do it here
526 self.test_confirm_email()
528 # Create another user and assign the mail address to that one
529 # in order to test the correct error message
530 otheruser = User.objects.create(username="otheruser")
531 confirmedemail = ConfirmedEmail.objects.last()
532 confirmedemail.user = otheruser
533 confirmedemail.save()
535 response = self.client.post( # noqa: F841
536 reverse("add_email"),
537 {
538 "email": self.email,
539 },
540 follow=True,
541 )
543 return self._check_form_validity(
544 response, "Address already confirmed (by someone else)", "email"
545 )
547 def test_remove_unconfirmed_non_existing_email(
548 self,
549 ): # pylint: disable=invalid-name
550 """
551 Remove unconfirmed email that doesn't exist
552 """
553 self.login()
554 url = reverse("remove_unconfirmed_email", args=[1234])
555 response = self.client.post(url, follow=True)
556 self.assertEqual(
557 response.status_code, 200, "unable to remove non existing address?"
558 )
559 self.assertEqual(
560 str(list(response.context[0]["messages"])[0]),
561 "Address does not exist",
562 "Removing address that does not\
563 exist, should return error message!",
564 )
566 def test_upload_image(
567 self, test_only_one=True
568 ): # pylint: disable=inconsistent-return-statements
569 """
570 Test uploading image
571 """
572 self.login()
573 url = reverse("upload_photo")
574 # rb => Read binary
575 with open(TEST_IMAGE_FILE, "rb") as photo_file:
576 photo_data = photo_file.read()
578 from django.core.files.uploadedfile import SimpleUploadedFile
580 uploaded_file = SimpleUploadedFile(
581 "deadbeef.png", photo_data, content_type="image/png"
582 )
583 response = self.client.post(
584 url,
585 {
586 "photo": uploaded_file,
587 "not_porn": True,
588 "can_distribute": True,
589 },
590 follow=True,
591 )
592 if not test_only_one:
593 return response
594 self.assertEqual(
595 self.user.photo_set.count(), 1, "there must be exactly one photo now!"
596 )
597 self.assertEqual(
598 str(list(response.context[0]["messages"])[-1]),
599 "Successfully uploaded",
600 "A valid image should return a success message!",
601 )
602 self.assertEqual(
603 self.user.photo_set.first().format,
604 "png",
605 "Format must be png, since we uploaded a png!",
606 )
608 def test_upload_too_many_images(self):
609 """
610 Test uploading more images than we are allowed
611 """
612 for _ in range(settings.MAX_NUM_PHOTOS + 1):
613 response = self.test_upload_image(test_only_one=False)
614 self.assertEqual(
615 self.user.photo_set.count(),
616 settings.MAX_NUM_PHOTOS,
617 "there may not be more photos than allowed!",
618 )
619 # Take care we need to check the last message
620 self.assertEqual(
621 str(list(response.context[0]["messages"])[-1]),
622 "Maximum number of photos (%i) reached" % settings.MAX_NUM_PHOTOS,
623 "Adding more than allowed images, should return error message!",
624 )
626 def test_upload_too_big_image(self):
627 """
628 Test uploading image that is too big
629 """
630 self.login()
631 url = reverse("upload_photo")
632 # rb => Read binary
633 response = self.client.post(
634 url,
635 {
636 "photo": io.StringIO("x" * (settings.MAX_PHOTO_SIZE + 1)),
637 "not_porn": True,
638 "can_distribute": True,
639 },
640 follow=True,
641 )
642 self.assertEqual(
643 str(list(response.context[0]["messages"])[0]),
644 "Image too big",
645 "Uploading too big image, should return error message!",
646 )
648 def test_upload_invalid_image(self):
649 """
650 Test invalid image data
651 """
652 self.login()
653 url = reverse("upload_photo")
654 # rb => Read binary
655 response = self.client.post(
656 url,
657 {
658 "photo": io.StringIO("x"),
659 "not_porn": True,
660 "can_distribute": True,
661 },
662 follow=True,
663 )
664 self.assertEqual(
665 str(list(response.context[0]["messages"])[0]),
666 "Invalid Format",
667 "Invalid img data should return error message!",
668 )
670 def test_upload_invalid_image_format(self): # pylint: disable=invalid-name
671 """
672 Test if invalid format is correctly detected
673 """
674 self.login()
675 url = reverse("upload_photo")
676 # rb => Read binary
677 with open(os.path.join(settings.STATIC_ROOT, "img", "mm.svg"), "rb") as photo:
678 response = self.client.post(
679 url,
680 {
681 "photo": photo,
682 "not_porn": True,
683 "can_distribute": True,
684 },
685 follow=True,
686 )
687 self.assertEqual(
688 str(list(response.context[0]["messages"])[0]),
689 "Invalid Format",
690 "Invalid img data should return error message!",
691 )
693 def test_upload_gif_image(self):
694 """
695 Test if gif is correctly detected and can be viewed
696 """
697 self._extracted_from_test_upload_webp_image_5(
698 "broken.gif",
699 "GIF upload failed?!",
700 "gif",
701 "Format must be gif, since we uploaded a GIF!",
702 )
704 def test_upload_jpg_image(self):
705 """
706 Test if jpg is correctly detected and can be viewed
707 """
708 self._extracted_from_test_upload_webp_image_5(
709 "broken.jpg",
710 "JPEG upload failed?!",
711 "jpg",
712 "Format must be jpeg, since we uploaded a jpeg!",
713 )
715 def test_upload_webp_image(self):
716 """
717 Test if webp is correctly detected and can be viewed
718 """
719 self._extracted_from_test_upload_webp_image_5(
720 "broken.webp",
721 "WEBP upload failed?!",
722 "webp",
723 "Format must be webp, since we uploaded a webp!",
724 )
726 def _extracted_from_test_upload_webp_image_5(
727 self, filename, message1, format, message2
728 ):
729 """
730 Helper function for common checks for gif, jpg, webp
731 """
732 self.login()
733 url = reverse("upload_photo")
734 with open(os.path.join(settings.STATIC_ROOT, "img", filename), "rb") as photo:
735 response = self.client.post(
736 url,
737 {"photo": photo, "not_porn": True, "can_distribute": True},
738 follow=True,
739 )
740 self.assertEqual(
741 str(list(response.context[0]["messages"])[0]),
742 "Successfully uploaded",
743 message1,
744 )
745 self.assertEqual(self.user.photo_set.first().format, format, message2)
746 self.test_confirm_email()
747 self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
748 urlobj = urlsplit(
749 libravatar_url(email=self.user.confirmedemail_set.first().email)
750 )
751 url = f"{urlobj.path}?{urlobj.query}"
752 response = self.client.get(url, follow=True)
753 self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
755 def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
756 """
757 Test if unsupported format is correctly detected
758 """
759 self.login()
760 url = reverse("upload_photo")
761 # rb => Read binary
762 with open(
763 os.path.join(settings.STATIC_ROOT, "img", "broken.tif"), "rb"
764 ) as photo:
765 response = self.client.post(
766 url,
767 {
768 "photo": photo,
769 "not_porn": True,
770 "can_distribute": True,
771 },
772 follow=True,
773 )
774 self.assertEqual(
775 str(list(response.context[0]["messages"])[0]),
776 "Invalid Format",
777 "Invalid img data should return error message!",
778 )
780 def test_automatic_photo_assign_to_confirmed_mail(
781 self,
782 ): # pylint: disable=invalid-name
783 """
784 Test if automatic assignment of photo works
785 """
786 self.test_upload_image()
787 self.test_confirm_email()
788 confirmed = self.user.confirmedemail_set.first()
789 self.assertEqual(confirmed.photo, self.user.photo_set.first())
791 def test_assign_photo_to_email(self):
792 """
793 Test assigning photo to mail address
794 """
795 self.test_confirm_email()
796 self.test_upload_image()
797 self.assertIsNone(self.user.confirmedemail_set.first().photo)
798 url = reverse(
799 "assign_photo_email", args=[self.user.confirmedemail_set.first().id]
800 )
801 # The get is for the view - test context data
802 self.client.get(
803 url,
804 {
805 "photo_id": self.user.photo_set.first().id,
806 },
807 )
808 # The post is for the actual assigning
809 response = self.client.post(
810 url,
811 {
812 "photo_id": self.user.photo_set.first().id,
813 },
814 follow=True,
815 )
816 self.assertEqual(response.status_code, 200, "cannot assign photo?")
817 self.assertEqual(
818 self.user.confirmedemail_set.first().photo, self.user.photo_set.first()
819 )
821 def test_no_photo_to_email(self):
822 """
823 Test assigning photo to mail address
824 """
825 self.test_confirm_email()
826 url = reverse(
827 "assign_photo_email", args=[self.user.confirmedemail_set.first().id]
828 )
829 response = self.client.post(
830 url,
831 {
832 "photoNone": True,
833 },
834 follow=True,
835 )
836 self.assertEqual(response.status_code, 200, "cannot un-assign photo?")
837 self.assertEqual(self.user.confirmedemail_set.first().photo, None)
839 def test_assign_photo_to_email_wo_photo_for_testing_template(
840 self,
841 ): # pylint: disable=invalid-name
842 """
843 Test assign photo template
844 """
845 self.test_confirm_email()
846 url = reverse(
847 "assign_photo_email", args=[self.user.confirmedemail_set.first().id]
848 )
849 # The get is for the view - test context data
850 response = self.client.get(url)
851 self.assertEqual(response.status_code, 200, "cannot fetch page?")
853 def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name
854 """
855 Test if assigning an invalid photo id returns the correct error message
856 """
857 self.test_confirm_email()
858 self.test_upload_image()
859 self.assertIsNone(self.user.confirmedemail_set.first().photo)
860 url = reverse(
861 "assign_photo_email", args=[self.user.confirmedemail_set.first().id]
862 )
863 response = self.client.post(
864 url,
865 {
866 "photo_id": 1234,
867 },
868 follow=True,
869 )
870 self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
871 self.assertEqual(
872 str(list(response.context[0]["messages"])[-1]),
873 "Photo does not exist",
874 "Assign non existing photo, does not return error message?",
875 )
877 def test_post_to_assign_photo_without_photo_id(
878 self,
879 ): # pylint: disable=invalid-name
880 """
881 Test if assigning photo without id returns the correct error message
882 """
883 self.test_confirm_email()
884 self.test_upload_image()
885 self.assertIsNone(self.user.confirmedemail_set.first().photo)
886 url = reverse(
887 "assign_photo_email", args=[self.user.confirmedemail_set.first().id]
888 )
889 response = self.client.post(url, {}, follow=True)
890 self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
891 self.assertEqual(
892 str(list(response.context[0]["messages"])[-1]),
893 "Invalid request [photo_id] missing",
894 "Assign non existing photo, does not return error message?",
895 )
897 def test_assign_photo_to_non_existing_mail(self): # pylint: disable=invalid-name
898 """
899 Test if assigning photo to mail address that doesn't exist returns
900 the correct error message
901 """
902 self.test_upload_image()
903 url = reverse("assign_photo_email", args=[1234])
904 response = self.client.post(
905 url,
906 {
907 "photo_id": self.user.photo_set.first().id,
908 },
909 follow=True,
910 )
911 self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
912 self.assertEqual(
913 str(list(response.context[0]["messages"])[-1]),
914 "Invalid request",
915 "Assign non existing photo, does not return error message?",
916 )
918 def test_import_photo_with_non_existing_email(self): # pylint: disable=invalid-name
919 """
920 Test if import with non existing mail address returns
921 the correct error message
922 """
923 self.login()
924 url = reverse("import_photo", args=[1234])
925 response = self.client.post(url, {}, follow=True)
926 self.assertEqual(response.status_code, 200, "cannot post import photo request?")
927 self.assertEqual(
928 str(list(response.context[0]["messages"])[0]),
929 "Address does not exist",
930 "Import photo with non existing mail id,\
931 does not return error message?",
932 )
934 def test_import_nothing(self):
935 """
936 Test if importing nothing causes the correct
937 error message to be returned
938 """
939 self.test_confirm_email()
940 url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id])
941 response = self.client.post(url, {}, follow=True)
942 self.assertEqual(response.status_code, 200, "cannot post import photo request?")
943 self.assertEqual(
944 str(list(response.context[0]["messages"])[-1]),
945 "Nothing importable",
946 "Importing with email that does not exist in Gravatar,\
947 should return an error message!",
948 )
950 def _manual_confirm(self):
951 """
952 Helper method to confirm manually, because testing is really hard
953 """
954 # Manual confirm, since testing is _really_ hard!
955 unconfirmed = self.user.unconfirmedopenid_set.first()
956 confirmed = ConfirmedOpenId()
957 confirmed.user = unconfirmed.user
958 confirmed.ip_address = "127.0.0.1"
959 confirmed.openid = unconfirmed.openid
960 confirmed.save()
961 unconfirmed.delete()
963 def test_add_openid(self, confirm=True):
964 """
965 Test if adding an OpenID works
966 """
968 self.login()
969 # Get page
970 response = self.client.get(reverse("add_openid"))
971 self.assertEqual(
972 response.status_code, 200, "Fetching page to add OpenID fails?"
973 )
975 response = self.client.post(
976 reverse("add_openid"),
977 {
978 "openid": self.openid,
979 },
980 )
981 self.assertEqual(response.status_code, 302, "OpenID must redirect")
983 if confirm:
984 self._manual_confirm()
986 def test_add_openid_twice(self):
987 """
988 Test if adding OpenID a second time works - it shouldn't
989 """
990 self.login()
991 # Get page
992 response = self.client.get(reverse("add_openid"))
993 self.assertEqual(
994 response.status_code, 200, "Fetching page to add OpenID fails?"
995 )
997 response = self.client.post(
998 reverse("add_openid"),
999 {
1000 "openid": self.openid,
1001 },
1002 )
1003 self.assertEqual(response.status_code, 302, "OpenID must redirect")
1004 response = self.client.post(
1005 reverse("add_openid"),
1006 {
1007 "openid": self.openid,
1008 },
1009 follow=True,
1010 )
1011 self.assertEqual(
1012 self.user.unconfirmedopenid_set.count(),
1013 1,
1014 "There must only be one unconfirmed ID!",
1015 )
1017 self._check_form_validity(
1018 response, "OpenID already added, but not confirmed yet!", "openid"
1019 )
1020 # Manual confirm, since testing is _really_ hard!
1021 unconfirmed = self.user.unconfirmedopenid_set.first()
1022 confirmed = ConfirmedOpenId()
1023 confirmed.user = unconfirmed.user
1024 confirmed.ip_address = "127.0.0.1"
1025 confirmed.openid = unconfirmed.openid
1026 confirmed.save()
1027 unconfirmed.delete()
1029 # Try adding it again - although already confirmed
1030 response = self.client.post(
1031 reverse("add_openid"),
1032 {
1033 "openid": self.openid,
1034 },
1035 follow=True,
1036 )
1038 return self._check_form_validity(
1039 response, "OpenID already added and confirmed!", "openid"
1040 )
1042 def _check_form_validity(self, response, message, field):
1043 """
1044 Helper method to check form, used in several test functions,
1045 deduplicating code
1046 """
1048 self.assertTrue(
1049 hasattr(response, "context"), "Response does not have a context"
1050 )
1051 result = response.context.get("form")
1052 self.assertIsNotNone(result, "No form found in response context")
1053 self.assertFalse(result.is_valid(), "Form should not be valid")
1054 self.assertIn(message, result.errors.get(field, []))
1055 return result
1057 def test_assign_photo_to_openid(self):
1058 """
1059 Test assignment of photo to openid
1060 """
1061 self.test_add_openid()
1062 self.test_upload_image()
1063 self.assertIsNone(self.user.confirmedopenid_set.first().photo)
1064 url = reverse(
1065 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
1066 )
1067 # The get is for the view - test context data
1068 self.client.get(
1069 url,
1070 {
1071 "photo_id": self.user.photo_set.first().id,
1072 },
1073 )
1074 # The post is for the actual assigning
1075 response = self.client.post(
1076 url,
1077 {
1078 "photo_id": self.user.photo_set.first().id,
1079 },
1080 follow=True,
1081 )
1082 self.assertEqual(response.status_code, 200, "cannot assign photo?")
1083 self.assertEqual(
1084 self.user.confirmedopenid_set.first().photo, self.user.photo_set.first()
1085 )
1087 def test_assign_photo_to_openid_wo_photo_for_testing_template(
1088 self,
1089 ): # pylint: disable=invalid-name
1090 """
1091 Test openid/photo assignment template
1092 """
1093 self.test_add_openid()
1094 url = reverse(
1095 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
1096 )
1097 response = self.client.get(url)
1098 self.assertEqual(response.status_code, 200, "cannot fetch page?")
1100 def test_assign_invalid_photo_id_to_openid(self): # pylint: disable=invalid-name
1101 """
1102 Test assigning invalid photo to openid returns
1103 the correct error message
1104 """
1105 self.test_add_openid()
1106 self.assertIsNone(self.user.confirmedopenid_set.first().photo)
1107 url = reverse(
1108 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
1109 )
1110 response = self.client.post(
1111 url,
1112 {
1113 "photo_id": 1234,
1114 },
1115 follow=True,
1116 )
1117 self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
1118 self.assertEqual(
1119 str(list(response.context[0]["messages"])[-1]),
1120 "Photo does not exist",
1121 "Assign non existing photo, does not return error message?",
1122 )
1124 def test_post_to_assign_photo_openid_without_photo_id(
1125 self,
1126 ): # pylint: disable=invalid-name
1127 """
1128 Test POST assign photo to openid without photo id
1129 returns the correct error message
1130 """
1131 self.test_add_openid()
1132 self.test_upload_image()
1133 self.assertIsNone(self.user.confirmedopenid_set.first().photo)
1134 url = reverse(
1135 "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
1136 )
1137 response = self.client.post(url, {}, follow=True)
1138 self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
1139 self.assertEqual(
1140 str(list(response.context[0]["messages"])[-1]),
1141 "Invalid request [photo_id] missing",
1142 "Assign non existing photo, does not return error message?",
1143 )
1145 def test_assign_photo_to_openid_non_existing_openid(
1146 self,
1147 ): # pylint: disable=invalid-name
1148 """
1149 Test assigning photo to openid that doesn't exist
1150 returns the correct error message.
1151 """
1152 self.test_upload_image()
1153 url = reverse("assign_photo_openid", args=[1234])
1154 response = self.client.post(
1155 url,
1156 {
1157 "photo_id": self.user.photo_set.first().id,
1158 },
1159 follow=True,
1160 )
1161 self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
1162 self.assertEqual(
1163 str(list(response.context[0]["messages"])[-1]),
1164 "Invalid request",
1165 "Assign non existing photo, does not return error message?",
1166 )
1168 def test_remove_confirmed_openid(self): # pylint: disable=invalid-name
1169 """
1170 Remove confirmed openid
1171 """
1172 self.test_add_openid()
1173 url = reverse(
1174 "remove_confirmed_openid", args=[self.user.confirmedopenid_set.first().id]
1175 )
1176 response = self.client.post(url, follow=True)
1177 self.assertEqual(
1178 response.status_code, 200, "unable to remove confirmed openid?"
1179 )
1180 self.assertEqual(
1181 str(list(response.context[0]["messages"])[-1]),
1182 "ID removed",
1183 "Removing confirmed openid does not work?",
1184 )
1186 def test_remove_not_existing_confirmed_openid(self): # pylint: disable=invalid-name
1187 """
1188 Try removing confirmed openid that doesn't exist
1189 """
1190 self.login()
1191 url = reverse("remove_confirmed_openid", args=[1234])
1192 response = self.client.post(url, follow=True)
1193 self.assertEqual(
1194 response.status_code, 200, "removing id does not redirect to profile?"
1195 )
1196 self.assertEqual(
1197 str(list(response.context[0]["messages"])[0]),
1198 "ID does not exist",
1199 "Removing not existing (confirmed) address, should produce an\
1200 error message!",
1201 )
1203 def test_remove_unconfirmed_openid(self):
1204 """
1205 Remove unconfirmed openid
1206 """
1207 self.test_add_openid(confirm=False)
1208 url = reverse(
1209 "remove_unconfirmed_openid",
1210 args=[self.user.unconfirmedopenid_set.first().id],
1211 )
1212 response = self.client.post(url, follow=True)
1213 self.assertEqual(
1214 response.status_code, 200, "unable to remove unconfirmed address?"
1215 )
1216 self.assertEqual(
1217 str(list(response.context[0]["messages"])[-1]),
1218 "ID removed",
1219 "Removing unconfirmed mail does not work?",
1220 )
1222 def test_remove_unconfirmed_non_existing_openid(
1223 self,
1224 ): # pylint: disable=invalid-name
1225 """
1226 Remove unconfirmed openid that doesn't exist
1227 """
1228 self.login()
1229 url = reverse("remove_unconfirmed_openid", args=[1234])
1230 response = self.client.post(url, follow=True)
1231 self.assertEqual(
1232 response.status_code, 200, "unable to remove unconfirmed address?"
1233 )
1234 self.assertEqual(
1235 str(list(response.context[0]["messages"])[0]),
1236 "ID does not exist",
1237 "Removing an non existing openid should return an error message",
1238 )
1240 def test_openid_redirect_view(self):
1241 """
1242 Test redirect view
1243 """
1244 self.test_add_openid(confirm=False)
1245 url = reverse(
1246 "openid_redirection", args=[self.user.unconfirmedopenid_set.first().id]
1247 )
1248 response = self.client.get(url, follow=True)
1249 self.assertEqual(
1250 response.status_code, 200, "unable to remove unconfirmed address?"
1251 )
1252 # self.assertContains(
1253 # response,
1254 # 'OpenID discovery failed: ', 1, 200,
1255 # 'This request must return an error in test mode'
1256 # )
1258 def test_set_photo_on_openid(self):
1259 """
1260 Test the set_photo function on our ConfirmedOpenId model.
1261 """
1262 self.test_add_openid()
1263 self.test_upload_image()
1264 self.assertIsNone(self.user.confirmedopenid_set.first().photo)
1265 self.user.confirmedopenid_set.first().set_photo(self.user.photo_set.first())
1266 self.assertEqual(
1267 self.user.confirmedopenid_set.first().photo,
1268 self.user.photo_set.first(),
1269 "set_photo did not work!?",
1270 )
1272 def test_avatar_url_mail(self, do_upload_and_confirm=True, size=(80, 80)):
1273 """
1274 Test fetching avatar via mail
1275 """
1276 if do_upload_and_confirm:
1277 self.test_upload_image()
1278 self.test_confirm_email()
1279 urlobj = urlsplit(
1280 libravatar_url(
1281 email=self.user.confirmedemail_set.first().email,
1282 size=size[0],
1283 )
1284 )
1285 url = f"{urlobj.path}?{urlobj.query}"
1286 response = self.client.get(url, follow=True)
1287 self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
1288 photodata = Image.open(BytesIO(response.content))
1289 self.assertEqual(photodata.size, size, "Why is this not the correct size?")
1291 def test_avatar_url_openid(self):
1292 """
1293 Test fetching avatar via openid
1294 """
1295 self.test_assign_photo_to_openid()
1296 urlobj = urlsplit(
1297 libravatar_url(
1298 openid=self.user.confirmedopenid_set.first().openid,
1299 size=80,
1300 )
1301 )
1302 url = f"{urlobj.path}?{urlobj.query}"
1303 response = self.client.get(url, follow=True)
1304 self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
1305 photodata = Image.open(BytesIO(response.content))
1306 self.assertEqual(photodata.size, (80, 80), "Why is this not the correct size?")
1308 def test_avatar_url_non_existing_mail_digest(self): # pylint: disable=invalid-name
1309 """
1310 Test fetching avatar via non existing mail digest
1311 """
1312 self.test_upload_image()
1313 self.test_confirm_email()
1314 urlobj = urlsplit(
1315 libravatar_url(
1316 email=self.user.confirmedemail_set.first().email,
1317 size=80,
1318 )
1319 )
1320 # Simply delete it, then it's digest is 'correct', but
1321 # the hash is no longer there
1322 addr = self.user.confirmedemail_set.first().email
1323 hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
1325 self.user.confirmedemail_set.first().delete()
1326 url = f"{urlobj.path}?{urlobj.query}"
1327 self.client.get(url, follow=True)
1328 # TODO: All these tests still fails under some circumstances - it needs further investigation
1329 # self.assertEqual(
1330 # response.redirect_chain[0][0],
1331 # f"/gravatarproxy/{digest}?s=80",
1332 # "Doesn't redirect to Gravatar?",
1333 # )
1334 # self.assertEqual(
1335 # response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
1336 # )
1337 # self.assertEqual(
1338 # response.redirect_chain[1][0],
1339 # f"/avatar/{digest}?s=80&forcedefault=y",
1340 # "Doesn't redirect with default forced on?",
1341 # )
1342 # self.assertEqual(
1343 # response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
1344 # )
1345 # self.assertEqual(
1346 # response.redirect_chain[2][0],
1347 # "/static/img/nobody/80.png",
1348 # "Doesn't redirect to static?",
1349 # )
1350 # self.assertRedirects(
1351 # response=response,
1352 # expected_url="/static/img/nobody/80.png",
1353 # msg_prefix="Why does this not redirect to Gravatar?",
1354 # )
1355 # Eventually one should check if the data is the same
1357 def test_avatar_url_non_existing_mail_digest_gravatarproxy_disabled(
1358 self,
1359 ): # pylint: disable=invalid-name
1360 """
1361 Test fetching avatar via non existing mail digest
1362 """
1363 self.test_upload_image()
1364 self.test_confirm_email()
1365 urlobj = urlsplit(
1366 libravatar_url(
1367 email=self.user.confirmedemail_set.first().email,
1368 size=80,
1369 )
1370 )
1371 # Simply delete it, then it digest is 'correct', but
1372 # the hash is no longer there
1373 self.user.confirmedemail_set.first().delete()
1374 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
1375 response = self.client.get(url, follow=True)
1376 self.assertEqual(
1377 response.redirect_chain[0][0],
1378 "/static/img/nobody/80.png",
1379 "Doesn't redirect to static?",
1380 )
1382 # self.assertRedirects(
1383 # response=response,
1384 # expected_url="/static/img/nobody/80.png",
1385 # msg_prefix="Why does this not redirect to the default img?",
1386 # )
1387 # Eventually one should check if the data is the same
1389 def test_avatar_url_non_existing_mail_digest_w_default_mm(
1390 self,
1391 ): # pylint: disable=invalid-name
1392 """
1393 Test fetching avatar via non existing mail digest and default 'mm'
1394 """
1395 urlobj = urlsplit(
1396 libravatar_url(
1397 email="asdf@company.local",
1398 size=80,
1399 default="mm",
1400 )
1401 )
1402 url = f"{urlobj.path}?{urlobj.query}"
1403 self.client.get(url, follow=False)
1405 def test_avatar_url_non_existing_mail_digest_w_default_mm_gravatarproxy_disabled(
1406 self,
1407 ): # pylint: disable=invalid-name
1408 """
1409 Test fetching avatar via non existing mail digest and default 'mm'
1410 """
1411 urlobj = urlsplit(
1412 libravatar_url(
1413 email="asdf@company.local",
1414 size=80,
1415 default="mm",
1416 )
1417 )
1418 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
1419 response = self.client.get(url, follow=True)
1420 self.assertEqual(
1421 response.redirect_chain[0][0],
1422 "/static/img/mm/80.png",
1423 "Doesn't redirect to static?",
1424 )
1426 # self.assertRedirects(
1427 # response=response,
1428 # expected_url="/static/img/mm/80.png",
1429 # msg_prefix="Why does this not redirect to the default img?",
1430 # )
1431 # Eventually one should check if the data is the same
1433 def test_avatar_url_non_existing_mail_digest_wo_default(
1434 self,
1435 ): # pylint: disable=invalid-name
1436 """
1437 Test fetching avatar via non existing mail digest and default 'mm'
1438 """
1439 urlobj = urlsplit(
1440 libravatar_url(
1441 email="asdf@company.local",
1442 size=80,
1443 )
1444 )
1445 digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest()
1446 url = f"{urlobj.path}?{urlobj.query}"
1447 response = self.client.get(url, follow=True)
1448 self.assertEqual(
1449 response.redirect_chain[0][0],
1450 f"/gravatarproxy/{digest}?s=80",
1451 "Doesn't redirect to Gravatar?",
1452 )
1453 self.assertEqual(
1454 response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
1455 )
1456 self.assertEqual(
1457 response.redirect_chain[1][0],
1458 f"/avatar/{digest}?s=80&forcedefault=y",
1459 "Doesn't redirect with default forced on?",
1460 )
1461 self.assertEqual(
1462 response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
1463 )
1464 self.assertEqual(
1465 response.redirect_chain[2][0],
1466 "/static/img/nobody/80.png",
1467 "Doesn't redirect to static?",
1468 )
1470 # self.assertRedirects(
1471 # response=response,
1472 # expected_url="/static/img/nobody/80.png",
1473 # msg_prefix="Why does this not redirect to the default img?",
1474 # )
1475 # Eventually one should check if the data is the same
1477 def test_avatar_url_non_existing_mail_digest_wo_default_gravatarproxy_disabled(
1478 self,
1479 ): # pylint: disable=invalid-name
1480 """
1481 Test fetching avatar via non existing mail digest and default 'mm'
1482 """
1483 urlobj = urlsplit(
1484 libravatar_url(
1485 email="asdf@company.local",
1486 size=80,
1487 )
1488 )
1489 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
1490 response = self.client.get(url, follow=True)
1491 self.assertEqual(
1492 response.redirect_chain[0][0],
1493 "/static/img/nobody/80.png",
1494 "Doesn't redirect to static?",
1495 )
1497 # self.assertRedirects(
1498 # response=response,
1499 # expected_url="/static/img/nobody/80.png",
1500 # msg_prefix="Why does this not redirect to the default img?",
1501 # )
1502 # Eventually one should check if the data is the same
1504 def test_avatar_url_default(self): # pylint: disable=invalid-name
1505 """
1506 Test fetching avatar for not existing mail with default specified
1507 """
1508 urlobj = urlsplit(
1509 libravatar_url(
1510 "xxx@xxx.xxx",
1511 size=80,
1512 default="/static/img/nobody.png",
1513 )
1514 )
1515 url = f"{urlobj.path}?{urlobj.query}"
1516 url += "&gravatarproxy=n"
1517 response = self.client.get(url, follow=False)
1518 self.assertEqual(response.status_code, 302, "Doesn't redirect with 302?")
1519 self.assertEqual(
1520 response["Location"],
1521 "/static/img/nobody.png",
1522 "Doesn't redirect to static img?",
1523 )
1525 def test_avatar_url_default_gravatarproxy_disabled(
1526 self,
1527 ): # pylint: disable=invalid-name
1528 """
1529 Test fetching avatar for not existing mail with default specified
1530 """
1531 urlobj = urlsplit(
1532 libravatar_url(
1533 "xxx@xxx.xxx",
1534 size=80,
1535 default="/static/img/nobody.png",
1536 )
1537 )
1538 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
1539 response = self.client.get(url, follow=True)
1540 self.assertEqual(
1541 response.redirect_chain[0][0],
1542 "/static/img/nobody.png",
1543 "Doesn't redirect to static?",
1544 )
1546 def test_avatar_url_default_external(self): # pylint: disable=invalid-name
1547 """
1548 Test fetching avatar for not existing mail with external default specified
1549 This shall *not* redirect to the external site (CWE-601)!
1550 """
1551 default = "http://host.tld/img.png"
1552 size = 80
1553 urlobj = urlsplit(
1554 libravatar_url(
1555 "xxx@xxx.xxx",
1556 size=size,
1557 default=default,
1558 )
1559 )
1560 url = f"{urlobj.path}?{urlobj.query}"
1561 response = self.client.get(url, follow=False)
1562 self.assertRedirects(
1563 response=response,
1564 expected_url=f"/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s={size}",
1565 fetch_redirect_response=False,
1566 msg_prefix="Why does this not redirect to the default img?",
1567 )
1569 def test_avatar_url_default_external_trusted(self): # pylint: disable=invalid-name
1570 """
1571 Test fetching avatar for not existing mail with external default specified
1572 """
1573 default = "https://ui-avatars.com/api/blah"
1574 urlobj = urlsplit(
1575 libravatar_url(
1576 "xxx@xxx.xxx",
1577 size=80,
1578 default=default,
1579 )
1580 )
1581 url = f"{urlobj.path}?{urlobj.query}"
1582 response = self.client.get(url, follow=False)
1583 self.assertRedirects(
1584 response=response,
1585 expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=https://ui-avatars.com/api/blah",
1586 fetch_redirect_response=False,
1587 msg_prefix="Why does this not redirect to the default img?",
1588 )
1590 def test_avatar_url_default_external_gravatarproxy_disabled(
1591 self,
1592 ): # pylint: disable=invalid-name
1593 """
1594 Test fetching avatar for not existing mail with external default specified
1595 This shall *not* redirect to the external site (CWE-601)!
1596 """
1597 default = "http://host.tld/img.png"
1598 urlobj = urlsplit(
1599 libravatar_url(
1600 "xxx@xxx.xxx",
1601 size=80,
1602 default=default,
1603 )
1604 )
1605 url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
1606 response = self.client.get(url, follow=False)
1607 self.assertRedirects(
1608 response=response,
1609 expected_url="/static/img/nobody/80.png",
1610 fetch_redirect_response=False,
1611 msg_prefix="Why does this not redirect to the default img?",
1612 )
1614 def test_crop_photo(self):
1615 """
1616 Test cropping photo
1617 """
1618 self.test_upload_image()
1619 self.test_confirm_email()
1620 url = reverse("crop_photo", args=[self.user.photo_set.first().pk])
1621 response = self.client.post(
1622 url,
1623 {
1624 "x": 10,
1625 "y": 10,
1626 "w": 20,
1627 "h": 20,
1628 },
1629 follow=True,
1630 )
1631 self.assertEqual(response.status_code, 200, "unable to crop?")
1632 self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20))
1633 img = Image.open(BytesIO(self.user.photo_set.first().data))
1634 self.assertEqual(
1635 img.size, (20, 20), "cropped to 20x20, but resulting image isn't 20x20!?"
1636 )
1638 def test_password_change_view(self):
1639 """
1640 Test password change view
1641 """
1642 self.login()
1643 url = reverse("password_change")
1644 response = self.client.get(url)
1645 self.assertEqual(
1646 response.status_code, 200, "unable to view password change view?"
1647 )
1649 def test_password_change_view_post_wrong_old_pw(self):
1650 """
1651 Test password change view post
1652 """
1653 self.login()
1654 response = self.client.post(
1655 reverse("password_change"),
1656 {
1657 "old_password": "xxx",
1658 "new_password1": self.password,
1659 "new_password2": self.password,
1660 },
1661 follow=True,
1662 )
1664 self.assertContains(
1665 response,
1666 "Your old password was entered incorrectly. Please enter it again.",
1667 1,
1668 200,
1669 "Old password as entered incorrectly, site should raise an error",
1670 )
1672 def test_password_change_view_post_wrong_new_password1(self):
1673 """
1674 Test password change view post
1675 """
1676 self.login()
1677 response = self.client.post(
1678 reverse("password_change"),
1679 {
1680 "old_password": self.password,
1681 "new_password1": f"{self.password}.",
1682 "new_password2": self.password,
1683 },
1684 follow=True,
1685 )
1686 self.assertContains(
1687 response,
1688 "The two password fields did",
1689 1,
1690 200,
1691 "Old password was entered incorrectly, site should raise an error",
1692 )
1694 def test_password_change_view_post_wrong_new_password2(self):
1695 """
1696 Test password change view post
1697 """
1698 self.login()
1699 response = self.client.post(
1700 reverse("password_change"),
1701 {
1702 "old_password": self.password,
1703 "new_password1": self.password,
1704 "new_password2": f"{self.password}.",
1705 },
1706 follow=True,
1707 )
1709 self.assertContains(
1710 response,
1711 "The two password fields did",
1712 1,
1713 200,
1714 "Old password as entered incorrectly, site should raise an error",
1715 )
1717 def test_password_change_view_post_common_password(self):
1718 """
1719 Test password change view post
1720 """
1721 self.login()
1722 response = self.client.post(
1723 reverse("password_change"),
1724 {
1725 "old_password": self.password,
1726 "new_password1": "Hallo",
1727 "new_password2": "Hallo",
1728 },
1729 follow=True,
1730 )
1732 self.assertContains(
1733 response,
1734 "This password is too common.",
1735 1,
1736 200,
1737 "Common password, site should raise an error",
1738 )
1740 def test_profile_must_list_first_and_lastname(self):
1741 """
1742 Test if profile view correctly lists first -/last name
1743 """
1744 self.login()
1745 response = self.client.get(reverse("profile"))
1746 self.assertContains(
1747 response,
1748 self.first_name,
1749 1,
1750 200,
1751 "First name not listed in profile page",
1752 )
1753 self.assertContains(
1754 response,
1755 self.last_name,
1756 1,
1757 200,
1758 "Last name not listed in profile page",
1759 )
1760 self.assertContains(
1761 response,
1762 f"{self.first_name} {self.last_name}",
1763 1,
1764 200,
1765 "First and last name not correctly listed in profile page",
1766 )
1768 def test_password_reset_page(self):
1769 """
1770 Just test if the password reset page come up correctly
1771 """
1772 response = self.client.get(reverse("password_reset"))
1773 self.assertEqual(response.status_code, 200, "no 200 ok?")
1775 def test_password_reset_wo_mail(self):
1776 """
1777 Test if the password reset doesn't error out
1778 if the mail address doesn't exist
1779 """
1780 # Avoid sending out mails
1781 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
1783 # Empty database / eliminate existing users
1784 User.objects.all().delete()
1785 url = reverse("password_reset")
1786 response = self.client.post(
1787 url,
1788 {
1789 "email": "asdf@asdf.local",
1790 },
1791 follow=True,
1792 )
1793 self.assertEqual(response.status_code, 200, "password reset page not working?")
1794 self.assertEqual(
1795 len(mail.outbox),
1796 0,
1797 "user does not exist, there should be no mail in the outbox!",
1798 )
1800 def test_password_reset_w_mail(self):
1801 """
1802 Test if the password reset works correctly with email in
1803 User object
1804 """
1805 # Avoid sending out mails
1806 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
1808 url = reverse("password_reset")
1809 # Our test user doesn't have an email address by default - but we need one set
1810 self.user.email = "asdf@asdf.local"
1811 self.user.save()
1812 response = self.client.post(
1813 url,
1814 {
1815 "email": self.user.email,
1816 },
1817 follow=True,
1818 )
1819 self.assertEqual(response.status_code, 200, "password reset page not working?")
1820 self.assertEqual(
1821 len(mail.outbox), 1, "User exists, there should be a mail in the outbox!"
1822 )
1823 self.assertEqual(
1824 mail.outbox[0].to[0],
1825 self.user.email,
1826 "Sending mails to the wrong \
1827 mail address?",
1828 )
1830 def test_password_reset_w_confirmed_mail(self):
1831 """
1832 Test if the password reset works correctly with confirmed
1833 mail
1834 """
1835 # Avoid sending out mails
1836 settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
1838 url = reverse("password_reset")
1839 # Our test user doesn't have a confirmed mail identity - add one
1840 self.user.confirmedemail_set.create(email="asdf@asdf.local")
1841 self.user.save()
1843 response = self.client.post(
1844 url,
1845 {
1846 "email": self.user.confirmedemail_set.first().email,
1847 },
1848 follow=True,
1849 )
1850 # Since the object is touched in another process, we need to refresh it
1851 self.user.refresh_from_db()
1852 self.assertEqual(response.status_code, 200, "password reset page not working?")
1853 self.assertEqual(
1854 self.user.email,
1855 self.user.confirmedemail_set.first().email,
1856 "The password reset view, should have corrected this!",
1857 )
1858 self.assertEqual(
1859 len(mail.outbox), 1, "user exists, there should be a mail in the outbox!"
1860 )
1861 self.assertEqual(
1862 mail.outbox[0].to[0],
1863 self.user.email,
1864 "why are we sending mails to the wrong mail address?",
1865 )
1867 def test_export(self):
1868 """
1869 Test if export works
1870 """
1872 # Create well known strings to check if export
1873 # works as expected
1874 self.user.confirmedemail_set.create(email="asdf@asdf.local")
1875 self.user.confirmedopenid_set.create(openid="http://asdf.asdf.local")
1876 self.user.save()
1878 # Ensure we have a photo uploaded
1879 self.test_upload_image()
1881 self.login()
1882 self.client.get(reverse("export"))
1883 response = self.client.post(
1884 reverse("export"),
1885 {},
1886 follow=False,
1887 )
1888 self.assertIsInstance(response.content, bytes)
1889 fh = gzip.open(BytesIO(response.content), "rb")
1890 content = fh.read()
1891 fh.close()
1892 root = xml.etree.ElementTree.fromstring(content)
1893 self.assertEqual(root.tag, "{%s}user" % settings.SCHEMAROOT)
1894 self.assertEqual(
1895 root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[0][1],
1896 self.user.username,
1897 )
1898 self.assertEqual(
1899 root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[1][1],
1900 self.user.password,
1901 )
1903 self.assertEqual(
1904 root.findall("{%s}emails" % settings.SCHEMAROOT)[0][0].text,
1905 self.user.confirmedemail_set.first().email,
1906 )
1907 self.assertEqual(
1908 root.findall("{%s}openids" % settings.SCHEMAROOT)[0][0].text,
1909 self.user.confirmedopenid_set.first().openid,
1910 )
1912 data = root.findall("{%s}photos" % settings.SCHEMAROOT)[0][0].text
1914 data = data.strip("'")
1915 data = data.strip("\\n")
1916 data = data.lstrip("b'")
1917 bindata = base64.decodebytes(bytes(data, "utf-8"))
1918 image = Image.open(BytesIO(bindata))
1919 self.assertTrue(hasattr(image, "png"))
1921 def test_upload_export(self):
1922 """
1923 Test if uploading export works
1924 """
1926 # Ensure we have data in place
1927 self.test_export()
1929 self.login()
1930 self.client.get(reverse("export"))
1931 response = self.client.post(
1932 reverse("export"),
1933 {},
1934 follow=False,
1935 )
1936 self.assertIsInstance(response.content, bytes)
1938 fh_gzip = gzip.open(BytesIO(response.content), "rb")
1939 fh = BytesIO(response.content)
1941 response = self._uploading_export_check(
1942 fh_gzip, "Unable to parse file: Not a gzipped file"
1943 )
1944 response = self._uploading_export_check(fh, "Choose items to be imported")
1945 self.assertContains(
1946 response,
1947 "asdf@asdf.local",
1948 2,
1949 200,
1950 "Upload didn't work?",
1951 )
1953 def _uploading_export_check(self, fh, message):
1954 """
1955 Helper function to upload an export
1956 """
1957 result = self.client.post(
1958 reverse("upload_export"),
1959 data={"not_porn": "on", "can_distribute": "on", "export_file": fh},
1960 follow=True,
1961 )
1962 fh.close()
1963 self.assertEqual(result.status_code, 200, "Upload worked")
1964 self.assertContains(result, message, 1, 200, "Upload didn't work?")
1966 return result
1968 def test_preferences_page(self):
1969 """
1970 Test if preferences page works
1971 """
1973 self.login()
1974 self.client.get(reverse("user_preference"))
1976 def test_delete_user(self):
1977 """
1978 Test if deleting user profile works
1979 """
1981 self.login()
1982 self.client.get(reverse("delete"))
1983 response = self.client.post(
1984 reverse("delete"),
1985 data={"password": self.password},
1986 follow=True,
1987 )
1988 self.assertEqual(response.status_code, 200, "Deletion worked")
1989 self.assertEqual(User.objects.count(), 0, "No user there any more")
1991 def test_confirm_already_confirmed(self):
1992 """
1993 Try to confirm a mail address that has been confirmed (by another user)
1994 """
1996 # Add mail address (stays unconfirmed)
1997 self.test_add_email()
1999 # Create a second user that will conflict
2000 user2 = User.objects.create_user(
2001 username=f"{self.username}1",
2002 password=self.password,
2003 first_name=self.first_name,
2004 last_name=self.last_name,
2005 )
2006 ConfirmedEmail.objects.create(
2007 email=self.email,
2008 user=user2,
2009 )
2011 # Just to be sure
2012 self.assertEqual(
2013 self.user.unconfirmedemail_set.first().email,
2014 user2.confirmedemail_set.first().email,
2015 "Mail not the same?",
2016 )
2018 # This needs to be caught
2019 with contextlib.suppress(AssertionError):
2020 self.test_confirm_email()
2021 # Request a random page, so we can access the messages
2022 response = self.client.get(reverse("profile"))
2023 self.assertEqual(
2024 str(list(response.context[0]["messages"])[0]),
2025 "This mail address has been taken already and cannot be confirmed",
2026 "This should return an error message!",
2027 )
2030class OpenIDErrorHandlingTestCase(TestCase):
2031 """
2032 Test cases for OpenID error handling and error.html template coverage
2033 """
2035 def setUp(self):
2036 """Set up test user and client"""
2037 self.username = random_string()
2038 self.password = random_string()
2039 self.user = User.objects.create_user(
2040 username=self.username,
2041 password=self.password,
2042 )
2043 self.client = Client()
2045 def login(self):
2046 """Login as test user"""
2047 self.client.login(username=self.username, password=self.password)
2049 def test_openid_discovery_failure_renders_error_template(self):
2050 """
2051 Test that OpenID discovery failure renders error.html template
2052 """
2053 from unittest.mock import patch, MagicMock
2054 from openid.consumer import consumer
2055 from ivatar.ivataraccount.models import UnconfirmedOpenId
2057 self.login()
2059 # Create an unconfirmed OpenID
2060 unconfirmed = UnconfirmedOpenId.objects.create(
2061 user=self.user,
2062 openid="http://invalid-openid-provider.example.com/",
2063 )
2065 # Mock the OpenID consumer to raise DiscoveryFailure
2066 with patch(
2067 "ivatar.ivataraccount.views.consumer.Consumer"
2068 ) as mock_consumer_class:
2069 mock_consumer = MagicMock()
2070 mock_consumer_class.return_value = mock_consumer
2071 # Create a proper DiscoveryFailure with required http_response parameter
2072 mock_response = MagicMock()
2073 mock_response.status_code = 404
2074 discovery_failure = consumer.DiscoveryFailure(
2075 "Invalid provider", mock_response
2076 )
2077 mock_consumer.begin.side_effect = discovery_failure
2079 # Make request to openid_redirection view
2080 response = self.client.get(
2081 reverse("openid_redirection", args=[unconfirmed.id]), follow=True
2082 )
2084 # Verify we get redirected to profile with error message
2085 self.assertEqual(response.status_code, 200)
2086 self.assertRedirects(response, reverse("profile"))
2088 # Check that error message is in the response
2089 messages = list(response.context[0]["messages"])
2090 self.assertTrue(
2091 any("OpenID discovery failed" in str(msg) for msg in messages)
2092 )
2094 def test_openid_confirmation_failure_renders_error_template(self):
2095 """
2096 Test that OpenID confirmation failure renders error.html template
2097 """
2098 from unittest.mock import patch, MagicMock
2099 from openid.consumer import consumer
2100 from ivatar.ivataraccount.models import UnconfirmedOpenId
2102 self.login()
2104 # Create an unconfirmed OpenID
2105 unconfirmed = UnconfirmedOpenId.objects.create(
2106 user=self.user,
2107 openid="http://test-provider.example.com/",
2108 )
2110 # Mock the OpenID consumer to return FAILURE status
2111 with patch(
2112 "ivatar.ivataraccount.views.consumer.Consumer"
2113 ) as mock_consumer_class:
2114 mock_consumer = MagicMock()
2115 mock_consumer_class.return_value = mock_consumer
2117 # Create a mock response with FAILURE status
2118 mock_response = MagicMock()
2119 mock_response.status = consumer.FAILURE
2120 mock_response.message = "Authentication failed"
2121 mock_consumer.complete.return_value = mock_response
2123 # Make request to confirm_openid view
2124 response = self.client.get(
2125 reverse("confirm_openid", args=[unconfirmed.id]), follow=True
2126 )
2128 # Verify we get redirected to profile with error message
2129 self.assertEqual(response.status_code, 200)
2130 self.assertRedirects(response, reverse("profile"))
2132 # Check that error message is in the response
2133 messages = list(response.context[0]["messages"])
2134 self.assertTrue(any("Confirmation failed" in str(msg) for msg in messages))
2136 def test_openid_cancellation_renders_error_template(self):
2137 """
2138 Test that OpenID cancellation renders error.html template
2139 """
2140 from unittest.mock import patch, MagicMock
2141 from openid.consumer import consumer
2142 from ivatar.ivataraccount.models import UnconfirmedOpenId
2144 self.login()
2146 # Create an unconfirmed OpenID
2147 unconfirmed = UnconfirmedOpenId.objects.create(
2148 user=self.user,
2149 openid="http://test-provider.example.com/",
2150 )
2152 # Mock the OpenID consumer to return CANCEL status
2153 with patch(
2154 "ivatar.ivataraccount.views.consumer.Consumer"
2155 ) as mock_consumer_class:
2156 mock_consumer = MagicMock()
2157 mock_consumer_class.return_value = mock_consumer
2159 # Create a mock response with CANCEL status
2160 mock_response = MagicMock()
2161 mock_response.status = consumer.CANCEL
2162 mock_consumer.complete.return_value = mock_response
2164 # Make request to confirm_openid view
2165 response = self.client.get(
2166 reverse("confirm_openid", args=[unconfirmed.id]), follow=True
2167 )
2169 # Verify we get redirected to profile with error message
2170 self.assertEqual(response.status_code, 200)
2171 self.assertRedirects(response, reverse("profile"))
2173 # Check that error message is in the response
2174 messages = list(response.context[0]["messages"])
2175 self.assertTrue(any("Cancelled by user" in str(msg) for msg in messages))
2177 def test_openid_unknown_error_renders_error_template(self):
2178 """
2179 Test that unknown OpenID verification error renders error.html template
2180 """
2181 from unittest.mock import patch, MagicMock
2182 from ivatar.ivataraccount.models import UnconfirmedOpenId
2184 self.login()
2186 # Create an unconfirmed OpenID
2187 unconfirmed = UnconfirmedOpenId.objects.create(
2188 user=self.user,
2189 openid="http://test-provider.example.com/",
2190 )
2192 # Mock the OpenID consumer to return unknown status
2193 with patch(
2194 "ivatar.ivataraccount.views.consumer.Consumer"
2195 ) as mock_consumer_class:
2196 mock_consumer = MagicMock()
2197 mock_consumer_class.return_value = mock_consumer
2199 # Create a mock response with unknown status
2200 mock_response = MagicMock()
2201 mock_response.status = "UNKNOWN_STATUS"
2202 mock_consumer.complete.return_value = mock_response
2204 # Make request to confirm_openid view
2205 response = self.client.get(
2206 reverse("confirm_openid", args=[unconfirmed.id]), follow=True
2207 )
2209 # Verify we get redirected to profile with error message
2210 self.assertEqual(response.status_code, 200)
2211 self.assertRedirects(response, reverse("profile"))
2213 # Check that error message is in the response
2214 messages = list(response.context[0]["messages"])
2215 self.assertTrue(
2216 any("Unknown verification error" in str(msg) for msg in messages)
2217 )
2219 def test_openid_nonexistent_id_error(self):
2220 """
2221 Test that accessing non-existent OpenID ID shows error message
2222 """
2223 self.login()
2225 # Try to access a non-existent OpenID ID
2226 response = self.client.get(
2227 reverse("openid_redirection", args=[99999]), follow=True
2228 )
2230 # Verify we get redirected to profile with error message
2231 self.assertEqual(response.status_code, 200)
2232 self.assertRedirects(response, reverse("profile"))
2234 # Check that error message is in the response
2235 messages = list(response.context[0]["messages"])
2236 self.assertTrue(any("ID does not exist" in str(msg) for msg in messages))
2238 def test_django_openid_auth_failure_template_coverage(self):
2239 """
2240 Test that django-openid-auth failure template uses error.html
2241 This test verifies the OpenID login page renders correctly
2242 """
2243 # Try to access the OpenID login page
2244 response = self.client.get(reverse("openid-login"))
2245 self.assertEqual(response.status_code, 200)
2247 # The login page should render successfully
2248 self.assertContains(response, "OpenID Login")
2250 def test_error_template_direct_rendering(self):
2251 """
2252 Test error.html template directly to ensure it renders correctly
2253 """
2254 from django.test import RequestFactory
2255 from django.template import Context, Template
2256 from django.contrib.auth.models import AnonymousUser
2258 # Test with authenticated user
2259 factory = RequestFactory()
2260 request = factory.get("/")
2261 request.user = self.user
2263 # Test template with error message
2264 template_content = """
2265 {% extends 'error.html' %}
2266 {% load i18n %}
2267 {% block errormessage %}
2268 {% trans 'Test error message:' %} {{ errormessage }}
2269 {% endblock errormessage %}
2270 """
2272 template = Template(template_content)
2273 context = Context(
2274 {
2275 "request": request,
2276 "errormessage": "This is a test error",
2277 "user": self.user,
2278 }
2279 )
2281 rendered = template.render(context)
2283 # Verify the template renders without errors
2284 self.assertIn("Error!", rendered)
2285 self.assertIn("This is a test error", rendered)
2286 # Check for the profile link in the navbar (not in the backlink block)
2287 self.assertIn("/accounts/profile/", rendered)
2289 # Test with anonymous user
2290 request.user = AnonymousUser()
2291 context = Context(
2292 {
2293 "request": request,
2294 "errormessage": "This is a test error",
2295 "user": AnonymousUser(),
2296 }
2297 )
2299 rendered = template.render(context)
2301 # Verify the template renders without errors for anonymous users
2302 self.assertIn("Error!", rendered)
2303 self.assertIn("This is a test error", rendered)
2304 # Should not contain profile link for anonymous users
2305 self.assertNotIn("/accounts/profile/", rendered)
2307 def test_openid_failure_template_inheritance(self):
2308 """
2309 Test that openid/failure.html properly extends error.html
2310 """
2311 from django.test import RequestFactory
2312 from django.template import Context, Template
2314 factory = RequestFactory()
2315 request = factory.get("/")
2316 request.user = self.user
2318 # Test the openid/failure.html template
2319 template_content = """
2320 {% extends 'error.html' %}
2321 {% load i18n %}
2322 {% block errormessage %}
2323 {% trans 'OpenID error:' %} {{ message }}
2324 {% endblock errormessage %}
2325 """
2327 template = Template(template_content)
2328 context = Context(
2329 {
2330 "request": request,
2331 "message": "Authentication failed",
2332 "user": self.user,
2333 }
2334 )
2336 rendered = template.render(context)
2338 # Verify the template renders correctly
2339 self.assertIn("Error!", rendered)
2340 self.assertIn("OpenID error:", rendered)
2341 self.assertIn("Authentication failed", rendered)
2342 # Check for the profile link in the navbar (not in the backlink block)
2343 self.assertIn("/accounts/profile/", rendered)