tests.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import random
  2. import string
  3. import time
  4. from django.test import TestCase
  5. from django.test.utils import override_settings
  6. from django.contrib.auth.models import User
  7. from django.core.urlresolvers import NoReverseMatch
  8. from django.core.urlresolvers import reverse
  9. from axes.decorators import COOLOFF_TIME
  10. from axes.decorators import FAILURE_LIMIT
  11. from axes.models import AccessLog
  12. from axes.signals import user_locked_out
  13. from axes.utils import reset
  14. class AccessAttemptTest(TestCase):
  15. """Test case using custom settings for testing
  16. """
  17. VALID_USERNAME = 'valid-username'
  18. VALID_PASSWORD = 'valid-password'
  19. LOCKED_MESSAGE = 'Account locked: too many login attempts.'
  20. LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
  21. def _login(self, is_valid_username=False, is_valid_password=False, user_agent='test-browser'):
  22. """Login a user. A valid credential is used when is_valid_username is True,
  23. otherwise it will use a random string to make a failed login.
  24. """
  25. try:
  26. admin_login = reverse('admin:login')
  27. except NoReverseMatch:
  28. admin_login = reverse('admin:index')
  29. if is_valid_username:
  30. # Use a valid username
  31. username = self.VALID_USERNAME
  32. else:
  33. # Generate a wrong random username
  34. chars = string.ascii_uppercase + string.digits
  35. username = ''.join(random.choice(chars) for x in range(10))
  36. if is_valid_password:
  37. password = self.VALID_PASSWORD
  38. else:
  39. password = 'invalid-password'
  40. response = self.client.post(admin_login, {
  41. 'username': username,
  42. 'password': password,
  43. 'this_is_the_login_form': 1,
  44. }, HTTP_USER_AGENT=user_agent)
  45. return response
  46. def setUp(self):
  47. """Create a valid user for login
  48. """
  49. self.user = User.objects.create_superuser(
  50. username=self.VALID_USERNAME,
  51. email='test@example.com',
  52. password=self.VALID_PASSWORD,
  53. )
  54. def test_failure_limit_once(self):
  55. """Tests the login lock trying to login one more time
  56. than failure limit
  57. """
  58. for i in range(1, FAILURE_LIMIT): # test until one try before the limit
  59. response = self._login()
  60. # Check if we are in the same login page
  61. self.assertContains(response, self.LOGIN_FORM_KEY)
  62. # So, we shouldn't have gotten a lock-out yet.
  63. # But we should get one now
  64. response = self._login()
  65. self.assertContains(response, self.LOCKED_MESSAGE)
  66. def test_failure_limit_many(self):
  67. """Tests the login lock trying to login a lot of times more
  68. than failure limit
  69. """
  70. for i in range(1, FAILURE_LIMIT):
  71. response = self._login()
  72. # Check if we are in the same login page
  73. self.assertContains(response, self.LOGIN_FORM_KEY)
  74. # So, we shouldn't have gotten a lock-out yet.
  75. # We should get a locked message each time we try again
  76. for i in range(0, random.randrange(1, FAILURE_LIMIT)):
  77. response = self._login()
  78. self.assertContains(response, self.LOCKED_MESSAGE)
  79. def test_valid_login(self):
  80. """Tests a valid login for a real username
  81. """
  82. response = self._login(is_valid_username=True, is_valid_password=True)
  83. self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
  84. def test_valid_logout(self):
  85. """Tests a valid logout and make sure the logout_time is updated
  86. """
  87. response = self._login(is_valid_username=True, is_valid_password=True)
  88. self.assertEquals(AccessLog.objects.latest('id').logout_time, None)
  89. response = self.client.get(reverse('admin:logout'))
  90. self.assertNotEquals(AccessLog.objects.latest('id').logout_time, None)
  91. self.assertContains(response, 'Logged out')
  92. def test_cooling_off(self):
  93. """Tests if the cooling time allows a user to login
  94. """
  95. self.test_failure_limit_once()
  96. # Wait for the cooling off period
  97. time.sleep(COOLOFF_TIME.total_seconds())
  98. # It should be possible to login again, make sure it is.
  99. self.test_valid_login()
  100. def test_cooling_off_for_trusted_user(self):
  101. """Test the cooling time for a trusted user
  102. """
  103. # Test successful login-logout, this makes the user trusted.
  104. self.test_valid_logout()
  105. # Try the cooling off time
  106. self.test_cooling_off()
  107. def test_long_user_agent_valid(self):
  108. """Tests if can handle a long user agent
  109. """
  110. long_user_agent = 'ie6' * 1024
  111. response = self._login(is_valid_username=True, is_valid_password=True, user_agent=long_user_agent)
  112. self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
  113. def test_long_user_agent_not_valid(self):
  114. """Tests if can handle a long user agent with failure
  115. """
  116. long_user_agent = 'ie6' * 1024
  117. for i in range(0, FAILURE_LIMIT + 1):
  118. response = self._login(user_agent=long_user_agent)
  119. self.assertContains(response, self.LOCKED_MESSAGE)
  120. def test_reset_ip(self):
  121. """Tests if can reset an ip address
  122. """
  123. # Make a lockout
  124. self.test_failure_limit_once()
  125. # Reset the ip so we can try again
  126. reset(ip='127.0.0.1')
  127. # Make a login attempt again
  128. self.test_valid_login()
  129. def test_reset_all(self):
  130. """Tests if can reset all attempts
  131. """
  132. # Make a lockout
  133. self.test_failure_limit_once()
  134. # Reset all attempts so we can try again
  135. reset()
  136. # Make a login attempt again
  137. self.test_valid_login()
  138. def test_send_lockout_signal(self):
  139. """Test if the lockout signal is emitted
  140. """
  141. class Scope(object): pass # this "hack" is needed so we don't have to use global variables or python3 features
  142. scope = Scope()
  143. scope.signal_received = 0
  144. def signal_handler(request, username, ip_address, *args, **kwargs):
  145. scope.signal_received += 1
  146. self.assertIsNotNone(request)
  147. # Connect signal handler
  148. user_locked_out.connect(signal_handler)
  149. # Make a lockout
  150. self.test_failure_limit_once()
  151. self.assertEquals(scope.signal_received, 1)
  152. reset()
  153. # Make another lockout
  154. self.test_failure_limit_once()
  155. self.assertEquals(scope.signal_received, 2)
  156. @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
  157. def test_lockout_by_combination_user_and_ip(self):
  158. """Tests the login lock with a valid username and invalid password
  159. when AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP is True
  160. """
  161. for i in range(1, FAILURE_LIMIT): # test until one try before the limit
  162. response = self._login(is_valid_username=True, is_valid_password=False)
  163. # Check if we are in the same login page
  164. self.assertContains(response, self.LOGIN_FORM_KEY)
  165. # So, we shouldn't have gotten a lock-out yet.
  166. # But we should get one now
  167. response = self._login(is_valid_username=True, is_valid_password=False)
  168. self.assertContains(response, self.LOCKED_MESSAGE)