Browse Source

User validators (without ban checks)

Rafał Pitoń 11 years ago
parent
commit
aad6a7accd

+ 18 - 2
docs/developers/validators.rst

@@ -2,11 +2,15 @@
 Misago Validators
 Misago Validators
 =================
 =================
 
 
-Misago apps implement plenty of validators, someof which are considered public API. Those validators are documented here.
+Misago apps implement plenty of validators, some of which are considered public API. Those validators are per convention contained within ``validators`` module of their respective apps.
+
+
+misago.core.validators
+======================
 
 
 
 
 validate_sluggable
 validate_sluggable
-==================
+------------------
 
 
 :py:class:`misago.core.validators.validate_sluggable`
 :py:class:`misago.core.validators.validate_sluggable`
 
 
@@ -17,3 +21,15 @@ To you use it, first instantiate it. If you want to define custom error messages
     from misago.core.validators import validate_sluggable
     from misago.core.validators import validate_sluggable
     validator = validate_sluggable()
     validator = validate_sluggable()
     validator(some_value)
     validator(some_value)
+
+
+misago.users.validators
+=======================
+
+
+validate_username
+-----------------
+
+:py:class:`misago.users.validators.validate_username`
+
+Function that takes username and runs content, length, availability and ban check validation in this order via calling dedicated validators.

+ 1 - 1
misago/users/migrations/0002_db_settings.py

@@ -70,7 +70,7 @@ class Migration(DataMigration):
                         'name': _("Minimum user password length"),
                         'name': _("Minimum user password length"),
                         'legend': _("Passwords"),
                         'legend': _("Passwords"),
                         'python_type': 'int',
                         'python_type': 'int',
-                        'value': 3,
+                        'value': 5,
                         'field_extra': {
                         'field_extra': {
                             'min_value': 2,
                             'min_value': 2,
                             'max_value': 255,
                             'max_value': 255,

+ 92 - 15
misago/users/tests/test_validators.py

@@ -3,10 +3,93 @@ from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.test import TestCase
 from django.test import TestCase
 from misago.conf import settings
 from misago.conf import settings
-from misago.users.validators import (validate_username_available,
+from misago.users.validators import (validate_email, validate_email_available,
+                                     validate_password,
+                                     validate_password_complexity,
+                                     validate_password_length,
+                                     _validate_password_alphanumerics,
+                                     _validate_password_case,
+                                     _validate_password_special,
+                                     validate_username,
+                                     validate_username_available,
                                      validate_username_content,
                                      validate_username_content,
-                                     validate_username_length,
-                                     validate_username)
+                                     validate_username_length)
+
+
+class ValidateEmailAvailableTests(TestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.test_user = User.objects.create_user('EricTheFish',
+                                                  'eric@test.com',
+                                                  'pass123')
+
+    def test_valid_email(self):
+        """validate_email_available allows available emails"""
+        validate_email_available('bob@boberson.com')
+
+    def test_invalid_email(self):
+        """validate_email_available disallows unvailable emails"""
+        with self.assertRaises(ValidationError):
+            validate_email_available(self.test_user.email)
+
+
+class ValidateEmailTests(TestCase):
+    def test_validate_email(self):
+        """validate_email has no crashes"""
+        validate_email('bob@boberson.com')
+        with self.assertRaises(ValidationError):
+            validate_email('*')
+
+
+class ValidatePasswordTests(TestCase):
+    def test_validate_password(self):
+        """validate_password has no crashes"""
+        validate_password('Bobbins.1')
+        with self.assertRaises(ValidationError):
+            validate_password('b')
+
+
+class ValidatePasswordLengthTests(TestCase):
+    def test_valid_password(self):
+        """validate_password_length allows valid password"""
+        validate_password_length('A' * (settings.password_length_min + 1))
+
+    def test_invalid_name(self):
+        """validate_password_length disallows invalid password"""
+        with self.assertRaises(ValidationError):
+            validate_password_length('A' * (settings.password_length_min - 1))
+
+
+class ValidatePasswordComplexityRules(TestCase):
+    def test_validate_password_alphanumerics(self):
+        """_validate_password_alphanumerics enforces complexity correctly"""
+        _validate_password_alphanumerics('abc123')
+        with self.assertRaises(ValidationError):
+            _validate_password_alphanumerics('abc')
+        with self.assertRaises(ValidationError):
+            _validate_password_alphanumerics('123')
+
+    def test_validate_password_case(self):
+        """_validate_password_case enforces complexity correctly"""
+        _validate_password_case('AbC')
+        with self.assertRaises(ValidationError):
+            _validate_password_case('abc')
+        with self.assertRaises(ValidationError):
+            _validate_password_case('--')
+
+    def test_validate_password_special(self):
+        """_validate_password_special enforces complexity correctly"""
+        _validate_password_special(u'łoł12 3&(#@$')
+        with self.assertRaises(ValidationError):
+            _validate_password_special('A')
+
+
+class ValidateUsernameTests(TestCase):
+    def test_validate_username(self):
+        """validate_username has no crashes"""
+        validate_username('LeBob')
+        with self.assertRaises(ValidationError):
+            validate_username('*')
 
 
 
 
 class ValidateUsernameAvailableTests(TestCase):
 class ValidateUsernameAvailableTests(TestCase):
@@ -16,24 +99,24 @@ class ValidateUsernameAvailableTests(TestCase):
                                                   'eric@test.com',
                                                   'eric@test.com',
                                                   'pass123')
                                                   'pass123')
 
 
-    def test_valid_names(self):
+    def test_valid_name(self):
         """validate_username_available allows available names"""
         """validate_username_available allows available names"""
         validate_username_available('BobBoberson')
         validate_username_available('BobBoberson')
 
 
-    def test_invalid_names(self):
+    def test_invalid_name(self):
         """validate_username_available disallows unvailable names"""
         """validate_username_available disallows unvailable names"""
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validate_username_available(self.test_user.username)
             validate_username_available(self.test_user.username)
 
 
 
 
 class ValidateUsernameContentTests(TestCase):
 class ValidateUsernameContentTests(TestCase):
-    def test_valid_names(self):
+    def test_valid_name(self):
         """validate_username_content allows valid names"""
         """validate_username_content allows valid names"""
         validate_username_content('123')
         validate_username_content('123')
         validate_username_content('Bob')
         validate_username_content('Bob')
         validate_username_content('Bob123')
         validate_username_content('Bob123')
 
 
-    def test_invalid_names(self):
+    def test_invalid_name(self):
         """validate_username_content disallows invalid names"""
         """validate_username_content disallows invalid names"""
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validate_username_content('!')
             validate_username_content('!')
@@ -48,20 +131,14 @@ class ValidateUsernameContentTests(TestCase):
 
 
 
 
 class ValidateUsernameLengthTests(TestCase):
 class ValidateUsernameLengthTests(TestCase):
-    def test_valid_names(self):
+    def test_valid_name(self):
         """validate_username_length allows valid names"""
         """validate_username_length allows valid names"""
         validate_username_length('a' * settings.username_length_min)
         validate_username_length('a' * settings.username_length_min)
         validate_username_length('a' * settings.username_length_max)
         validate_username_length('a' * settings.username_length_max)
 
 
-    def test_invalid_names(self):
+    def test_invalid_name(self):
         """validate_username_length disallows invalid names"""
         """validate_username_length disallows invalid names"""
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validate_username_length('a' * (settings.username_length_min - 1))
             validate_username_length('a' * (settings.username_length_min - 1))
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             validate_username_length('a' * (settings.username_length_max + 1))
             validate_username_length('a' * (settings.username_length_max + 1))
-
-
-class ValidateUsernameTests(TestCase):
-    def test_validate_username(self):
-        """validate_username has no crashes"""
-        validate_username('LeBob')

+ 77 - 5
misago/users/validators.py

@@ -1,5 +1,6 @@
 import re
 import re
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
+from django.core.validators import validate_email as validate_email_contents
 from django.utils.translation import ungettext, ugettext_lazy as _
 from django.utils.translation import ungettext, ugettext_lazy as _
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from misago.conf import settings
 from misago.conf import settings
@@ -19,6 +20,10 @@ def validate_username_available(value):
         raise ValidationError(_("This username is not available."))
         raise ValidationError(_("This username is not available."))
 
 
 
 
+def validate_username_banned(value):
+    """TODO for when bans will be reimplemented from 0.5"""
+
+
 def validate_username_content(value):
 def validate_username_content(value):
     if not USERNAME_REGEX.match(value):
     if not USERNAME_REGEX.match(value):
         raise ValidationError(
         raise ValidationError(
@@ -29,25 +34,26 @@ def validate_username_length(value):
     if len(value) < settings.username_length_min:
     if len(value) < settings.username_length_min:
         message = ungettext(
         message = ungettext(
             'Username must be at least one character long.',
             'Username must be at least one character long.',
-            'Username must be at least %(count)d characters long.',
+            'Username must be at least %(length)d characters long.',
             settings.username_length_min)
             settings.username_length_min)
-        message = message % {'count': settings.username_length_min}
+        message = message % {'length': settings.username_length_min}
         raise ValidationError(message)
         raise ValidationError(message)
 
 
     if len(value) > settings.username_length_max:
     if len(value) > settings.username_length_max:
         message = ungettext(
         message = ungettext(
             "Username cannot be longer than one characters.",
             "Username cannot be longer than one characters.",
-            "Username cannot be longer than %(count)d characters.",
+            "Username cannot be longer than %(length)d characters.",
             settings.username_length_max)
             settings.username_length_max)
-        message = message % {'count': settings.username_length_max}
+        message = message % {'length': settings.username_length_max}
         raise ValidationError(message)
         raise ValidationError(message)
 
 
 
 
 def validate_username(value):
 def validate_username(value):
-    """Shortcut function that does complete validation of username"""
+    """shortcut function that does complete validation of username"""
     validate_username_content(value)
     validate_username_content(value)
     validate_username_length(value)
     validate_username_length(value)
     validate_username_available(value)
     validate_username_available(value)
+    validate_username_banned(value)
 
 
 
 
 def validate_email_available(value):
 def validate_email_available(value):
@@ -59,3 +65,69 @@ def validate_email_available(value):
         pass
         pass
     else:
     else:
         raise ValidationError(_("This e-mail address is not available."))
         raise ValidationError(_("This e-mail address is not available."))
+
+
+def validate_email_banned(value):
+    """TODO for when bans will be reimplemented from 0.5"""
+
+
+def validate_email(value):
+    """shortcut function that does complete validation of email"""
+    validate_email_contents(value)
+    validate_email_available(value)
+    validate_email_banned(value)
+
+
+def _validate_password_alphanumerics(value):
+    digits_len = len(filter(type(value).isdigit, value))
+
+    if not digits_len or digits_len == len(value):
+        raise ValidationError(
+            _("Password must contain digits in addition to other characters."))
+
+
+def _validate_password_case(value):
+    for char in value:
+        if char != char.lower():
+            break
+    else:
+        raise ValidationError(
+            _("Password must contain characters with different cases."))
+
+
+ALPHANUMERICS_RE = re.compile('[\W_]+', re.UNICODE)
+def _validate_password_special(value):
+    alphanumerics_len = len(ALPHANUMERICS_RE.sub('', value))
+
+    if not alphanumerics_len or alphanumerics_len == len(value):
+        raise ValidationError(
+            _("Password must contain special signs "
+              "in addition to other characters."))
+
+
+PASSWORD_COMPLEXITY_RULES = {
+    'alphanumerics': _validate_password_alphanumerics,
+    'case': _validate_password_case,
+    'special': _validate_password_special,
+}
+
+
+def validate_password_complexity(value):
+    for test in settings.password_complexity:
+        PASSWORD_COMPLEXITY_RULES[test](value)
+
+
+def validate_password_length(value):
+    if len(value) < settings.password_length_min:
+        message = ungettext(
+            'Valid password must be at least one character long.',
+            'valid password must be at least %(length)d characters long.',
+            settings.password_length_min)
+        message = message % {'length': settings.password_length_min}
+        raise ValidationError(message)
+
+
+def validate_password(value):
+    """shortcut function that does complete validation of password"""
+    validate_password_length(value)
+    validate_password_complexity(value)