Files
ivatar/ivatar/ivataraccount/test_views.py

2417 lines
81 KiB
Python

"""
Test our views in ivatar.ivataraccount.views and ivatar.views
"""
import contextlib
# pylint: disable=too-many-lines
from urllib.parse import urlsplit
from io import BytesIO
from contextlib import suppress
import io
import os
import gzip
import xml.etree.ElementTree
import base64
import django
from django.test import TestCase
from django.test import Client
from django.test import override_settings
from django.urls import reverse
from django.core import mail
from django.core.cache import caches
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
import hashlib
from libravatar import libravatar_url
from PIL import Image
os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings"
django.setup()
# pylint: disable=wrong-import-position
from ivatar import settings
from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
from ivatar.ivataraccount.models import Photo, ConfirmedOpenId, ConfirmedEmail
from ivatar.utils import random_string
# pylint: enable=wrong-import-position
TEST_IMAGE_FILE = os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png")
@override_settings()
class Tester(TestCase): # pylint: disable=too-many-public-methods
"""
Main test class
"""
client = Client()
user = None
username = random_string()
password = random_string()
email = "{}@{}.org".format(username, random_string())
# Dunno why random tld doesn't work, but I'm too lazy now to investigate
openid = "http://{}.{}.{}/".format(username, random_string(), "org")
first_name = random_string()
last_name = random_string()
def login(self):
"""
Login as user
"""
self.client.login(username=self.username, password=self.password)
def setUp(self):
"""
Prepare for tests.
- Create user
"""
self.user = User.objects.create_user(
username=self.username,
password=self.password,
first_name=self.first_name,
last_name=self.last_name,
)
# Disable caching
settings.CACHES["default"] = {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
}
caches._settings = None
with suppress(AttributeError):
# clear the existing cache connection
delattr(caches._connections, "default")
def test_new_user(self):
"""
Create a new user
"""
response = self.client.get(reverse("new_account"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
# Empty database / eliminate existing users
User.objects.all().delete()
url = reverse("new_account")
response = self.client.post(
url,
{
"username": self.username,
"password1": self.password,
"password2": self.password,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "unable to create user?")
self.assertEqual(response.context[0]["user"].username, self.username)
def test_new_user_twice(self):
"""
Try to create a user that already exists
"""
response = self.client.get(reverse("new_account"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
# Due to setUp(), we already have this user!
url = reverse("new_account")
response = self.client.post(
url,
{
"username": self.username,
"password1": self.password,
"password2": self.password,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "unable to create user?")
self.assertEqual(response.context[0]["user"].username, "")
self.assertContains(
response,
"A user with that username already exists.",
1,
200,
"can we create a user a second time???",
)
def test_set_password(self):
"""
Change the user password
"""
self.login()
response = self.client.get(reverse("password_set"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
self.password = random_string()
response = self.client.post(
reverse("password_set"),
{
"new_password1": self.password,
"new_password2": self.password,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot change password?")
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"password changed successfully - please login again",
"password change not successful?",
)
self.assertIsNotNone(
authenticate(
username=self.username,
password=self.password,
),
"cannot authenticate with new password!?",
)
self.login()
response = self.client.get(reverse("profile"))
self.assertEqual(response.context[0]["user"].is_anonymous, False)
def test_add_email(self):
"""
Add e-mail address
"""
self.login()
response = self.client.get(reverse("add_email"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot add email?")
self.assertEqual(
len(response.context[0]["messages"]),
1,
"there must not be more or less than ONE (1) message",
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Address added successfully",
"unable to add mail address?",
)
def test_confirm_email(self):
"""
Confirm unconfirmed email
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
unconfirmed = self.user.unconfirmedemail_set.first()
verification_key = unconfirmed.verification_key
url = reverse("confirm_email", args=[verification_key])
response = self.client.get(url)
self.assertEqual(response.status_code, 200, "unable to confirm mail address?")
self.assertEqual(
self.user.unconfirmedemail_set.count(),
0,
"there must not be any unconfirmed address, after confirming it",
)
self.assertEqual(
self.user.confirmedemail_set.count(),
1,
"there must not be more or less than ONE (1) confirmed address!",
)
def test_confirm_email_w_invalid_auth_key(self): # pylint: disable=invalid-name
"""
Test confirmation with invalid auth key
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
url = reverse("confirm_email", args=["x"])
response = self.client.get(url, follow=True)
self.assertEqual(
response.status_code,
200,
"Not able to request confirmation - without verification key?",
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Verification key incorrect",
"Confirm w/o verification key does not produce error message?",
)
def test_confirm_email_w_non_existing_auth_key(
self,
): # pylint: disable=invalid-name
"""
Test confirmation with non existing auth key
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
url = reverse("confirm_email", args=["x" * 64])
response = self.client.get(url, follow=True)
self.assertEqual(
response.status_code,
200,
"Not able to request confirmation - without verification key?",
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Verification key does not exist",
"Confirm w/o non existing key does not produce error message?",
)
def test_remove_confirmed_email(self):
"""
Remove confirmed email
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": self.email,
},
) # Create test address
unconfirmed = self.user.unconfirmedemail_set.first()
verification_key = unconfirmed.verification_key
url = reverse("confirm_email", args=[verification_key])
self.client.get(url) # Confirm
url = reverse(
"remove_confirmed_email", args=[self.user.confirmedemail_set.first().id]
)
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove confirmed address?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Address removed",
"Removing confirmed mail does not work?",
)
def test_remove_not_existing_confirmed_email(self): # pylint: disable=invalid-name
"""
Try removing confirmed mail that doesn't exist
"""
self.login()
url = reverse("remove_confirmed_email", args=[1234])
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "removing email does not redirect to profile?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Address does not exist",
"Removing not existing (confirmed) address, should produce an\
error message!",
)
def test_remove_unconfirmed_email(self):
"""
Remove unconfirmed email
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": self.email,
},
) # Create test address
url = reverse(
"remove_unconfirmed_email", args=[self.user.unconfirmedemail_set.first().id]
)
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove unconfirmed address?"
)
# Take care, since we do not fetch any page now, the message we need
# to check is the _second_ (aka [1], since first is [0])
self.assertEqual(
str(list(response.context[0]["messages"])[1]),
"Address removed",
"Removing unconfirmed mail does not work?",
)
def test_gravatar_photo_import(self):
"""
import photo from Gravatar (with known mail address)
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
response = self.client.post(
reverse("add_email"),
{
"email": "oliver@linux-kernel.at", # Wow, static :-[
},
) # Create test address
unconfirmed = self.user.unconfirmedemail_set.first()
verification_key = unconfirmed.verification_key
url = reverse("confirm_email", args=[verification_key])
self.client.get(url) # Confirm
url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id])
response = self.client.post(
url,
{
"photo_Gravatar": 1,
},
follow=True,
)
self.assertEqual(
response.status_code, 200, "unable to import photo from Gravatar?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Gravatar image successfully imported",
"Importing gravatar photo did not work?",
)
self.assertIsInstance(
self.user.photo_set.first(), Photo, "why is there no Photo (instance)?"
)
def test_raw_image(self):
"""
test raw image view (as seen in profile <img src=
"""
# Ensure we have a photo
self.test_gravatar_photo_import()
response = self.client.get(
reverse("raw_image", args=[self.user.photo_set.first().id])
)
self.assertEqual(response.status_code, 200, "cannot fetch photo?")
# Probably not the best way to access the content type
self.assertEqual(response["Content-Type"], "image/jpg", "Content type wrong!?")
self.assertEqual(
bytes(response.content),
bytes(self.user.photo_set.first().data),
"raw_image should return the same content as if we\
read it directly from the DB",
)
def test_delete_photo(self):
"""
test deleting the photo
"""
# Ensure we have a photo
self.test_gravatar_photo_import()
url = reverse("delete_photo", args=[self.user.photo_set.first().id])
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "deleting photo does not work?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Photo deleted successfully",
"Photo deletion did not work?",
)
def test_delete_non_existing_photo(self):
"""
test deleting the photo
"""
# Ensure we have a photo
self.test_gravatar_photo_import()
url = reverse("delete_photo", args=[1234])
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "post to delete does not work?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"No such image or no permission to delete it",
"Deleting photo that does not exist, should return error message",
)
def test_too_many_unconfirmed_email(self):
"""
Request too many unconfirmed email addresses, make sure we
cannot add more
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
max_num_unconfirmed = getattr(
settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT
)
for i in range(max_num_unconfirmed + 1):
response = self.client.post( # noqa: F841
reverse("add_email"),
{
"email": "%i.%s" % (i, self.email),
},
follow=True,
) # Create test addresses + 1 too much
return self._check_form_validity(
response, "Too many unconfirmed mail addresses!", "__all__"
)
def test_add_mail_address_twice(self):
"""
Request the same mail address two times, should not lead to
having the same address twice
"""
self.login()
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
for _ in range(2):
response = self.client.post( # noqa: F841
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
return self._check_form_validity(
response, "Address already added, currently unconfirmed", "email"
)
def test_add_already_confirmed_email_self(self): # pylint: disable=invalid-name
"""
Request adding mail address that is already confirmed (by someone)
"""
# Create test mail and confirm it, reuse test code
# Should set EMAIL_BACKEND, so no need to do it here
self.test_confirm_email()
response = self.client.post( # noqa: F841
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
return self._check_form_validity(
response, "Address already confirmed (by you)", "email"
)
def test_add_already_confirmed_email_other(self): # pylint: disable=invalid-name
"""
Request adding mail address that is already confirmed (by someone)
"""
# Create test mail and confirm it, reuse test code
# Should set EMAIL_BACKEND, so no need to do it here
self.test_confirm_email()
# Create another user and assign the mail address to that one
# in order to test the correct error message
otheruser = User.objects.create(username="otheruser")
confirmedemail = ConfirmedEmail.objects.last()
confirmedemail.user = otheruser
confirmedemail.save()
response = self.client.post( # noqa: F841
reverse("add_email"),
{
"email": self.email,
},
follow=True,
)
return self._check_form_validity(
response, "Address already confirmed (by someone else)", "email"
)
def test_remove_unconfirmed_non_existing_email(
self,
): # pylint: disable=invalid-name
"""
Remove unconfirmed email that doesn't exist
"""
self.login()
url = reverse("remove_unconfirmed_email", args=[1234])
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove non existing address?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Address does not exist",
"Removing address that does not\
exist, should return error message!",
)
def test_upload_image(
self, test_only_one=True
): # pylint: disable=inconsistent-return-statements
"""
Test uploading image
"""
self.login()
url = reverse("upload_photo")
# rb => Read binary
with open(TEST_IMAGE_FILE, "rb") as photo_file:
photo_data = photo_file.read()
from django.core.files.uploadedfile import SimpleUploadedFile
uploaded_file = SimpleUploadedFile(
"deadbeef.png", photo_data, content_type="image/png"
)
response = self.client.post(
url,
{
"photo": uploaded_file,
"not_porn": True,
"can_distribute": True,
},
follow=True,
)
if not test_only_one:
return response
self.assertEqual(
self.user.photo_set.count(), 1, "there must be exactly one photo now!"
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Successfully uploaded",
"A valid image should return a success message!",
)
self.assertEqual(
self.user.photo_set.first().format,
"png",
"Format must be png, since we uploaded a png!",
)
def test_upload_too_many_images(self):
"""
Test uploading more images than we are allowed
"""
for _ in range(settings.MAX_NUM_PHOTOS + 1):
response = self.test_upload_image(test_only_one=False)
self.assertEqual(
self.user.photo_set.count(),
settings.MAX_NUM_PHOTOS,
"there may not be more photos than allowed!",
)
# Take care we need to check the last message
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Maximum number of photos (%i) reached" % settings.MAX_NUM_PHOTOS,
"Adding more than allowed images, should return error message!",
)
def test_upload_too_big_image(self):
"""
Test uploading image that is too big
"""
self.login()
url = reverse("upload_photo")
# rb => Read binary
response = self.client.post(
url,
{
"photo": io.StringIO("x" * (settings.MAX_PHOTO_SIZE + 1)),
"not_porn": True,
"can_distribute": True,
},
follow=True,
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Image too big",
"Uploading too big image, should return error message!",
)
def test_upload_invalid_image(self):
"""
Test invalid image data
"""
self.login()
url = reverse("upload_photo")
# rb => Read binary
response = self.client.post(
url,
{
"photo": io.StringIO("x"),
"not_porn": True,
"can_distribute": True,
},
follow=True,
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Invalid Format",
"Invalid img data should return error message!",
)
def test_upload_invalid_image_format(self): # pylint: disable=invalid-name
"""
Test if invalid format is correctly detected
"""
self.login()
url = reverse("upload_photo")
# rb => Read binary
with open(os.path.join(settings.STATIC_ROOT, "img", "mm.svg"), "rb") as photo:
response = self.client.post(
url,
{
"photo": photo,
"not_porn": True,
"can_distribute": True,
},
follow=True,
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Invalid Format",
"Invalid img data should return error message!",
)
def test_upload_gif_image(self):
"""
Test if gif is correctly detected and can be viewed
"""
self._extracted_from_test_upload_webp_image_5(
"broken.gif",
"GIF upload failed?!",
"gif",
"Format must be gif, since we uploaded a GIF!",
)
def test_upload_jpg_image(self):
"""
Test if jpg is correctly detected and can be viewed
"""
self._extracted_from_test_upload_webp_image_5(
"broken.jpg",
"JPEG upload failed?!",
"jpg",
"Format must be jpeg, since we uploaded a jpeg!",
)
def test_upload_webp_image(self):
"""
Test if webp is correctly detected and can be viewed
"""
self._extracted_from_test_upload_webp_image_5(
"broken.webp",
"WEBP upload failed?!",
"webp",
"Format must be webp, since we uploaded a webp!",
)
def _extracted_from_test_upload_webp_image_5(
self, filename, message1, format, message2
):
"""
Helper function for common checks for gif, jpg, webp
"""
self.login()
url = reverse("upload_photo")
with open(os.path.join(settings.STATIC_ROOT, "img", filename), "rb") as photo:
response = self.client.post(
url,
{"photo": photo, "not_porn": True, "can_distribute": True},
follow=True,
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Successfully uploaded",
message1,
)
self.assertEqual(self.user.photo_set.first().format, format, message2)
self.test_confirm_email()
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
urlobj = urlsplit(
libravatar_url(email=self.user.confirmedemail_set.first().email)
)
url = f"{urlobj.path}?{urlobj.query}"
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
"""
Test if unsupported format is correctly detected
"""
self.login()
url = reverse("upload_photo")
# rb => Read binary
with open(
os.path.join(settings.STATIC_ROOT, "img", "broken.tif"), "rb"
) as photo:
response = self.client.post(
url,
{
"photo": photo,
"not_porn": True,
"can_distribute": True,
},
follow=True,
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Invalid Format",
"Invalid img data should return error message!",
)
def test_automatic_photo_assign_to_confirmed_mail(
self,
): # pylint: disable=invalid-name
"""
Test if automatic assignment of photo works
"""
self.test_upload_image()
self.test_confirm_email()
confirmed = self.user.confirmedemail_set.first()
self.assertEqual(confirmed.photo, self.user.photo_set.first())
def test_assign_photo_to_email(self):
"""
Test assigning photo to mail address
"""
self.test_confirm_email()
self.test_upload_image()
self.assertIsNone(self.user.confirmedemail_set.first().photo)
url = reverse(
"assign_photo_email", args=[self.user.confirmedemail_set.first().id]
)
# The get is for the view - test context data
self.client.get(
url,
{
"photo_id": self.user.photo_set.first().id,
},
)
# The post is for the actual assigning
response = self.client.post(
url,
{
"photo_id": self.user.photo_set.first().id,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot assign photo?")
self.assertEqual(
self.user.confirmedemail_set.first().photo, self.user.photo_set.first()
)
def test_no_photo_to_email(self):
"""
Test assigning photo to mail address
"""
self.test_confirm_email()
url = reverse(
"assign_photo_email", args=[self.user.confirmedemail_set.first().id]
)
response = self.client.post(
url,
{
"photoNone": True,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot un-assign photo?")
self.assertEqual(self.user.confirmedemail_set.first().photo, None)
def test_assign_photo_to_email_wo_photo_for_testing_template(
self,
): # pylint: disable=invalid-name
"""
Test assign photo template
"""
self.test_confirm_email()
url = reverse(
"assign_photo_email", args=[self.user.confirmedemail_set.first().id]
)
# The get is for the view - test context data
response = self.client.get(url)
self.assertEqual(response.status_code, 200, "cannot fetch page?")
def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name
"""
Test if assigning an invalid photo id returns the correct error message
"""
self.test_confirm_email()
self.test_upload_image()
self.assertIsNone(self.user.confirmedemail_set.first().photo)
url = reverse(
"assign_photo_email", args=[self.user.confirmedemail_set.first().id]
)
response = self.client.post(
url,
{
"photo_id": 1234,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Photo does not exist",
"Assign non existing photo, does not return error message?",
)
def test_post_to_assign_photo_without_photo_id(
self,
): # pylint: disable=invalid-name
"""
Test if assigning photo without id returns the correct error message
"""
self.test_confirm_email()
self.test_upload_image()
self.assertIsNone(self.user.confirmedemail_set.first().photo)
url = reverse(
"assign_photo_email", args=[self.user.confirmedemail_set.first().id]
)
response = self.client.post(url, {}, follow=True)
self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Invalid request [photo_id] missing",
"Assign non existing photo, does not return error message?",
)
def test_assign_photo_to_non_existing_mail(self): # pylint: disable=invalid-name
"""
Test if assigning photo to mail address that doesn't exist returns
the correct error message
"""
self.test_upload_image()
url = reverse("assign_photo_email", args=[1234])
response = self.client.post(
url,
{
"photo_id": self.user.photo_set.first().id,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Invalid request",
"Assign non existing photo, does not return error message?",
)
def test_import_photo_with_non_existing_email(self): # pylint: disable=invalid-name
"""
Test if import with non existing mail address returns
the correct error message
"""
self.login()
url = reverse("import_photo", args=[1234])
response = self.client.post(url, {}, follow=True)
self.assertEqual(response.status_code, 200, "cannot post import photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"Address does not exist",
"Import photo with non existing mail id,\
does not return error message?",
)
def test_import_nothing(self):
"""
Test if importing nothing causes the correct
error message to be returned
"""
self.test_confirm_email()
url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id])
response = self.client.post(url, {}, follow=True)
self.assertEqual(response.status_code, 200, "cannot post import photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Nothing importable",
"Importing with email that does not exist in Gravatar,\
should return an error message!",
)
def _manual_confirm(self):
"""
Helper method to confirm manually, because testing is really hard
"""
# Manual confirm, since testing is _really_ hard!
unconfirmed = self.user.unconfirmedopenid_set.first()
confirmed = ConfirmedOpenId()
confirmed.user = unconfirmed.user
confirmed.ip_address = "127.0.0.1"
confirmed.openid = unconfirmed.openid
confirmed.save()
unconfirmed.delete()
def test_add_openid(self, confirm=True):
"""
Test if adding an OpenID works
"""
self.login()
# Get page
response = self.client.get(reverse("add_openid"))
self.assertEqual(
response.status_code, 200, "Fetching page to add OpenID fails?"
)
response = self.client.post(
reverse("add_openid"),
{
"openid": self.openid,
},
)
self.assertEqual(response.status_code, 302, "OpenID must redirect")
if confirm:
self._manual_confirm()
def test_add_openid_twice(self):
"""
Test if adding OpenID a second time works - it shouldn't
"""
self.login()
# Get page
response = self.client.get(reverse("add_openid"))
self.assertEqual(
response.status_code, 200, "Fetching page to add OpenID fails?"
)
response = self.client.post(
reverse("add_openid"),
{
"openid": self.openid,
},
)
self.assertEqual(response.status_code, 302, "OpenID must redirect")
response = self.client.post(
reverse("add_openid"),
{
"openid": self.openid,
},
follow=True,
)
self.assertEqual(
self.user.unconfirmedopenid_set.count(),
1,
"There must only be one unconfirmed ID!",
)
self._check_form_validity(
response, "OpenID already added, but not confirmed yet!", "openid"
)
# Manual confirm, since testing is _really_ hard!
unconfirmed = self.user.unconfirmedopenid_set.first()
confirmed = ConfirmedOpenId()
confirmed.user = unconfirmed.user
confirmed.ip_address = "127.0.0.1"
confirmed.openid = unconfirmed.openid
confirmed.save()
unconfirmed.delete()
# Try adding it again - although already confirmed
response = self.client.post(
reverse("add_openid"),
{
"openid": self.openid,
},
follow=True,
)
return self._check_form_validity(
response, "OpenID already added and confirmed!", "openid"
)
def _check_form_validity(self, response, message, field):
"""
Helper method to check form, used in several test functions,
deduplicating code
"""
self.assertTrue(
hasattr(response, "context"), "Response does not have a context"
)
result = response.context.get("form")
self.assertIsNotNone(result, "No form found in response context")
self.assertFalse(result.is_valid(), "Form should not be valid")
self.assertIn(message, result.errors.get(field, []))
return result
def test_assign_photo_to_openid(self):
"""
Test assignment of photo to openid
"""
self.test_add_openid()
self.test_upload_image()
self.assertIsNone(self.user.confirmedopenid_set.first().photo)
url = reverse(
"assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
)
# The get is for the view - test context data
self.client.get(
url,
{
"photo_id": self.user.photo_set.first().id,
},
)
# The post is for the actual assigning
response = self.client.post(
url,
{
"photo_id": self.user.photo_set.first().id,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot assign photo?")
self.assertEqual(
self.user.confirmedopenid_set.first().photo, self.user.photo_set.first()
)
def test_assign_photo_to_openid_wo_photo_for_testing_template(
self,
): # pylint: disable=invalid-name
"""
Test openid/photo assignment template
"""
self.test_add_openid()
url = reverse(
"assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200, "cannot fetch page?")
def test_assign_invalid_photo_id_to_openid(self): # pylint: disable=invalid-name
"""
Test assigning invalid photo to openid returns
the correct error message
"""
self.test_add_openid()
self.assertIsNone(self.user.confirmedopenid_set.first().photo)
url = reverse(
"assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
)
response = self.client.post(
url,
{
"photo_id": 1234,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Photo does not exist",
"Assign non existing photo, does not return error message?",
)
def test_post_to_assign_photo_openid_without_photo_id(
self,
): # pylint: disable=invalid-name
"""
Test POST assign photo to openid without photo id
returns the correct error message
"""
self.test_add_openid()
self.test_upload_image()
self.assertIsNone(self.user.confirmedopenid_set.first().photo)
url = reverse(
"assign_photo_openid", args=[self.user.confirmedopenid_set.first().id]
)
response = self.client.post(url, {}, follow=True)
self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Invalid request [photo_id] missing",
"Assign non existing photo, does not return error message?",
)
def test_assign_photo_to_openid_non_existing_openid(
self,
): # pylint: disable=invalid-name
"""
Test assigning photo to openid that doesn't exist
returns the correct error message.
"""
self.test_upload_image()
url = reverse("assign_photo_openid", args=[1234])
response = self.client.post(
url,
{
"photo_id": self.user.photo_set.first().id,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "cannot post assign photo request?")
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"Invalid request",
"Assign non existing photo, does not return error message?",
)
def test_remove_confirmed_openid(self): # pylint: disable=invalid-name
"""
Remove confirmed openid
"""
self.test_add_openid()
url = reverse(
"remove_confirmed_openid", args=[self.user.confirmedopenid_set.first().id]
)
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove confirmed openid?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"ID removed",
"Removing confirmed openid does not work?",
)
def test_remove_not_existing_confirmed_openid(self): # pylint: disable=invalid-name
"""
Try removing confirmed openid that doesn't exist
"""
self.login()
url = reverse("remove_confirmed_openid", args=[1234])
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "removing id does not redirect to profile?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"ID does not exist",
"Removing not existing (confirmed) address, should produce an\
error message!",
)
def test_remove_unconfirmed_openid(self):
"""
Remove unconfirmed openid
"""
self.test_add_openid(confirm=False)
url = reverse(
"remove_unconfirmed_openid",
args=[self.user.unconfirmedopenid_set.first().id],
)
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove unconfirmed address?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[-1]),
"ID removed",
"Removing unconfirmed mail does not work?",
)
def test_remove_unconfirmed_non_existing_openid(
self,
): # pylint: disable=invalid-name
"""
Remove unconfirmed openid that doesn't exist
"""
self.login()
url = reverse("remove_unconfirmed_openid", args=[1234])
response = self.client.post(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove unconfirmed address?"
)
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"ID does not exist",
"Removing an non existing openid should return an error message",
)
def test_openid_redirect_view(self):
"""
Test redirect view
"""
self.test_add_openid(confirm=False)
url = reverse(
"openid_redirection", args=[self.user.unconfirmedopenid_set.first().id]
)
response = self.client.get(url, follow=True)
self.assertEqual(
response.status_code, 200, "unable to remove unconfirmed address?"
)
# self.assertContains(
# response,
# 'OpenID discovery failed: ', 1, 200,
# 'This request must return an error in test mode'
# )
def test_set_photo_on_openid(self):
"""
Test the set_photo function on our ConfirmedOpenId model.
"""
self.test_add_openid()
self.test_upload_image()
self.assertIsNone(self.user.confirmedopenid_set.first().photo)
self.user.confirmedopenid_set.first().set_photo(self.user.photo_set.first())
self.assertEqual(
self.user.confirmedopenid_set.first().photo,
self.user.photo_set.first(),
"set_photo did not work!?",
)
def test_avatar_url_mail(self, do_upload_and_confirm=True, size=(80, 80)):
"""
Test fetching avatar via mail
"""
if do_upload_and_confirm:
self.test_upload_image()
self.test_confirm_email()
urlobj = urlsplit(
libravatar_url(
email=self.user.confirmedemail_set.first().email,
size=size[0],
)
)
url = f"{urlobj.path}?{urlobj.query}"
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
photodata = Image.open(BytesIO(response.content))
self.assertEqual(photodata.size, size, "Why is this not the correct size?")
def test_avatar_url_openid(self):
"""
Test fetching avatar via openid
"""
self.test_assign_photo_to_openid()
urlobj = urlsplit(
libravatar_url(
openid=self.user.confirmedopenid_set.first().openid,
size=80,
)
)
url = f"{urlobj.path}?{urlobj.query}"
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
photodata = Image.open(BytesIO(response.content))
self.assertEqual(photodata.size, (80, 80), "Why is this not the correct size?")
def test_avatar_url_non_existing_mail_digest(self): # pylint: disable=invalid-name
"""
Test fetching avatar via non existing mail digest
"""
self.test_upload_image()
self.test_confirm_email()
urlobj = urlsplit(
libravatar_url(
email=self.user.confirmedemail_set.first().email,
size=80,
)
)
# Simply delete it, then it's digest is 'correct', but
# the hash is no longer there
addr = self.user.confirmedemail_set.first().email
hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest()
self.user.confirmedemail_set.first().delete()
url = f"{urlobj.path}?{urlobj.query}"
self.client.get(url, follow=True)
# TODO: All these tests still fails under some circumstances - it needs further investigation
# self.assertEqual(
# response.redirect_chain[0][0],
# f"/gravatarproxy/{digest}?s=80",
# "Doesn't redirect to Gravatar?",
# )
# self.assertEqual(
# response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
# )
# self.assertEqual(
# response.redirect_chain[1][0],
# f"/avatar/{digest}?s=80&forcedefault=y",
# "Doesn't redirect with default forced on?",
# )
# self.assertEqual(
# response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
# )
# self.assertEqual(
# response.redirect_chain[2][0],
# "/static/img/nobody/80.png",
# "Doesn't redirect to static?",
# )
# self.assertRedirects(
# response=response,
# expected_url="/static/img/nobody/80.png",
# msg_prefix="Why does this not redirect to Gravatar?",
# )
# Eventually one should check if the data is the same
def test_avatar_url_non_existing_mail_digest_gravatarproxy_disabled(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar via non existing mail digest
"""
self.test_upload_image()
self.test_confirm_email()
urlobj = urlsplit(
libravatar_url(
email=self.user.confirmedemail_set.first().email,
size=80,
)
)
# Simply delete it, then it digest is 'correct', but
# the hash is no longer there
self.user.confirmedemail_set.first().delete()
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
response = self.client.get(url, follow=True)
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# self.assertRedirects(
# response=response,
# expected_url="/static/img/nobody/80.png",
# msg_prefix="Why does this not redirect to the default img?",
# )
# Eventually one should check if the data is the same
def test_avatar_url_non_existing_mail_digest_w_default_mm(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar via non existing mail digest and default 'mm'
"""
urlobj = urlsplit(
libravatar_url(
email="asdf@company.local",
size=80,
default="mm",
)
)
url = f"{urlobj.path}?{urlobj.query}"
self.client.get(url, follow=False)
def test_avatar_url_non_existing_mail_digest_w_default_mm_gravatarproxy_disabled(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar via non existing mail digest and default 'mm'
"""
urlobj = urlsplit(
libravatar_url(
email="asdf@company.local",
size=80,
default="mm",
)
)
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
response = self.client.get(url, follow=True)
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/mm/80.png",
"Doesn't redirect to static?",
)
# self.assertRedirects(
# response=response,
# expected_url="/static/img/mm/80.png",
# msg_prefix="Why does this not redirect to the default img?",
# )
# Eventually one should check if the data is the same
def test_avatar_url_non_existing_mail_digest_wo_default(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar via non existing mail digest and default 'mm'
"""
urlobj = urlsplit(
libravatar_url(
email="asdf@company.local",
size=80,
)
)
digest = hashlib.md5("asdf@company.local".lower().encode("utf-8")).hexdigest()
url = f"{urlobj.path}?{urlobj.query}"
response = self.client.get(url, follow=True)
self.assertEqual(
response.redirect_chain[0][0],
f"/gravatarproxy/{digest}?s=80",
"Doesn't redirect to Gravatar?",
)
self.assertEqual(
response.redirect_chain[0][1], 302, "Doesn't redirect with 302?"
)
self.assertEqual(
response.redirect_chain[1][0],
f"/avatar/{digest}?s=80&forcedefault=y",
"Doesn't redirect with default forced on?",
)
self.assertEqual(
response.redirect_chain[1][1], 302, "Doesn't redirect with 302?"
)
self.assertEqual(
response.redirect_chain[2][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# self.assertRedirects(
# response=response,
# expected_url="/static/img/nobody/80.png",
# msg_prefix="Why does this not redirect to the default img?",
# )
# Eventually one should check if the data is the same
def test_avatar_url_non_existing_mail_digest_wo_default_gravatarproxy_disabled(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar via non existing mail digest and default 'mm'
"""
urlobj = urlsplit(
libravatar_url(
email="asdf@company.local",
size=80,
)
)
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
response = self.client.get(url, follow=True)
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/nobody/80.png",
"Doesn't redirect to static?",
)
# self.assertRedirects(
# response=response,
# expected_url="/static/img/nobody/80.png",
# msg_prefix="Why does this not redirect to the default img?",
# )
# Eventually one should check if the data is the same
def test_avatar_url_default(self): # pylint: disable=invalid-name
"""
Test fetching avatar for not existing mail with default specified
"""
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
size=80,
default="/static/img/nobody.png",
)
)
url = f"{urlobj.path}?{urlobj.query}"
url += "&gravatarproxy=n"
response = self.client.get(url, follow=False)
self.assertEqual(response.status_code, 302, "Doesn't redirect with 302?")
self.assertEqual(
response["Location"],
"/static/img/nobody.png",
"Doesn't redirect to static img?",
)
def test_avatar_url_default_gravatarproxy_disabled(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar for not existing mail with default specified
"""
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
size=80,
default="/static/img/nobody.png",
)
)
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
response = self.client.get(url, follow=True)
self.assertEqual(
response.redirect_chain[0][0],
"/static/img/nobody.png",
"Doesn't redirect to static?",
)
def test_avatar_url_default_external(self): # pylint: disable=invalid-name
"""
Test fetching avatar for not existing mail with external default specified
This shall *not* redirect to the external site (CWE-601)!
"""
default = "http://host.tld/img.png"
size = 80
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
size=size,
default=default,
)
)
url = f"{urlobj.path}?{urlobj.query}"
response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url=f"/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s={size}",
fetch_redirect_response=False,
msg_prefix="Why does this not redirect to the default img?",
)
def test_avatar_url_default_external_trusted(self): # pylint: disable=invalid-name
"""
Test fetching avatar for not existing mail with external default specified
"""
default = "https://ui-avatars.com/api/blah"
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
size=80,
default=default,
)
)
url = f"{urlobj.path}?{urlobj.query}"
response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=https://ui-avatars.com/api/blah",
fetch_redirect_response=False,
msg_prefix="Why does this not redirect to the default img?",
)
def test_avatar_url_default_external_gravatarproxy_disabled(
self,
): # pylint: disable=invalid-name
"""
Test fetching avatar for not existing mail with external default specified
This shall *not* redirect to the external site (CWE-601)!
"""
default = "http://host.tld/img.png"
urlobj = urlsplit(
libravatar_url(
"xxx@xxx.xxx",
size=80,
default=default,
)
)
url = f"{urlobj.path}?{urlobj.query}&gravatarproxy=n"
response = self.client.get(url, follow=False)
self.assertRedirects(
response=response,
expected_url="/static/img/nobody/80.png",
fetch_redirect_response=False,
msg_prefix="Why does this not redirect to the default img?",
)
def test_crop_photo(self):
"""
Test cropping photo
"""
self.test_upload_image()
self.test_confirm_email()
url = reverse("crop_photo", args=[self.user.photo_set.first().pk])
response = self.client.post(
url,
{
"x": 10,
"y": 10,
"w": 20,
"h": 20,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "unable to crop?")
self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20))
img = Image.open(BytesIO(self.user.photo_set.first().data))
self.assertEqual(
img.size, (20, 20), "cropped to 20x20, but resulting image isn't 20x20!?"
)
def test_password_change_view(self):
"""
Test password change view
"""
self.login()
url = reverse("password_change")
response = self.client.get(url)
self.assertEqual(
response.status_code, 200, "unable to view password change view?"
)
def test_password_change_view_post_wrong_old_pw(self):
"""
Test password change view post
"""
self.login()
response = self.client.post(
reverse("password_change"),
{
"old_password": "xxx",
"new_password1": self.password,
"new_password2": self.password,
},
follow=True,
)
self.assertContains(
response,
"Your old password was entered incorrectly. Please enter it again.",
1,
200,
"Old password as entered incorrectly, site should raise an error",
)
def test_password_change_view_post_wrong_new_password1(self):
"""
Test password change view post
"""
self.login()
response = self.client.post(
reverse("password_change"),
{
"old_password": self.password,
"new_password1": f"{self.password}.",
"new_password2": self.password,
},
follow=True,
)
self.assertContains(
response,
"The two password fields did",
1,
200,
"Old password was entered incorrectly, site should raise an error",
)
def test_password_change_view_post_wrong_new_password2(self):
"""
Test password change view post
"""
self.login()
response = self.client.post(
reverse("password_change"),
{
"old_password": self.password,
"new_password1": self.password,
"new_password2": f"{self.password}.",
},
follow=True,
)
self.assertContains(
response,
"The two password fields did",
1,
200,
"Old password as entered incorrectly, site should raise an error",
)
def test_password_change_view_post_common_password(self):
"""
Test password change view post
"""
self.login()
response = self.client.post(
reverse("password_change"),
{
"old_password": self.password,
"new_password1": "Hallo",
"new_password2": "Hallo",
},
follow=True,
)
self.assertContains(
response,
"This password is too common.",
1,
200,
"Common password, site should raise an error",
)
def test_profile_must_list_first_and_lastname(self):
"""
Test if profile view correctly lists first -/last name
"""
self.login()
response = self.client.get(reverse("profile"))
self.assertContains(
response,
self.first_name,
1,
200,
"First name not listed in profile page",
)
self.assertContains(
response,
self.last_name,
1,
200,
"Last name not listed in profile page",
)
self.assertContains(
response,
f"{self.first_name} {self.last_name}",
1,
200,
"First and last name not correctly listed in profile page",
)
def test_password_reset_page(self):
"""
Just test if the password reset page come up correctly
"""
response = self.client.get(reverse("password_reset"))
self.assertEqual(response.status_code, 200, "no 200 ok?")
def test_password_reset_wo_mail(self):
"""
Test if the password reset doesn't error out
if the mail address doesn't exist
"""
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# Empty database / eliminate existing users
User.objects.all().delete()
url = reverse("password_reset")
response = self.client.post(
url,
{
"email": "asdf@asdf.local",
},
follow=True,
)
self.assertEqual(response.status_code, 200, "password reset page not working?")
self.assertEqual(
len(mail.outbox),
0,
"user does not exist, there should be no mail in the outbox!",
)
def test_password_reset_w_mail(self):
"""
Test if the password reset works correctly with email in
User object
"""
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
url = reverse("password_reset")
# Our test user doesn't have an email address by default - but we need one set
self.user.email = "asdf@asdf.local"
self.user.save()
response = self.client.post(
url,
{
"email": self.user.email,
},
follow=True,
)
self.assertEqual(response.status_code, 200, "password reset page not working?")
self.assertEqual(
len(mail.outbox), 1, "User exists, there should be a mail in the outbox!"
)
self.assertEqual(
mail.outbox[0].to[0],
self.user.email,
"Sending mails to the wrong \
mail address?",
)
def test_password_reset_w_confirmed_mail(self):
"""
Test if the password reset works correctly with confirmed
mail
"""
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
url = reverse("password_reset")
# Our test user doesn't have a confirmed mail identity - add one
self.user.confirmedemail_set.create(email="asdf@asdf.local")
self.user.save()
response = self.client.post(
url,
{
"email": self.user.confirmedemail_set.first().email,
},
follow=True,
)
# Since the object is touched in another process, we need to refresh it
self.user.refresh_from_db()
self.assertEqual(response.status_code, 200, "password reset page not working?")
self.assertEqual(
self.user.email,
self.user.confirmedemail_set.first().email,
"The password reset view, should have corrected this!",
)
self.assertEqual(
len(mail.outbox), 1, "user exists, there should be a mail in the outbox!"
)
self.assertEqual(
mail.outbox[0].to[0],
self.user.email,
"why are we sending mails to the wrong mail address?",
)
def test_password_reset_w_confirmed_mail_no_password(self):
"""
Test password reset for user with confirmed email but no password set.
This tests the specific case that was failing with Django 4.2+ where
User.objects.make_random_password() was deprecated.
Reproduces the scenario where a user has a confirmed email address
but their password field is empty or starts with "!" (unusable password).
"""
# Avoid sending out mails
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# Create a user with no usable password (starts with "!")
test_user = User.objects.create_user(
username="testuser_no_pass",
email="", # No email in User model
)
# Set an unusable password (starts with "!")
test_user.set_unusable_password()
test_user.save()
# Add a confirmed email (this is the key scenario)
confirmed_email = test_user.confirmedemail_set.create(email="test@example.com")
# Verify the user has no usable password
self.assertTrue(
test_user.password.startswith("!"),
"Test user should have unusable password starting with '!'",
)
url = reverse("password_reset")
# Attempt password reset - this should work without AttributeError
response = self.client.post(
url,
{
"email": confirmed_email.email,
},
follow=True,
)
# Refresh user from database to see changes
test_user.refresh_from_db()
# Verify the request succeeded
self.assertEqual(response.status_code, 200, "password reset page not working?")
# Verify that the user now has a usable password (no longer starts with "!")
self.assertFalse(
test_user.password.startswith("!"),
"User should now have a usable password after reset",
)
# Verify the email was set on the user object
self.assertEqual(
test_user.email,
confirmed_email.email,
"The password reset view should have set the email on user object",
)
# Verify a reset email was sent
self.assertEqual(
len(mail.outbox), 1, "user exists, there should be a mail in the outbox!"
)
self.assertEqual(
mail.outbox[0].to[0],
test_user.email,
"reset email should be sent to the correct address",
)
# Clean up
test_user.delete()
def test_export(self):
"""
Test if export works
"""
# Create well known strings to check if export
# works as expected
self.user.confirmedemail_set.create(email="asdf@asdf.local")
self.user.confirmedopenid_set.create(openid="http://asdf.asdf.local")
self.user.save()
# Ensure we have a photo uploaded
self.test_upload_image()
self.login()
self.client.get(reverse("export"))
response = self.client.post(
reverse("export"),
{},
follow=False,
)
self.assertIsInstance(response.content, bytes)
fh = gzip.open(BytesIO(response.content), "rb")
content = fh.read()
fh.close()
root = xml.etree.ElementTree.fromstring(content)
self.assertEqual(root.tag, "{%s}user" % settings.SCHEMAROOT)
self.assertEqual(
root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[0][1],
self.user.username,
)
self.assertEqual(
root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[1][1],
self.user.password,
)
self.assertEqual(
root.findall("{%s}emails" % settings.SCHEMAROOT)[0][0].text,
self.user.confirmedemail_set.first().email,
)
self.assertEqual(
root.findall("{%s}openids" % settings.SCHEMAROOT)[0][0].text,
self.user.confirmedopenid_set.first().openid,
)
data = root.findall("{%s}photos" % settings.SCHEMAROOT)[0][0].text
data = data.strip("'")
data = data.strip("\\n")
data = data.lstrip("b'")
bindata = base64.decodebytes(bytes(data, "utf-8"))
image = Image.open(BytesIO(bindata))
self.assertTrue(hasattr(image, "png"))
def test_upload_export(self):
"""
Test if uploading export works
"""
# Ensure we have data in place
self.test_export()
self.login()
self.client.get(reverse("export"))
response = self.client.post(
reverse("export"),
{},
follow=False,
)
self.assertIsInstance(response.content, bytes)
fh_gzip = gzip.open(BytesIO(response.content), "rb")
fh = BytesIO(response.content)
response = self._uploading_export_check(
fh_gzip, "Unable to parse file: Not a gzipped file"
)
response = self._uploading_export_check(fh, "Choose items to be imported")
self.assertContains(
response,
"asdf@asdf.local",
2,
200,
"Upload didn't work?",
)
def _uploading_export_check(self, fh, message):
"""
Helper function to upload an export
"""
result = self.client.post(
reverse("upload_export"),
data={"not_porn": "on", "can_distribute": "on", "export_file": fh},
follow=True,
)
fh.close()
self.assertEqual(result.status_code, 200, "Upload worked")
self.assertContains(result, message, 1, 200, "Upload didn't work?")
return result
def test_preferences_page(self):
"""
Test if preferences page works
"""
self.login()
self.client.get(reverse("user_preference"))
def test_delete_user(self):
"""
Test if deleting user profile works
"""
self.login()
self.client.get(reverse("delete"))
response = self.client.post(
reverse("delete"),
data={"password": self.password},
follow=True,
)
self.assertEqual(response.status_code, 200, "Deletion worked")
self.assertEqual(User.objects.count(), 0, "No user there any more")
def test_confirm_already_confirmed(self):
"""
Try to confirm a mail address that has been confirmed (by another user)
"""
# Add mail address (stays unconfirmed)
self.test_add_email()
# Create a second user that will conflict
user2 = User.objects.create_user(
username=f"{self.username}1",
password=self.password,
first_name=self.first_name,
last_name=self.last_name,
)
ConfirmedEmail.objects.create(
email=self.email,
user=user2,
)
# Just to be sure
self.assertEqual(
self.user.unconfirmedemail_set.first().email,
user2.confirmedemail_set.first().email,
"Mail not the same?",
)
# This needs to be caught
with contextlib.suppress(AssertionError):
self.test_confirm_email()
# Request a random page, so we can access the messages
response = self.client.get(reverse("profile"))
self.assertEqual(
str(list(response.context[0]["messages"])[0]),
"This mail address has been taken already and cannot be confirmed",
"This should return an error message!",
)
class OpenIDErrorHandlingTestCase(TestCase):
"""
Test cases for OpenID error handling and error.html template coverage
"""
def setUp(self):
"""Set up test user and client"""
self.username = random_string()
self.password = random_string()
self.user = User.objects.create_user(
username=self.username,
password=self.password,
)
self.client = Client()
def login(self):
"""Login as test user"""
self.client.login(username=self.username, password=self.password)
def test_openid_discovery_failure_renders_error_template(self):
"""
Test that OpenID discovery failure renders error.html template
"""
from unittest.mock import patch, MagicMock
from openid.consumer import consumer
from ivatar.ivataraccount.models import UnconfirmedOpenId
self.login()
# Create an unconfirmed OpenID
unconfirmed = UnconfirmedOpenId.objects.create(
user=self.user,
openid="http://invalid-openid-provider.example.com/",
)
# Mock the OpenID consumer to raise DiscoveryFailure
with patch(
"ivatar.ivataraccount.views.consumer.Consumer"
) as mock_consumer_class:
mock_consumer = MagicMock()
mock_consumer_class.return_value = mock_consumer
# Create a proper DiscoveryFailure with required http_response parameter
mock_response = MagicMock()
mock_response.status_code = 404
discovery_failure = consumer.DiscoveryFailure(
"Invalid provider", mock_response
)
mock_consumer.begin.side_effect = discovery_failure
# Make request to openid_redirection view
response = self.client.get(
reverse("openid_redirection", args=[unconfirmed.id]), follow=True
)
# Verify we get redirected to profile with error message
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, reverse("profile"))
# Check that error message is in the response
messages = list(response.context[0]["messages"])
self.assertTrue(
any("OpenID discovery failed" in str(msg) for msg in messages)
)
def test_openid_confirmation_failure_renders_error_template(self):
"""
Test that OpenID confirmation failure renders error.html template
"""
from unittest.mock import patch, MagicMock
from openid.consumer import consumer
from ivatar.ivataraccount.models import UnconfirmedOpenId
self.login()
# Create an unconfirmed OpenID
unconfirmed = UnconfirmedOpenId.objects.create(
user=self.user,
openid="http://test-provider.example.com/",
)
# Mock the OpenID consumer to return FAILURE status
with patch(
"ivatar.ivataraccount.views.consumer.Consumer"
) as mock_consumer_class:
mock_consumer = MagicMock()
mock_consumer_class.return_value = mock_consumer
# Create a mock response with FAILURE status
mock_response = MagicMock()
mock_response.status = consumer.FAILURE
mock_response.message = "Authentication failed"
mock_consumer.complete.return_value = mock_response
# Make request to confirm_openid view
response = self.client.get(
reverse("confirm_openid", args=[unconfirmed.id]), follow=True
)
# Verify we get redirected to profile with error message
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, reverse("profile"))
# Check that error message is in the response
messages = list(response.context[0]["messages"])
self.assertTrue(any("Confirmation failed" in str(msg) for msg in messages))
def test_openid_cancellation_renders_error_template(self):
"""
Test that OpenID cancellation renders error.html template
"""
from unittest.mock import patch, MagicMock
from openid.consumer import consumer
from ivatar.ivataraccount.models import UnconfirmedOpenId
self.login()
# Create an unconfirmed OpenID
unconfirmed = UnconfirmedOpenId.objects.create(
user=self.user,
openid="http://test-provider.example.com/",
)
# Mock the OpenID consumer to return CANCEL status
with patch(
"ivatar.ivataraccount.views.consumer.Consumer"
) as mock_consumer_class:
mock_consumer = MagicMock()
mock_consumer_class.return_value = mock_consumer
# Create a mock response with CANCEL status
mock_response = MagicMock()
mock_response.status = consumer.CANCEL
mock_consumer.complete.return_value = mock_response
# Make request to confirm_openid view
response = self.client.get(
reverse("confirm_openid", args=[unconfirmed.id]), follow=True
)
# Verify we get redirected to profile with error message
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, reverse("profile"))
# Check that error message is in the response
messages = list(response.context[0]["messages"])
self.assertTrue(any("Cancelled by user" in str(msg) for msg in messages))
def test_openid_unknown_error_renders_error_template(self):
"""
Test that unknown OpenID verification error renders error.html template
"""
from unittest.mock import patch, MagicMock
from ivatar.ivataraccount.models import UnconfirmedOpenId
self.login()
# Create an unconfirmed OpenID
unconfirmed = UnconfirmedOpenId.objects.create(
user=self.user,
openid="http://test-provider.example.com/",
)
# Mock the OpenID consumer to return unknown status
with patch(
"ivatar.ivataraccount.views.consumer.Consumer"
) as mock_consumer_class:
mock_consumer = MagicMock()
mock_consumer_class.return_value = mock_consumer
# Create a mock response with unknown status
mock_response = MagicMock()
mock_response.status = "UNKNOWN_STATUS"
mock_consumer.complete.return_value = mock_response
# Make request to confirm_openid view
response = self.client.get(
reverse("confirm_openid", args=[unconfirmed.id]), follow=True
)
# Verify we get redirected to profile with error message
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, reverse("profile"))
# Check that error message is in the response
messages = list(response.context[0]["messages"])
self.assertTrue(
any("Unknown verification error" in str(msg) for msg in messages)
)
def test_openid_nonexistent_id_error(self):
"""
Test that accessing non-existent OpenID ID shows error message
"""
self.login()
# Try to access a non-existent OpenID ID
response = self.client.get(
reverse("openid_redirection", args=[99999]), follow=True
)
# Verify we get redirected to profile with error message
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, reverse("profile"))
# Check that error message is in the response
messages = list(response.context[0]["messages"])
self.assertTrue(any("ID does not exist" in str(msg) for msg in messages))
def test_django_openid_auth_failure_template_coverage(self):
"""
Test that django-openid-auth failure template uses error.html
This test verifies the OpenID login page renders correctly
"""
# Try to access the OpenID login page
response = self.client.get(reverse("openid-login"))
self.assertEqual(response.status_code, 200)
# The login page should render successfully
self.assertContains(response, "OpenID Login")
def test_error_template_direct_rendering(self):
"""
Test error.html template directly to ensure it renders correctly
"""
from django.test import RequestFactory
from django.template import Context, Template
from django.contrib.auth.models import AnonymousUser
# Test with authenticated user
factory = RequestFactory()
request = factory.get("/")
request.user = self.user
# Test template with error message
template_content = """
{% extends 'error.html' %}
{% load i18n %}
{% block errormessage %}
{% trans 'Test error message:' %} {{ errormessage }}
{% endblock errormessage %}
"""
template = Template(template_content)
context = Context(
{
"request": request,
"errormessage": "This is a test error",
"user": self.user,
}
)
rendered = template.render(context)
# Verify the template renders without errors
self.assertIn("Error!", rendered)
self.assertIn("This is a test error", rendered)
# Check for the profile link in the navbar (not in the backlink block)
self.assertIn("/accounts/profile/", rendered)
# Test with anonymous user
request.user = AnonymousUser()
context = Context(
{
"request": request,
"errormessage": "This is a test error",
"user": AnonymousUser(),
}
)
rendered = template.render(context)
# Verify the template renders without errors for anonymous users
self.assertIn("Error!", rendered)
self.assertIn("This is a test error", rendered)
# Should not contain profile link for anonymous users
self.assertNotIn("/accounts/profile/", rendered)
def test_openid_failure_template_inheritance(self):
"""
Test that openid/failure.html properly extends error.html
"""
from django.test import RequestFactory
from django.template import Context, Template
factory = RequestFactory()
request = factory.get("/")
request.user = self.user
# Test the openid/failure.html template
template_content = """
{% extends 'error.html' %}
{% load i18n %}
{% block errormessage %}
{% trans 'OpenID error:' %} {{ message }}
{% endblock errormessage %}
"""
template = Template(template_content)
context = Context(
{
"request": request,
"message": "Authentication failed",
"user": self.user,
}
)
rendered = template.render(context)
# Verify the template renders correctly
self.assertIn("Error!", rendered)
self.assertIn("OpenID error:", rendered)
self.assertIn("Authentication failed", rendered)
# Check for the profile link in the navbar (not in the backlink block)
self.assertIn("/accounts/profile/", rendered)