test_validators.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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 SimpleTestCase, 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(SimpleTestCase):
  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_changed_with_custom_validator(self):
  50. class Validator:
  51. def password_changed(self, password, user):
  52. self.password = password
  53. self.user = user
  54. user = object()
  55. validator = Validator()
  56. password_changed('password', user=user, password_validators=(validator,))
  57. self.assertIs(validator.user, user)
  58. self.assertEqual(validator.password, 'password')
  59. def test_password_validators_help_texts(self):
  60. help_texts = password_validators_help_texts()
  61. self.assertEqual(len(help_texts), 2)
  62. self.assertIn('12 characters', help_texts[1])
  63. self.assertEqual(password_validators_help_texts(password_validators=[]), [])
  64. def test_password_validators_help_text_html(self):
  65. help_text = password_validators_help_text_html()
  66. self.assertEqual(help_text.count('<li>'), 2)
  67. self.assertIn('12 characters', help_text)
  68. def test_password_validators_help_text_html_escaping(self):
  69. class AmpersandValidator:
  70. def get_help_text(self):
  71. return 'Must contain &'
  72. help_text = password_validators_help_text_html([AmpersandValidator()])
  73. self.assertEqual(help_text, '<ul><li>Must contain &amp;</li></ul>')
  74. # help_text is marked safe and therefore unchanged by conditional_escape().
  75. self.assertEqual(help_text, conditional_escape(help_text))
  76. @override_settings(AUTH_PASSWORD_VALIDATORS=[])
  77. def test_empty_password_validator_help_text_html(self):
  78. self.assertEqual(password_validators_help_text_html(), '')
  79. class MinimumLengthValidatorTest(SimpleTestCase):
  80. def test_validate(self):
  81. expected_error = "This password is too short. It must contain at least %d characters."
  82. self.assertIsNone(MinimumLengthValidator().validate('12345678'))
  83. self.assertIsNone(MinimumLengthValidator(min_length=3).validate('123'))
  84. with self.assertRaises(ValidationError) as cm:
  85. MinimumLengthValidator().validate('1234567')
  86. self.assertEqual(cm.exception.messages, [expected_error % 8])
  87. self.assertEqual(cm.exception.error_list[0].code, 'password_too_short')
  88. with self.assertRaises(ValidationError) as cm:
  89. MinimumLengthValidator(min_length=3).validate('12')
  90. self.assertEqual(cm.exception.messages, [expected_error % 3])
  91. def test_help_text(self):
  92. self.assertEqual(
  93. MinimumLengthValidator().get_help_text(),
  94. "Your password must contain at least 8 characters."
  95. )
  96. class UserAttributeSimilarityValidatorTest(TestCase):
  97. def test_validate(self):
  98. user = User.objects.create_user(
  99. username='testclient', password='password', email='testclient@example.com',
  100. first_name='Test', last_name='Client',
  101. )
  102. expected_error = "The password is too similar to the %s."
  103. self.assertIsNone(UserAttributeSimilarityValidator().validate('testclient'))
  104. with self.assertRaises(ValidationError) as cm:
  105. UserAttributeSimilarityValidator().validate('testclient', user=user),
  106. self.assertEqual(cm.exception.messages, [expected_error % "username"])
  107. self.assertEqual(cm.exception.error_list[0].code, 'password_too_similar')
  108. with self.assertRaises(ValidationError) as cm:
  109. UserAttributeSimilarityValidator().validate('example.com', user=user),
  110. self.assertEqual(cm.exception.messages, [expected_error % "email address"])
  111. with self.assertRaises(ValidationError) as cm:
  112. UserAttributeSimilarityValidator(
  113. user_attributes=['first_name'],
  114. max_similarity=0.3,
  115. ).validate('testclient', user=user)
  116. self.assertEqual(cm.exception.messages, [expected_error % "first name"])
  117. # max_similarity=1 doesn't allow passwords that are identical to the
  118. # attribute's value.
  119. with self.assertRaises(ValidationError) as cm:
  120. UserAttributeSimilarityValidator(
  121. user_attributes=['first_name'],
  122. max_similarity=1,
  123. ).validate(user.first_name, user=user)
  124. self.assertEqual(cm.exception.messages, [expected_error % "first name"])
  125. # max_similarity=0 rejects all passwords.
  126. with self.assertRaises(ValidationError) as cm:
  127. UserAttributeSimilarityValidator(
  128. user_attributes=['first_name'],
  129. max_similarity=0,
  130. ).validate('XXX', user=user)
  131. self.assertEqual(cm.exception.messages, [expected_error % "first name"])
  132. # Passes validation.
  133. self.assertIsNone(
  134. UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user)
  135. )
  136. @isolate_apps('auth_tests')
  137. def test_validate_property(self):
  138. class TestUser(models.Model):
  139. pass
  140. @property
  141. def username(self):
  142. return 'foobar'
  143. with self.assertRaises(ValidationError) as cm:
  144. UserAttributeSimilarityValidator().validate('foobar', user=TestUser()),
  145. self.assertEqual(cm.exception.messages, ['The password is too similar to the username.'])
  146. def test_help_text(self):
  147. self.assertEqual(
  148. UserAttributeSimilarityValidator().get_help_text(),
  149. "Your password can't be too similar to your other personal information."
  150. )
  151. class CommonPasswordValidatorTest(SimpleTestCase):
  152. def test_validate(self):
  153. expected_error = "This password is too common."
  154. self.assertIsNone(CommonPasswordValidator().validate('a-safe-password'))
  155. with self.assertRaises(ValidationError) as cm:
  156. CommonPasswordValidator().validate('godzilla')
  157. self.assertEqual(cm.exception.messages, [expected_error])
  158. def test_validate_custom_list(self):
  159. path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'common-passwords-custom.txt')
  160. validator = CommonPasswordValidator(password_list_path=path)
  161. expected_error = "This password is too common."
  162. self.assertIsNone(validator.validate('a-safe-password'))
  163. with self.assertRaises(ValidationError) as cm:
  164. validator.validate('from-my-custom-list')
  165. self.assertEqual(cm.exception.messages, [expected_error])
  166. self.assertEqual(cm.exception.error_list[0].code, 'password_too_common')
  167. def test_validate_django_supplied_file(self):
  168. validator = CommonPasswordValidator()
  169. for password in validator.passwords:
  170. self.assertEqual(password, password.lower())
  171. def test_help_text(self):
  172. self.assertEqual(
  173. CommonPasswordValidator().get_help_text(),
  174. "Your password can't be a commonly used password."
  175. )
  176. class NumericPasswordValidatorTest(SimpleTestCase):
  177. def test_validate(self):
  178. expected_error = "This password is entirely numeric."
  179. self.assertIsNone(NumericPasswordValidator().validate('a-safe-password'))
  180. with self.assertRaises(ValidationError) as cm:
  181. NumericPasswordValidator().validate('42424242')
  182. self.assertEqual(cm.exception.messages, [expected_error])
  183. self.assertEqual(cm.exception.error_list[0].code, 'password_entirely_numeric')
  184. def test_help_text(self):
  185. self.assertEqual(
  186. NumericPasswordValidator().get_help_text(),
  187. "Your password can't be entirely numeric."
  188. )
  189. class UsernameValidatorsTests(SimpleTestCase):
  190. def test_unicode_validator(self):
  191. valid_usernames = ['joe', 'René', 'ᴮᴵᴳᴮᴵᴿᴰ', 'أحمد']
  192. invalid_usernames = [
  193. "o'connell", "عبد ال",
  194. "zerowidth\u200Bspace", "nonbreaking\u00A0space",
  195. "en\u2013dash",
  196. ]
  197. v = validators.UnicodeUsernameValidator()
  198. for valid in valid_usernames:
  199. with self.subTest(valid=valid):
  200. v(valid)
  201. for invalid in invalid_usernames:
  202. with self.subTest(invalid=invalid):
  203. with self.assertRaises(ValidationError):
  204. v(invalid)
  205. def test_ascii_validator(self):
  206. valid_usernames = ['glenn', 'GLEnN', 'jean-marc']
  207. invalid_usernames = ["o'connell", 'Éric', 'jean marc', "أحمد"]
  208. v = validators.ASCIIUsernameValidator()
  209. for valid in valid_usernames:
  210. with self.subTest(valid=valid):
  211. v(valid)
  212. for invalid in invalid_usernames:
  213. with self.subTest(invalid=invalid):
  214. with self.assertRaises(ValidationError):
  215. v(invalid)