# -*- 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('(?Phttps?://[^\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