316 lines
13 KiB
Python
316 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
""" Tests for the account API. """
|
|
|
|
import re
|
|
from unittest import skipUnless
|
|
|
|
from nose.tools import raises
|
|
from mock import patch
|
|
import ddt
|
|
from dateutil.parser import parse as parse_datetime
|
|
from django.core import mail
|
|
from django.test import TestCase
|
|
from django.conf import settings
|
|
|
|
from user_api.api import account as account_api
|
|
from user_api.models import UserProfile
|
|
|
|
|
|
@ddt.ddt
|
|
class AccountApiTest(TestCase):
|
|
|
|
USERNAME = u'frank-underwood'
|
|
PASSWORD = u'ṕáśśẃőŕd'
|
|
EMAIL = u'frank+underwood@example.com'
|
|
|
|
ORIG_HOST = 'example.com'
|
|
IS_SECURE = False
|
|
|
|
INVALID_USERNAMES = [
|
|
None,
|
|
u'',
|
|
u'a',
|
|
u'a' * (account_api.USERNAME_MAX_LENGTH + 1),
|
|
u'invalid_symbol_@',
|
|
u'invalid-unicode_fŕáńḱ',
|
|
]
|
|
|
|
INVALID_EMAILS = [
|
|
None,
|
|
u'',
|
|
u'a',
|
|
'no_domain',
|
|
'no+domain',
|
|
'@',
|
|
'@domain.com',
|
|
'test@no_extension',
|
|
u'fŕáńḱ@example.com',
|
|
u'frank@éxáḿṕĺé.ćőḿ',
|
|
|
|
# Long email -- subtract the length of the @domain
|
|
# except for one character (so we exceed the max length limit)
|
|
u'{user}@example.com'.format(
|
|
user=(u'e' * (account_api.EMAIL_MAX_LENGTH - 11))
|
|
)
|
|
]
|
|
|
|
INVALID_PASSWORDS = [
|
|
None,
|
|
u'',
|
|
u'a',
|
|
u'a' * (account_api.PASSWORD_MAX_LENGTH + 1)
|
|
]
|
|
|
|
def test_activate_account(self):
|
|
# Create the account, which is initially inactive
|
|
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
account = account_api.account_info(self.USERNAME)
|
|
self.assertEqual(account, {
|
|
'username': self.USERNAME,
|
|
'email': self.EMAIL,
|
|
'is_active': False
|
|
})
|
|
|
|
# Activate the account and verify that it is now active
|
|
account_api.activate_account(activation_key)
|
|
account = account_api.account_info(self.USERNAME)
|
|
self.assertTrue(account['is_active'])
|
|
|
|
def test_change_email(self):
|
|
# Request an email change
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
activation_key = account_api.request_email_change(
|
|
self.USERNAME, u'new+email@example.com', self.PASSWORD
|
|
)
|
|
|
|
# Verify that the email has not yet changed
|
|
account = account_api.account_info(self.USERNAME)
|
|
self.assertEqual(account['email'], self.EMAIL)
|
|
|
|
# Confirm the change, using the activation code
|
|
old_email, new_email = account_api.confirm_email_change(activation_key)
|
|
self.assertEqual(old_email, self.EMAIL)
|
|
self.assertEqual(new_email, u'new+email@example.com')
|
|
|
|
# Verify that the email is changed
|
|
account = account_api.account_info(self.USERNAME)
|
|
self.assertEqual(account['email'], u'new+email@example.com')
|
|
|
|
def test_confirm_email_change_repeat(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
activation_key = account_api.request_email_change(
|
|
self.USERNAME, u'new+email@example.com', self.PASSWORD
|
|
)
|
|
|
|
# Confirm the change once
|
|
account_api.confirm_email_change(activation_key)
|
|
|
|
# Confirm the change again. The activation code should be
|
|
# single-use, so this should raise an error.
|
|
with self.assertRaises(account_api.AccountNotAuthorized):
|
|
account_api.confirm_email_change(activation_key)
|
|
|
|
def test_create_account_duplicate_username(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
with self.assertRaises(account_api.AccountUserAlreadyExists):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, 'different+email@example.com')
|
|
|
|
# Email uniqueness constraints were introduced in a database migration,
|
|
# which we disable in the unit tests to improve the speed of the test suite.
|
|
@skipUnless(settings.SOUTH_TESTS_MIGRATE, "South migrations required")
|
|
def test_create_account_duplicate_email(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
with self.assertRaises(account_api.AccountUserAlreadyExists):
|
|
account_api.create_account('different_user', self.PASSWORD, self.EMAIL)
|
|
|
|
def test_username_too_long(self):
|
|
long_username = 'e' * (account_api.USERNAME_MAX_LENGTH + 1)
|
|
with self.assertRaises(account_api.AccountUsernameInvalid):
|
|
account_api.create_account(long_username, self.PASSWORD, self.EMAIL)
|
|
|
|
def test_account_info_no_user(self):
|
|
self.assertIs(account_api.account_info('does_not_exist'), None)
|
|
|
|
@raises(account_api.AccountEmailInvalid)
|
|
@ddt.data(*INVALID_EMAILS)
|
|
def test_create_account_invalid_email(self, invalid_email):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, invalid_email)
|
|
|
|
@raises(account_api.AccountPasswordInvalid)
|
|
@ddt.data(*INVALID_PASSWORDS)
|
|
def test_create_account_invalid_password(self, invalid_password):
|
|
account_api.create_account(self.USERNAME, invalid_password, self.EMAIL)
|
|
|
|
@raises(account_api.AccountPasswordInvalid)
|
|
def test_create_account_username_password_equal(self):
|
|
# Username and password cannot be the same
|
|
account_api.create_account(self.USERNAME, self.USERNAME, self.EMAIL)
|
|
|
|
@raises(account_api.AccountRequestError)
|
|
@ddt.data(*INVALID_USERNAMES)
|
|
def test_create_account_invalid_username(self, invalid_username):
|
|
account_api.create_account(invalid_username, self.PASSWORD, self.EMAIL)
|
|
|
|
@raises(account_api.AccountNotAuthorized)
|
|
def test_activate_account_invalid_key(self):
|
|
account_api.activate_account(u'invalid')
|
|
|
|
@raises(account_api.AccountUserNotFound)
|
|
def test_request_email_change_no_user(self):
|
|
account_api.request_email_change(u'no_such_user', self.EMAIL, self.PASSWORD)
|
|
|
|
@ddt.data(*INVALID_EMAILS)
|
|
def test_request_email_change_invalid_email(self, invalid_email):
|
|
# Create an account with a valid email address
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
|
|
# Attempt to change the account to an invalid email
|
|
with self.assertRaises(account_api.AccountEmailInvalid):
|
|
account_api.request_email_change(self.USERNAME, invalid_email, self.PASSWORD)
|
|
|
|
def test_request_email_change_already_exists(self):
|
|
# Create two accounts, both activated
|
|
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
account_api.activate_account(activation_key)
|
|
activation_key = account_api.create_account(u'another_user', u'password', u'another+user@example.com')
|
|
account_api.activate_account(activation_key)
|
|
|
|
# Try to change the first user's email to the same as the second user's
|
|
with self.assertRaises(account_api.AccountEmailAlreadyExists):
|
|
account_api.request_email_change(self.USERNAME, u'another+user@example.com', self.PASSWORD)
|
|
|
|
def test_request_email_change_duplicates_unactivated_account(self):
|
|
# Create two accounts, but the second account is inactive
|
|
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
account_api.activate_account(activation_key)
|
|
account_api.create_account(u'another_user', u'password', u'another+user@example.com')
|
|
|
|
# Try to change the first user's email to the same as the second user's
|
|
# Since the second user has not yet activated, this should succeed.
|
|
account_api.request_email_change(self.USERNAME, u'another+user@example.com', self.PASSWORD)
|
|
|
|
def test_request_email_change_same_address(self):
|
|
# Create and activate the account
|
|
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
account_api.activate_account(activation_key)
|
|
|
|
# Try to change the email address to the current address
|
|
with self.assertRaises(account_api.AccountEmailAlreadyExists):
|
|
account_api.request_email_change(self.USERNAME, self.EMAIL, self.PASSWORD)
|
|
|
|
def test_request_email_change_wrong_password(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
|
|
# Use the wrong password
|
|
with self.assertRaises(account_api.AccountNotAuthorized):
|
|
account_api.request_email_change(self.USERNAME, u'new+email@example.com', u'wrong password')
|
|
|
|
def test_confirm_email_change_invalid_activation_key(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
account_api.request_email_change(self.USERNAME, u'new+email@example.com', self.PASSWORD)
|
|
|
|
with self.assertRaises(account_api.AccountNotAuthorized):
|
|
account_api.confirm_email_change(u'invalid')
|
|
|
|
def test_confirm_email_change_no_request_pending(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
|
|
def test_confirm_email_already_exists(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
|
|
# Request a change
|
|
activation_key = account_api.request_email_change(
|
|
self.USERNAME, u'new+email@example.com', self.PASSWORD
|
|
)
|
|
|
|
# Another use takes the email before we confirm the change
|
|
account_api.create_account(u'other_user', u'password', u'new+email@example.com')
|
|
|
|
# When we try to confirm our change, we get an error because the email is taken
|
|
with self.assertRaises(account_api.AccountEmailAlreadyExists):
|
|
account_api.confirm_email_change(activation_key)
|
|
|
|
# Verify that the email was NOT changed
|
|
self.assertEqual(account_api.account_info(self.USERNAME)['email'], self.EMAIL)
|
|
|
|
def test_confirm_email_no_user_profile(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
activation_key = account_api.request_email_change(
|
|
self.USERNAME, u'new+email@example.com', self.PASSWORD
|
|
)
|
|
|
|
# This should never happen, but just in case...
|
|
UserProfile.objects.get(user__username=self.USERNAME).delete()
|
|
|
|
with self.assertRaises(account_api.AccountInternalError):
|
|
account_api.confirm_email_change(activation_key)
|
|
|
|
def test_record_email_change_history(self):
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
|
|
# Change the email once
|
|
activation_key = account_api.request_email_change(
|
|
self.USERNAME, u'new+email@example.com', self.PASSWORD
|
|
)
|
|
account_api.confirm_email_change(activation_key)
|
|
|
|
# Verify that the old email appears in the history
|
|
meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta()
|
|
self.assertEqual(len(meta['old_emails']), 1)
|
|
email, timestamp = meta['old_emails'][0]
|
|
self.assertEqual(email, self.EMAIL)
|
|
self._assert_is_datetime(timestamp)
|
|
|
|
# Change the email again
|
|
activation_key = account_api.request_email_change(
|
|
self.USERNAME, u'another_new+email@example.com', self.PASSWORD
|
|
)
|
|
account_api.confirm_email_change(activation_key)
|
|
|
|
# Verify that both emails appear in the history
|
|
meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta()
|
|
self.assertEqual(len(meta['old_emails']), 2)
|
|
email, timestamp = meta['old_emails'][1]
|
|
self.assertEqual(email, 'new+email@example.com')
|
|
self._assert_is_datetime(timestamp)
|
|
|
|
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
|
|
def test_request_password_change(self):
|
|
# Create and activate an account
|
|
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
account_api.activate_account(activation_key)
|
|
|
|
# Request a password change
|
|
account_api.request_password_change(self.EMAIL, self.ORIG_HOST, self.IS_SECURE)
|
|
|
|
# Verify that one email message has been sent
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
|
|
# Verify that the body of the message contains something that looks
|
|
# like an activation link
|
|
email_body = mail.outbox[0].body
|
|
result = re.search('(?P<url>https?://[^\s]+)', email_body)
|
|
self.assertIsNot(result, None)
|
|
|
|
@raises(account_api.AccountUserNotFound)
|
|
@ddt.data(True, False)
|
|
def test_request_password_change_invalid_user(self, create_inactive_account):
|
|
if create_inactive_account:
|
|
# Create an account, but do not activate it
|
|
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
|
|
|
|
account_api.request_password_change(self.EMAIL, self.ORIG_HOST, self.IS_SECURE)
|
|
|
|
# Verify that no email messages have been sent
|
|
self.assertEqual(len(mail.outbox), 0)
|
|
|
|
def _assert_is_datetime(self, timestamp):
|
|
if not timestamp:
|
|
return False
|
|
try:
|
|
parse_datetime(timestamp)
|
|
except ValueError:
|
|
return False
|
|
else:
|
|
return True
|