tests.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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 AccessAttempt, 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', **kwargs):
  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. post_data = {
  41. 'username': username,
  42. 'password': password,
  43. 'this_is_the_login_form': 1,
  44. }
  45. post_data.update(kwargs)
  46. response = self.client.post(admin_login, post_data, HTTP_USER_AGENT=user_agent)
  47. return response
  48. def setUp(self):
  49. """Create a valid user for login
  50. """
  51. self.user = User.objects.create_superuser(
  52. username=self.VALID_USERNAME,
  53. email='test@example.com',
  54. password=self.VALID_PASSWORD,
  55. )
  56. def test_failure_limit_once(self):
  57. """Tests the login lock trying to login one more time
  58. than failure limit
  59. """
  60. for i in range(1, FAILURE_LIMIT): # test until one try before the limit
  61. response = self._login()
  62. # Check if we are in the same login page
  63. self.assertContains(response, self.LOGIN_FORM_KEY)
  64. # So, we shouldn't have gotten a lock-out yet.
  65. # But we should get one now
  66. response = self._login()
  67. self.assertContains(response, self.LOCKED_MESSAGE)
  68. def test_failure_limit_many(self):
  69. """Tests the login lock trying to login a lot of times more
  70. than failure limit
  71. """
  72. for i in range(1, FAILURE_LIMIT):
  73. response = self._login()
  74. # Check if we are in the same login page
  75. self.assertContains(response, self.LOGIN_FORM_KEY)
  76. # So, we shouldn't have gotten a lock-out yet.
  77. # We should get a locked message each time we try again
  78. for i in range(0, random.randrange(1, FAILURE_LIMIT)):
  79. response = self._login()
  80. self.assertContains(response, self.LOCKED_MESSAGE)
  81. def test_valid_login(self):
  82. """Tests a valid login for a real username
  83. """
  84. response = self._login(is_valid_username=True, is_valid_password=True)
  85. self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
  86. def test_valid_logout(self):
  87. """Tests a valid logout and make sure the logout_time is updated
  88. """
  89. response = self._login(is_valid_username=True, is_valid_password=True)
  90. self.assertEquals(AccessLog.objects.latest('id').logout_time, None)
  91. response = self.client.get(reverse('admin:logout'))
  92. self.assertNotEquals(AccessLog.objects.latest('id').logout_time, None)
  93. self.assertContains(response, 'Logged out')
  94. def test_cooling_off(self):
  95. """Tests if the cooling time allows a user to login
  96. """
  97. self.test_failure_limit_once()
  98. # Wait for the cooling off period
  99. time.sleep(COOLOFF_TIME.total_seconds())
  100. # It should be possible to login again, make sure it is.
  101. self.test_valid_login()
  102. def test_cooling_off_for_trusted_user(self):
  103. """Test the cooling time for a trusted user
  104. """
  105. # Test successful login-logout, this makes the user trusted.
  106. self.test_valid_logout()
  107. # Try the cooling off time
  108. self.test_cooling_off()
  109. def test_long_user_agent_valid(self):
  110. """Tests if can handle a long user agent
  111. """
  112. long_user_agent = 'ie6' * 1024
  113. response = self._login(is_valid_username=True, is_valid_password=True, user_agent=long_user_agent)
  114. self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
  115. def test_long_user_agent_not_valid(self):
  116. """Tests if can handle a long user agent with failure
  117. """
  118. long_user_agent = 'ie6' * 1024
  119. for i in range(0, FAILURE_LIMIT + 1):
  120. response = self._login(user_agent=long_user_agent)
  121. self.assertContains(response, self.LOCKED_MESSAGE)
  122. def test_reset_ip(self):
  123. """Tests if can reset an ip address
  124. """
  125. # Make a lockout
  126. self.test_failure_limit_once()
  127. # Reset the ip so we can try again
  128. reset(ip='127.0.0.1')
  129. # Make a login attempt again
  130. self.test_valid_login()
  131. def test_reset_all(self):
  132. """Tests if can reset all attempts
  133. """
  134. # Make a lockout
  135. self.test_failure_limit_once()
  136. # Reset all attempts so we can try again
  137. reset()
  138. # Make a login attempt again
  139. self.test_valid_login()
  140. def test_send_lockout_signal(self):
  141. """Test if the lockout signal is emitted
  142. """
  143. class Scope(object): pass # this "hack" is needed so we don't have to use global variables or python3 features
  144. scope = Scope()
  145. scope.signal_received = 0
  146. def signal_handler(request, username, ip_address, *args, **kwargs):
  147. scope.signal_received += 1
  148. self.assertIsNotNone(request)
  149. # Connect signal handler
  150. user_locked_out.connect(signal_handler)
  151. # Make a lockout
  152. self.test_failure_limit_once()
  153. self.assertEquals(scope.signal_received, 1)
  154. reset()
  155. # Make another lockout
  156. self.test_failure_limit_once()
  157. self.assertEquals(scope.signal_received, 2)
  158. @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
  159. def test_lockout_by_combination_user_and_ip(self):
  160. """Tests the login lock with a valid username and invalid password
  161. when AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP is True
  162. """
  163. for i in range(1, FAILURE_LIMIT): # test until one try before the limit
  164. response = self._login(is_valid_username=True, is_valid_password=False)
  165. # Check if we are in the same login page
  166. self.assertContains(response, self.LOGIN_FORM_KEY)
  167. # So, we shouldn't have gotten a lock-out yet.
  168. # But we should get one now
  169. response = self._login(is_valid_username=True, is_valid_password=False)
  170. self.assertContains(response, self.LOCKED_MESSAGE)
  171. def test_log_data_truncated(self):
  172. """Tests that query2str properly truncates data to the max_length (default 1024)
  173. """
  174. extra_data = {string.ascii_letters * x: x for x in range(0, 1000)} # An impossibly large post dict
  175. self._login(**extra_data)
  176. self.assertEquals(len(AccessAttempt.objects.latest('id').post_data), 1024)