1. import os
    
  2. 
    
  3. from django.contrib.auth import validators
    
  4. from django.contrib.auth.models import User
    
  5. from django.contrib.auth.password_validation import (
    
  6.     CommonPasswordValidator,
    
  7.     MinimumLengthValidator,
    
  8.     NumericPasswordValidator,
    
  9.     UserAttributeSimilarityValidator,
    
  10.     get_default_password_validators,
    
  11.     get_password_validators,
    
  12.     password_changed,
    
  13.     password_validators_help_text_html,
    
  14.     password_validators_help_texts,
    
  15.     validate_password,
    
  16. )
    
  17. from django.core.exceptions import ValidationError
    
  18. from django.db import models
    
  19. from django.test import SimpleTestCase, TestCase, override_settings
    
  20. from django.test.utils import isolate_apps
    
  21. from django.utils.html import conditional_escape
    
  22. 
    
  23. 
    
  24. @override_settings(
    
  25.     AUTH_PASSWORD_VALIDATORS=[
    
  26.         {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    
  27.         {
    
  28.             "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    
  29.             "OPTIONS": {
    
  30.                 "min_length": 12,
    
  31.             },
    
  32.         },
    
  33.     ]
    
  34. )
    
  35. class PasswordValidationTest(SimpleTestCase):
    
  36.     def test_get_default_password_validators(self):
    
  37.         validators = get_default_password_validators()
    
  38.         self.assertEqual(len(validators), 2)
    
  39.         self.assertEqual(validators[0].__class__.__name__, "CommonPasswordValidator")
    
  40.         self.assertEqual(validators[1].__class__.__name__, "MinimumLengthValidator")
    
  41.         self.assertEqual(validators[1].min_length, 12)
    
  42. 
    
  43.     def test_get_password_validators_custom(self):
    
  44.         validator_config = [
    
  45.             {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}
    
  46.         ]
    
  47.         validators = get_password_validators(validator_config)
    
  48.         self.assertEqual(len(validators), 1)
    
  49.         self.assertEqual(validators[0].__class__.__name__, "CommonPasswordValidator")
    
  50. 
    
  51.         self.assertEqual(get_password_validators([]), [])
    
  52. 
    
  53.     def test_validate_password(self):
    
  54.         self.assertIsNone(validate_password("sufficiently-long"))
    
  55.         msg_too_short = (
    
  56.             "This password is too short. It must contain at least 12 characters."
    
  57.         )
    
  58. 
    
  59.         with self.assertRaises(ValidationError) as cm:
    
  60.             validate_password("django4242")
    
  61.         self.assertEqual(cm.exception.messages, [msg_too_short])
    
  62.         self.assertEqual(cm.exception.error_list[0].code, "password_too_short")
    
  63. 
    
  64.         with self.assertRaises(ValidationError) as cm:
    
  65.             validate_password("password")
    
  66.         self.assertEqual(
    
  67.             cm.exception.messages, ["This password is too common.", msg_too_short]
    
  68.         )
    
  69.         self.assertEqual(cm.exception.error_list[0].code, "password_too_common")
    
  70. 
    
  71.         self.assertIsNone(validate_password("password", password_validators=[]))
    
  72. 
    
  73.     def test_password_changed(self):
    
  74.         self.assertIsNone(password_changed("password"))
    
  75. 
    
  76.     def test_password_changed_with_custom_validator(self):
    
  77.         class Validator:
    
  78.             def password_changed(self, password, user):
    
  79.                 self.password = password
    
  80.                 self.user = user
    
  81. 
    
  82.         user = object()
    
  83.         validator = Validator()
    
  84.         password_changed("password", user=user, password_validators=(validator,))
    
  85.         self.assertIs(validator.user, user)
    
  86.         self.assertEqual(validator.password, "password")
    
  87. 
    
  88.     def test_password_validators_help_texts(self):
    
  89.         help_texts = password_validators_help_texts()
    
  90.         self.assertEqual(len(help_texts), 2)
    
  91.         self.assertIn("12 characters", help_texts[1])
    
  92. 
    
  93.         self.assertEqual(password_validators_help_texts(password_validators=[]), [])
    
  94. 
    
  95.     def test_password_validators_help_text_html(self):
    
  96.         help_text = password_validators_help_text_html()
    
  97.         self.assertEqual(help_text.count("<li>"), 2)
    
  98.         self.assertIn("12 characters", help_text)
    
  99. 
    
  100.     def test_password_validators_help_text_html_escaping(self):
    
  101.         class AmpersandValidator:
    
  102.             def get_help_text(self):
    
  103.                 return "Must contain &"
    
  104. 
    
  105.         help_text = password_validators_help_text_html([AmpersandValidator()])
    
  106.         self.assertEqual(help_text, "<ul><li>Must contain &amp;</li></ul>")
    
  107.         # help_text is marked safe and therefore unchanged by conditional_escape().
    
  108.         self.assertEqual(help_text, conditional_escape(help_text))
    
  109. 
    
  110.     @override_settings(AUTH_PASSWORD_VALIDATORS=[])
    
  111.     def test_empty_password_validator_help_text_html(self):
    
  112.         self.assertEqual(password_validators_help_text_html(), "")
    
  113. 
    
  114. 
    
  115. class MinimumLengthValidatorTest(SimpleTestCase):
    
  116.     def test_validate(self):
    
  117.         expected_error = (
    
  118.             "This password is too short. It must contain at least %d characters."
    
  119.         )
    
  120.         self.assertIsNone(MinimumLengthValidator().validate("12345678"))
    
  121.         self.assertIsNone(MinimumLengthValidator(min_length=3).validate("123"))
    
  122. 
    
  123.         with self.assertRaises(ValidationError) as cm:
    
  124.             MinimumLengthValidator().validate("1234567")
    
  125.         self.assertEqual(cm.exception.messages, [expected_error % 8])
    
  126.         self.assertEqual(cm.exception.error_list[0].code, "password_too_short")
    
  127. 
    
  128.         with self.assertRaises(ValidationError) as cm:
    
  129.             MinimumLengthValidator(min_length=3).validate("12")
    
  130.         self.assertEqual(cm.exception.messages, [expected_error % 3])
    
  131. 
    
  132.     def test_help_text(self):
    
  133.         self.assertEqual(
    
  134.             MinimumLengthValidator().get_help_text(),
    
  135.             "Your password must contain at least 8 characters.",
    
  136.         )
    
  137. 
    
  138. 
    
  139. class UserAttributeSimilarityValidatorTest(TestCase):
    
  140.     def test_validate(self):
    
  141.         user = User.objects.create_user(
    
  142.             username="testclient",
    
  143.             password="password",
    
  144.             email="[email protected]",
    
  145.             first_name="Test",
    
  146.             last_name="Client",
    
  147.         )
    
  148.         expected_error = "The password is too similar to the %s."
    
  149. 
    
  150.         self.assertIsNone(UserAttributeSimilarityValidator().validate("testclient"))
    
  151. 
    
  152.         with self.assertRaises(ValidationError) as cm:
    
  153.             UserAttributeSimilarityValidator().validate("testclient", user=user),
    
  154.         self.assertEqual(cm.exception.messages, [expected_error % "username"])
    
  155.         self.assertEqual(cm.exception.error_list[0].code, "password_too_similar")
    
  156. 
    
  157.         with self.assertRaises(ValidationError) as cm:
    
  158.             UserAttributeSimilarityValidator().validate("example.com", user=user),
    
  159.         self.assertEqual(cm.exception.messages, [expected_error % "email address"])
    
  160. 
    
  161.         with self.assertRaises(ValidationError) as cm:
    
  162.             UserAttributeSimilarityValidator(
    
  163.                 user_attributes=["first_name"],
    
  164.                 max_similarity=0.3,
    
  165.             ).validate("testclient", user=user)
    
  166.         self.assertEqual(cm.exception.messages, [expected_error % "first name"])
    
  167.         # max_similarity=1 doesn't allow passwords that are identical to the
    
  168.         # attribute's value.
    
  169.         with self.assertRaises(ValidationError) as cm:
    
  170.             UserAttributeSimilarityValidator(
    
  171.                 user_attributes=["first_name"],
    
  172.                 max_similarity=1,
    
  173.             ).validate(user.first_name, user=user)
    
  174.         self.assertEqual(cm.exception.messages, [expected_error % "first name"])
    
  175.         # Very low max_similarity is rejected.
    
  176.         msg = "max_similarity must be at least 0.1"
    
  177.         with self.assertRaisesMessage(ValueError, msg):
    
  178.             UserAttributeSimilarityValidator(max_similarity=0.09)
    
  179.         # Passes validation.
    
  180.         self.assertIsNone(
    
  181.             UserAttributeSimilarityValidator(user_attributes=["first_name"]).validate(
    
  182.                 "testclient", user=user
    
  183.             )
    
  184.         )
    
  185. 
    
  186.     @isolate_apps("auth_tests")
    
  187.     def test_validate_property(self):
    
  188.         class TestUser(models.Model):
    
  189.             pass
    
  190. 
    
  191.             @property
    
  192.             def username(self):
    
  193.                 return "foobar"
    
  194. 
    
  195.         with self.assertRaises(ValidationError) as cm:
    
  196.             UserAttributeSimilarityValidator().validate("foobar", user=TestUser()),
    
  197.         self.assertEqual(
    
  198.             cm.exception.messages, ["The password is too similar to the username."]
    
  199.         )
    
  200. 
    
  201.     def test_help_text(self):
    
  202.         self.assertEqual(
    
  203.             UserAttributeSimilarityValidator().get_help_text(),
    
  204.             "Your password can’t be too similar to your other personal information.",
    
  205.         )
    
  206. 
    
  207. 
    
  208. class CommonPasswordValidatorTest(SimpleTestCase):
    
  209.     def test_validate(self):
    
  210.         expected_error = "This password is too common."
    
  211.         self.assertIsNone(CommonPasswordValidator().validate("a-safe-password"))
    
  212. 
    
  213.         with self.assertRaises(ValidationError) as cm:
    
  214.             CommonPasswordValidator().validate("godzilla")
    
  215.         self.assertEqual(cm.exception.messages, [expected_error])
    
  216. 
    
  217.     def test_validate_custom_list(self):
    
  218.         path = os.path.join(
    
  219.             os.path.dirname(os.path.realpath(__file__)), "common-passwords-custom.txt"
    
  220.         )
    
  221.         validator = CommonPasswordValidator(password_list_path=path)
    
  222.         expected_error = "This password is too common."
    
  223.         self.assertIsNone(validator.validate("a-safe-password"))
    
  224. 
    
  225.         with self.assertRaises(ValidationError) as cm:
    
  226.             validator.validate("from-my-custom-list")
    
  227.         self.assertEqual(cm.exception.messages, [expected_error])
    
  228.         self.assertEqual(cm.exception.error_list[0].code, "password_too_common")
    
  229. 
    
  230.     def test_validate_django_supplied_file(self):
    
  231.         validator = CommonPasswordValidator()
    
  232.         for password in validator.passwords:
    
  233.             self.assertEqual(password, password.lower())
    
  234. 
    
  235.     def test_help_text(self):
    
  236.         self.assertEqual(
    
  237.             CommonPasswordValidator().get_help_text(),
    
  238.             "Your password can’t be a commonly used password.",
    
  239.         )
    
  240. 
    
  241. 
    
  242. class NumericPasswordValidatorTest(SimpleTestCase):
    
  243.     def test_validate(self):
    
  244.         expected_error = "This password is entirely numeric."
    
  245.         self.assertIsNone(NumericPasswordValidator().validate("a-safe-password"))
    
  246. 
    
  247.         with self.assertRaises(ValidationError) as cm:
    
  248.             NumericPasswordValidator().validate("42424242")
    
  249.         self.assertEqual(cm.exception.messages, [expected_error])
    
  250.         self.assertEqual(cm.exception.error_list[0].code, "password_entirely_numeric")
    
  251. 
    
  252.     def test_help_text(self):
    
  253.         self.assertEqual(
    
  254.             NumericPasswordValidator().get_help_text(),
    
  255.             "Your password can’t be entirely numeric.",
    
  256.         )
    
  257. 
    
  258. 
    
  259. class UsernameValidatorsTests(SimpleTestCase):
    
  260.     def test_unicode_validator(self):
    
  261.         valid_usernames = ["joe", "René", "ᴮᴵᴳᴮᴵᴿᴰ", "أحمد"]
    
  262.         invalid_usernames = [
    
  263.             "o'connell",
    
  264.             "عبد ال",
    
  265.             "zerowidth\u200Bspace",
    
  266.             "nonbreaking\u00A0space",
    
  267.             "en\u2013dash",
    
  268.             "trailingnewline\u000A",
    
  269.         ]
    
  270.         v = validators.UnicodeUsernameValidator()
    
  271.         for valid in valid_usernames:
    
  272.             with self.subTest(valid=valid):
    
  273.                 v(valid)
    
  274.         for invalid in invalid_usernames:
    
  275.             with self.subTest(invalid=invalid):
    
  276.                 with self.assertRaises(ValidationError):
    
  277.                     v(invalid)
    
  278. 
    
  279.     def test_ascii_validator(self):
    
  280.         valid_usernames = ["glenn", "GLEnN", "jean-marc"]
    
  281.         invalid_usernames = [
    
  282.             "o'connell",
    
  283.             "Éric",
    
  284.             "jean marc",
    
  285.             "أحمد",
    
  286.             "trailingnewline\n",
    
  287.         ]
    
  288.         v = validators.ASCIIUsernameValidator()
    
  289.         for valid in valid_usernames:
    
  290.             with self.subTest(valid=valid):
    
  291.                 v(valid)
    
  292.         for invalid in invalid_usernames:
    
  293.             with self.subTest(invalid=invalid):
    
  294.                 with self.assertRaises(ValidationError):
    
  295.                     v(invalid)