2
0

test_validators.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import os
  2. from django.contrib.auth import validators
  3. from django.contrib.auth.models import User
  4. from django.contrib.auth.password_validation import (
  5. CommonPasswordValidator, MinimumLengthValidator, NumericPasswordValidator,
  6. UserAttributeSimilarityValidator, get_default_password_validators,
  7. get_password_validators, password_changed,
  8. password_validators_help_text_html, password_validators_help_texts,
  9. validate_password,
  10. )
  11. from django.core.exceptions import ValidationError
  12. from django.db import models
  13. from django.test import TestCase, override_settings
  14. from django.test.utils import isolate_apps
  15. from django.utils.html import conditional_escape
  16. @override_settings(AUTH_PASSWORD_VALIDATORS=[
  17. {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
  18. {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {
  19. 'min_length': 12,
  20. }},
  21. ])
  22. class PasswordValidationTest(TestCase):
  23. def test_get_default_password_validators(self):
  24. validators = get_default_password_validators()
  25. self.assertEqual(len(validators), 2)
  26. self.assertEqual(validators[0].__class__.__name__, 'CommonPasswordValidator')
  27. self.assertEqual(validators[1].__class__.__name__, 'MinimumLengthValidator')
  28. self.assertEqual(validators[1].min_length, 12)
  29. def test_get_password_validators_custom(self):
  30. validator_config = [{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}]
  31. validators = get_password_validators(validator_config)
  32. self.assertEqual(len(validators), 1)
  33. self.assertEqual(validators[0].__class__.__name__, 'CommonPasswordValidator')
  34. self.assertEqual(get_password_validators([]), [])
  35. def test_validate_password(self):
  36. self.assertIsNone(validate_password('sufficiently-long'))
  37. msg_too_short = 'This password is too short. It must contain at least 12 characters.'
  38. with self.assertRaises(ValidationError) as cm:
  39. validate_password('django4242')
  40. self.assertEqual(cm.exception.messages, [msg_too_short])
  41. self.assertEqual(cm.exception.error_list[0].code, 'password_too_short')
  42. with self.assertRaises(ValidationError) as cm:
  43. validate_password('password')
  44. self.assertEqual(cm.exception.messages, ['This password is too common.', msg_too_short])
  45. self.assertEqual(cm.exception.error_list[0].code, 'password_too_common')
  46. self.assertIsNone(validate_password('password', password_validators=[]))
  47. def test_password_changed(self):
  48. self.assertIsNone(password_changed('password'))
  49. def test_password_validators_help_texts(self):
  50. help_texts = password_validators_help_texts()
  51. self.assertEqual(len(help_texts), 2)
  52. self.assertIn('12 characters', help_texts[1])
  53. self.assertEqual(password_validators_help_texts(password_validators=[]), [])
  54. def test_password_validators_help_text_html(self):
  55. help_text = password_validators_help_text_html()
  56. self.assertEqual(help_text.count('<li>'), 2)
  57. self.assertIn('12 characters', help_text)
  58. def test_password_validators_help_text_html_escaping(self):
  59. class AmpersandValidator:
  60. def get_help_text(self):
  61. return 'Must contain &'
  62. help_text = password_validators_help_text_html([AmpersandValidator()])
  63. self.assertEqual(help_text, '<ul><li>Must contain &amp;</li></ul>')
  64. # help_text is marked safe and therefore unchanged by conditional_escape().
  65. self.assertEqual(help_text, conditional_escape(help_text))
  66. @override_settings(AUTH_PASSWORD_VALIDATORS=[])
  67. def test_empty_password_validator_help_text_html(self):
  68. self.assertEqual(password_validators_help_text_html(), '')
  69. class MinimumLengthValidatorTest(TestCase):
  70. def test_validate(self):
  71. expected_error = "This password is too short. It must contain at least %d characters."
  72. self.assertIsNone(MinimumLengthValidator().validate('12345678'))
  73. self.assertIsNone(MinimumLengthValidator(min_length=3).validate('123'))
  74. with self.assertRaises(ValidationError) as cm:
  75. MinimumLengthValidator().validate('1234567')
  76. self.assertEqual(cm.exception.messages, [expected_error % 8])
  77. self.assertEqual(cm.exception.error_list[0].code, 'password_too_short')
  78. with self.assertRaises(ValidationError) as cm:
  79. MinimumLengthValidator(min_length=3).validate('12')
  80. self.assertEqual(cm.exception.messages, [expected_error % 3])
  81. def test_help_text(self):
  82. self.assertEqual(
  83. MinimumLengthValidator().get_help_text(),
  84. "Your password must contain at least 8 characters."
  85. )
  86. class UserAttributeSimilarityValidatorTest(TestCase):
  87. def test_validate(self):
  88. user = User.objects.create_user(
  89. username='testclient', password='password', email='testclient@example.com',
  90. first_name='Test', last_name='Client',
  91. )
  92. expected_error = "The password is too similar to the %s."
  93. self.assertIsNone(UserAttributeSimilarityValidator().validate('testclient'))
  94. with self.assertRaises(ValidationError) as cm:
  95. UserAttributeSimilarityValidator().validate('testclient', user=user),
  96. self.assertEqual(cm.exception.messages, [expected_error % "username"])
  97. self.assertEqual(cm.exception.error_list[0].code, 'password_too_similar')
  98. with self.assertRaises(ValidationError) as cm:
  99. UserAttributeSimilarityValidator().validate('example.com', user=user),
  100. self.assertEqual(cm.exception.messages, [expected_error % "email address"])
  101. with self.assertRaises(ValidationError) as cm:
  102. UserAttributeSimilarityValidator(
  103. user_attributes=['first_name'],
  104. max_similarity=0.3,
  105. ).validate('testclient', user=user)
  106. self.assertEqual(cm.exception.messages, [expected_error % "first name"])
  107. # max_similarity=1 doesn't allow passwords that are identical to the
  108. # attribute's value.
  109. with self.assertRaises(ValidationError) as cm:
  110. UserAttributeSimilarityValidator(
  111. user_attributes=['first_name'],
  112. max_similarity=1,
  113. ).validate(user.first_name, user=user)
  114. self.assertEqual(cm.exception.messages, [expected_error % "first name"])
  115. # max_similarity=0 rejects all passwords.
  116. with self.assertRaises(ValidationError) as cm:
  117. UserAttributeSimilarityValidator(
  118. user_attributes=['first_name'],
  119. max_similarity=0,
  120. ).validate('XXX', user=user)
  121. self.assertEqual(cm.exception.messages, [expected_error % "first name"])
  122. # Passes validation.
  123. self.assertIsNone(
  124. UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user)
  125. )
  126. @isolate_apps('auth_tests')
  127. def test_validate_property(self):
  128. class TestUser(models.Model):
  129. pass
  130. @property
  131. def username(self):
  132. return 'foobar'
  133. with self.assertRaises(ValidationError) as cm:
  134. UserAttributeSimilarityValidator().validate('foobar', user=TestUser()),
  135. self.assertEqual(cm.exception.messages, ['The password is too similar to the username.'])
  136. def test_help_text(self):
  137. self.assertEqual(
  138. UserAttributeSimilarityValidator().get_help_text(),
  139. "Your password can't be too similar to your other personal information."
  140. )
  141. class CommonPasswordValidatorTest(TestCase):
  142. def test_validate(self):
  143. expected_error = "This password is too common."
  144. self.assertIsNone(CommonPasswordValidator().validate('a-safe-password'))
  145. with self.assertRaises(ValidationError) as cm:
  146. CommonPasswordValidator().validate('godzilla')
  147. self.assertEqual(cm.exception.messages, [expected_error])
  148. def test_validate_custom_list(self):
  149. path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'common-passwords-custom.txt')
  150. validator = CommonPasswordValidator(password_list_path=path)
  151. expected_error = "This password is too common."
  152. self.assertIsNone(validator.validate('a-safe-password'))
  153. with self.assertRaises(ValidationError) as cm:
  154. validator.validate('from-my-custom-list')
  155. self.assertEqual(cm.exception.messages, [expected_error])
  156. self.assertEqual(cm.exception.error_list[0].code, 'password_too_common')
  157. def test_help_text(self):
  158. self.assertEqual(
  159. CommonPasswordValidator().get_help_text(),
  160. "Your password can't be a commonly used password."
  161. )
  162. class NumericPasswordValidatorTest(TestCase):
  163. def test_validate(self):
  164. expected_error = "This password is entirely numeric."
  165. self.assertIsNone(NumericPasswordValidator().validate('a-safe-password'))
  166. with self.assertRaises(ValidationError) as cm:
  167. NumericPasswordValidator().validate('42424242')
  168. self.assertEqual(cm.exception.messages, [expected_error])
  169. self.assertEqual(cm.exception.error_list[0].code, 'password_entirely_numeric')
  170. def test_help_text(self):
  171. self.assertEqual(
  172. NumericPasswordValidator().get_help_text(),
  173. "Your password can't be entirely numeric."
  174. )
  175. class UsernameValidatorsTests(TestCase):
  176. def test_unicode_validator(self):
  177. valid_usernames = ['joe', 'René', 'ᴮᴵᴳᴮᴵᴿᴰ', 'أحمد']
  178. invalid_usernames = [
  179. "o'connell", "عبد ال",
  180. "zerowidth\u200Bspace", "nonbreaking\u00A0space",
  181. "en\u2013dash",
  182. ]
  183. v = validators.UnicodeUsernameValidator()
  184. for valid in valid_usernames:
  185. with self.subTest(valid=valid):
  186. v(valid)
  187. for invalid in invalid_usernames:
  188. with self.subTest(invalid=invalid):
  189. with self.assertRaises(ValidationError):
  190. v(invalid)
  191. def test_ascii_validator(self):
  192. valid_usernames = ['glenn', 'GLEnN', 'jean-marc']
  193. invalid_usernames = ["o'connell", 'Éric', 'jean marc', "أحمد"]
  194. v = validators.ASCIIUsernameValidator()
  195. for valid in valid_usernames:
  196. with self.subTest(valid=valid):
  197. v(valid)
  198. for invalid in invalid_usernames:
  199. with self.subTest(invalid=invalid):
  200. with self.assertRaises(ValidationError):
  201. v(invalid)