Merge pull request #22361 from edx/diana/move-register-validation
Move RegistrationValidationView into user_authn.
This commit is contained in:
@@ -19,7 +19,6 @@ from .accounts.views import (
|
|||||||
UsernameReplacementView
|
UsernameReplacementView
|
||||||
)
|
)
|
||||||
from .preferences.views import PreferencesDetailView, PreferencesView
|
from .preferences.views import PreferencesDetailView, PreferencesView
|
||||||
from .validation.views import RegistrationValidationView
|
|
||||||
from .verification_api.views import IDVerificationStatusView
|
from .verification_api.views import IDVerificationStatusView
|
||||||
|
|
||||||
ME = AccountViewSet.as_view({
|
ME = AccountViewSet.as_view({
|
||||||
@@ -158,11 +157,6 @@ urlpatterns = [
|
|||||||
UsernameReplacementView.as_view(),
|
UsernameReplacementView.as_view(),
|
||||||
name='username_replacement'
|
name='username_replacement'
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r'^v1/validation/registration$',
|
|
||||||
RegistrationValidationView.as_view(),
|
|
||||||
name='registration_validation'
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r'^v1/preferences/{}$'.format(settings.USERNAME_PATTERN),
|
r'^v1/preferences/{}$'.format(settings.USERNAME_PATTERN),
|
||||||
PreferencesView.as_view(),
|
PreferencesView.as_view(),
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Tests for an API endpoint for client-side user data validation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import ddt
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test.utils import override_settings
|
|
||||||
from django.urls import reverse
|
|
||||||
from six import text_type
|
|
||||||
from six.moves import range
|
|
||||||
|
|
||||||
from openedx.core.djangoapps.user_api import accounts
|
|
||||||
from openedx.core.djangoapps.user_api.accounts.tests import testutils
|
|
||||||
from openedx.core.djangoapps.user_api.validation.views import RegistrationValidationThrottle
|
|
||||||
from openedx.core.lib.api import test_utils
|
|
||||||
from util.password_policy_validators import DEFAULT_MAX_PASSWORD_LENGTH
|
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
|
||||||
class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
|
||||||
"""
|
|
||||||
Tests for validity of user data in registration forms.
|
|
||||||
"""
|
|
||||||
|
|
||||||
endpoint_name = 'registration_validation'
|
|
||||||
path = reverse(endpoint_name)
|
|
||||||
|
|
||||||
def get_validation_decision(self, data):
|
|
||||||
response = self.client.post(self.path, data)
|
|
||||||
return response.data.get('validation_decisions', {})
|
|
||||||
|
|
||||||
def assertValidationDecision(self, data, decision):
|
|
||||||
self.assertEqual(
|
|
||||||
self.get_validation_decision(data),
|
|
||||||
decision
|
|
||||||
)
|
|
||||||
|
|
||||||
def assertNotValidationDecision(self, data, decision):
|
|
||||||
self.assertNotEqual(
|
|
||||||
self.get_validation_decision(data),
|
|
||||||
decision
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_no_decision_for_empty_request(self):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_no_decision_for_invalid_request(self):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'invalid_field': 'random_user_data'},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
@ddt.data(
|
|
||||||
['name', [name for name in testutils.VALID_NAMES]],
|
|
||||||
['email', [email for email in testutils.VALID_EMAILS]],
|
|
||||||
['password', [password for password in testutils.VALID_PASSWORDS]],
|
|
||||||
['username', [username for username in testutils.VALID_USERNAMES]],
|
|
||||||
['country', [country for country in testutils.VALID_COUNTRIES]]
|
|
||||||
)
|
|
||||||
@ddt.unpack
|
|
||||||
def test_positive_validation_decision(self, form_field_name, user_data):
|
|
||||||
"""
|
|
||||||
Test if {0} as any item in {1} gives a positive validation decision.
|
|
||||||
"""
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{form_field_name: user_data},
|
|
||||||
{form_field_name: ''}
|
|
||||||
)
|
|
||||||
|
|
||||||
@ddt.data(
|
|
||||||
# Skip None type for invalidity checks.
|
|
||||||
['name', [name for name in testutils.INVALID_NAMES[1:]]],
|
|
||||||
['email', [email for email in testutils.INVALID_EMAILS[1:]]],
|
|
||||||
['password', [password for password in testutils.INVALID_PASSWORDS[1:]]],
|
|
||||||
['username', [username for username in testutils.INVALID_USERNAMES[1:]]],
|
|
||||||
['country', [country for country in testutils.INVALID_COUNTRIES[1:]]]
|
|
||||||
)
|
|
||||||
@ddt.unpack
|
|
||||||
def test_negative_validation_decision(self, form_field_name, user_data):
|
|
||||||
"""
|
|
||||||
Test if {0} as any item in {1} gives a negative validation decision.
|
|
||||||
"""
|
|
||||||
self.assertNotValidationDecision(
|
|
||||||
{form_field_name: user_data},
|
|
||||||
{form_field_name: ''}
|
|
||||||
)
|
|
||||||
|
|
||||||
@ddt.data(
|
|
||||||
['username', 'username@email.com'], # No conflict
|
|
||||||
['user', 'username@email.com'], # Username conflict
|
|
||||||
['username', 'user@email.com'], # Email conflict
|
|
||||||
['user', 'user@email.com'] # Both conflict
|
|
||||||
)
|
|
||||||
@ddt.unpack
|
|
||||||
def test_existence_conflict(self, username, email):
|
|
||||||
"""
|
|
||||||
Test if username '{0}' and email '{1}' have conflicts with
|
|
||||||
username 'user' and email 'user@email.com'.
|
|
||||||
"""
|
|
||||||
user = User.objects.create_user(username='user', email='user@email.com')
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{
|
|
||||||
'username': username,
|
|
||||||
'email': email
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": accounts.USERNAME_CONFLICT_MSG.format(
|
|
||||||
username=user.username
|
|
||||||
) if username == user.username else '',
|
|
||||||
"email": accounts.EMAIL_CONFLICT_MSG.format(
|
|
||||||
email_address=user.email
|
|
||||||
) if email == user.email else ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@ddt.data('', ('e' * accounts.EMAIL_MAX_LENGTH) + '@email.com')
|
|
||||||
def test_email_bad_length_validation_decision(self, email):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'email': email},
|
|
||||||
{'email': accounts.EMAIL_BAD_LENGTH_MSG}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_email_generically_invalid_validation_decision(self):
|
|
||||||
email = 'email'
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'email': email},
|
|
||||||
{'email': accounts.EMAIL_INVALID_MSG.format(email=email)}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_confirm_email_matches_email(self):
|
|
||||||
email = 'user@email.com'
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'email': email, 'confirm_email': email},
|
|
||||||
{'email': '', 'confirm_email': ''}
|
|
||||||
)
|
|
||||||
|
|
||||||
@ddt.data('', 'users@other.email')
|
|
||||||
def test_confirm_email_doesnt_equal_email(self, confirm_email):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'email': 'user@email.com', 'confirm_email': confirm_email},
|
|
||||||
{'email': '', 'confirm_email': text_type(accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG)}
|
|
||||||
)
|
|
||||||
|
|
||||||
@ddt.data(
|
|
||||||
'u' * (accounts.USERNAME_MIN_LENGTH - 1),
|
|
||||||
'u' * (accounts.USERNAME_MAX_LENGTH + 1)
|
|
||||||
)
|
|
||||||
def test_username_bad_length_validation_decision(self, username):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'username': username},
|
|
||||||
{'username': text_type(accounts.USERNAME_BAD_LENGTH_MSG)}
|
|
||||||
)
|
|
||||||
|
|
||||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames disabled.")
|
|
||||||
@ddt.data(*testutils.INVALID_USERNAMES_UNICODE)
|
|
||||||
def test_username_invalid_unicode_validation_decision(self, username):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'username': username},
|
|
||||||
{'username': text_type(accounts.USERNAME_INVALID_CHARS_UNICODE)}
|
|
||||||
)
|
|
||||||
|
|
||||||
@unittest.skipIf(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames enabled.")
|
|
||||||
@ddt.data(*testutils.INVALID_USERNAMES_ASCII)
|
|
||||||
def test_username_invalid_ascii_validation_decision(self, username):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'username': username},
|
|
||||||
{"username": text_type(accounts.USERNAME_INVALID_CHARS_ASCII)}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_password_empty_validation_decision(self):
|
|
||||||
# 2 is the default setting for minimum length found in lms/envs/common.py
|
|
||||||
# under AUTH_PASSWORD_VALIDATORS.MinimumLengthValidator
|
|
||||||
msg = u'This password is too short. It must contain at least 2 characters.'
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'password': ''},
|
|
||||||
{"password": msg}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_password_bad_min_length_validation_decision(self):
|
|
||||||
password = 'p'
|
|
||||||
# 2 is the default setting for minimum length found in lms/envs/common.py
|
|
||||||
# under AUTH_PASSWORD_VALIDATORS.MinimumLengthValidator
|
|
||||||
msg = u'This password is too short. It must contain at least 2 characters.'
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'password': password},
|
|
||||||
{"password": msg}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_password_bad_max_length_validation_decision(self):
|
|
||||||
password = 'p' * DEFAULT_MAX_PASSWORD_LENGTH
|
|
||||||
# 75 is the default setting for maximum length found in lms/envs/common.py
|
|
||||||
# under AUTH_PASSWORD_VALIDATORS.MaximumLengthValidator
|
|
||||||
msg = u'This password is too long. It must contain no more than 75 characters.'
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{'password': password},
|
|
||||||
{"password": msg}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_password_equals_username_validation_decision(self):
|
|
||||||
self.assertValidationDecision(
|
|
||||||
{"username": "somephrase", "password": "somephrase"},
|
|
||||||
{"username": "", "password": u"The password is too similar to the username."}
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
CACHES={
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
||||||
'LOCATION': 'registration_proxy',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_rate_limiting_registration_view(self):
|
|
||||||
"""
|
|
||||||
Confirm rate limits work as expected for registration
|
|
||||||
end point /api/user/v1/validation/registration/. Note
|
|
||||||
that drf's rate limiting makes use of the default cache
|
|
||||||
to enforce limits; that's why this test needs a "real"
|
|
||||||
default cache (as opposed to the usual-for-tests DummyCache)
|
|
||||||
"""
|
|
||||||
for _ in range(RegistrationValidationThrottle().num_requests):
|
|
||||||
self.request_without_auth('post', self.path)
|
|
||||||
response = self.request_without_auth('post', self.path)
|
|
||||||
self.assertEqual(response.status_code, 429)
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
An API for client-side validation of (potential) user data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from ipware.ip import get_ip
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.throttling import AnonRateThrottle
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from openedx.core.djangoapps.user_api.accounts.api import (
|
|
||||||
get_confirm_email_validation_error,
|
|
||||||
get_country_validation_error,
|
|
||||||
get_email_existence_validation_error,
|
|
||||||
get_email_validation_error,
|
|
||||||
get_name_validation_error,
|
|
||||||
get_password_validation_error,
|
|
||||||
get_username_existence_validation_error,
|
|
||||||
get_username_validation_error
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationValidationThrottle(AnonRateThrottle):
|
|
||||||
"""
|
|
||||||
Custom throttle rate for /api/user/v1/validation/registration
|
|
||||||
endpoint's use case.
|
|
||||||
"""
|
|
||||||
|
|
||||||
scope = 'registration_validation'
|
|
||||||
|
|
||||||
def get_ident(self, request):
|
|
||||||
client_ip = get_ip(request)
|
|
||||||
return client_ip
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationValidationView(APIView):
|
|
||||||
"""
|
|
||||||
**Use Cases**
|
|
||||||
|
|
||||||
Get validation information about user data during registration.
|
|
||||||
Client-side may request validation for any number of form fields,
|
|
||||||
and the API will return a conclusion from its analysis for each
|
|
||||||
input (i.e. valid or not valid, or a custom, detailed message).
|
|
||||||
|
|
||||||
**Example Requests and Responses**
|
|
||||||
|
|
||||||
- Checks the validity of the username and email inputs separately.
|
|
||||||
POST /api/user/v1/validation/registration/
|
|
||||||
>>> {
|
|
||||||
>>> "username": "hi_im_new",
|
|
||||||
>>> "email": "newguy101@edx.org"
|
|
||||||
>>> }
|
|
||||||
RESPONSE
|
|
||||||
>>> {
|
|
||||||
>>> "validation_decisions": {
|
|
||||||
>>> "username": "",
|
|
||||||
>>> "email": ""
|
|
||||||
>>> }
|
|
||||||
>>> }
|
|
||||||
Empty strings indicate that there was no problem with the input.
|
|
||||||
|
|
||||||
- Checks the validity of the password field (its validity depends
|
|
||||||
upon both the username and password fields, so we need both). If
|
|
||||||
only password is input, we don't check for password/username
|
|
||||||
compatibility issues.
|
|
||||||
POST /api/user/v1/validation/registration/
|
|
||||||
>>> {
|
|
||||||
>>> "username": "myname",
|
|
||||||
>>> "password": "myname"
|
|
||||||
>>> }
|
|
||||||
RESPONSE
|
|
||||||
>>> {
|
|
||||||
>>> "validation_decisions": {
|
|
||||||
>>> "username": "",
|
|
||||||
>>> "password": "Password cannot be the same as the username."
|
|
||||||
>>> }
|
|
||||||
>>> }
|
|
||||||
|
|
||||||
- Checks the validity of the username, email, and password fields
|
|
||||||
separately, and also tells whether an account exists. The password
|
|
||||||
field's validity depends upon both the username and password, and
|
|
||||||
the account's existence depends upon both the username and email.
|
|
||||||
POST /api/user/v1/validation/registration/
|
|
||||||
>>> {
|
|
||||||
>>> "username": "hi_im_new",
|
|
||||||
>>> "email": "cto@edx.org",
|
|
||||||
>>> "password": "p"
|
|
||||||
>>> }
|
|
||||||
RESPONSE
|
|
||||||
>>> {
|
|
||||||
>>> "validation_decisions": {
|
|
||||||
>>> "username": "",
|
|
||||||
>>> "email": "It looks like cto@edx.org belongs to an existing account. Try again with a different email address.",
|
|
||||||
>>> "password": "Password must be at least 2 characters long",
|
|
||||||
>>> }
|
|
||||||
>>> }
|
|
||||||
In this example, username is valid and (we assume) there is
|
|
||||||
a preexisting account with that email. The password also seems
|
|
||||||
to contain the username.
|
|
||||||
|
|
||||||
Note that a validation decision is returned *for all* inputs, whether
|
|
||||||
positive or negative.
|
|
||||||
|
|
||||||
**Available Handlers**
|
|
||||||
|
|
||||||
"name":
|
|
||||||
A handler to check the validity of the user's real name.
|
|
||||||
"username":
|
|
||||||
A handler to check the validity of usernames.
|
|
||||||
"email":
|
|
||||||
A handler to check the validity of emails.
|
|
||||||
"confirm_email":
|
|
||||||
A handler to check whether the confirmation email field matches
|
|
||||||
the email field.
|
|
||||||
"password":
|
|
||||||
A handler to check the validity of passwords; a compatibility
|
|
||||||
decision with the username is made if it exists in the input.
|
|
||||||
"country":
|
|
||||||
A handler to check whether the validity of country fields.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# This end-point is available to anonymous users, so no authentication is needed.
|
|
||||||
authentication_classes = []
|
|
||||||
throttle_classes = (RegistrationValidationThrottle,)
|
|
||||||
|
|
||||||
def name_handler(self, request):
|
|
||||||
name = request.data.get('name')
|
|
||||||
return get_name_validation_error(name)
|
|
||||||
|
|
||||||
def username_handler(self, request):
|
|
||||||
username = request.data.get('username')
|
|
||||||
invalid_username_error = get_username_validation_error(username)
|
|
||||||
username_exists_error = get_username_existence_validation_error(username)
|
|
||||||
# We prefer seeing for invalidity first.
|
|
||||||
# Some invalid usernames (like for superusers) may exist.
|
|
||||||
return invalid_username_error or username_exists_error
|
|
||||||
|
|
||||||
def email_handler(self, request):
|
|
||||||
email = request.data.get('email')
|
|
||||||
invalid_email_error = get_email_validation_error(email)
|
|
||||||
email_exists_error = get_email_existence_validation_error(email)
|
|
||||||
# We prefer seeing for invalidity first.
|
|
||||||
# Some invalid emails (like a blank one for superusers) may exist.
|
|
||||||
return invalid_email_error or email_exists_error
|
|
||||||
|
|
||||||
def confirm_email_handler(self, request):
|
|
||||||
email = request.data.get('email')
|
|
||||||
confirm_email = request.data.get('confirm_email')
|
|
||||||
return get_confirm_email_validation_error(confirm_email, email)
|
|
||||||
|
|
||||||
def password_handler(self, request):
|
|
||||||
username = request.data.get('username')
|
|
||||||
email = request.data.get('email')
|
|
||||||
password = request.data.get('password')
|
|
||||||
return get_password_validation_error(password, username, email)
|
|
||||||
|
|
||||||
def country_handler(self, request):
|
|
||||||
country = request.data.get('country')
|
|
||||||
return get_country_validation_error(country)
|
|
||||||
|
|
||||||
validation_handlers = {
|
|
||||||
"name": name_handler,
|
|
||||||
"username": username_handler,
|
|
||||||
"email": email_handler,
|
|
||||||
"confirm_email": confirm_email_handler,
|
|
||||||
"password": password_handler,
|
|
||||||
"country": country_handler
|
|
||||||
}
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
"""
|
|
||||||
POST /api/user/v1/validation/registration/
|
|
||||||
|
|
||||||
Expects request of the form
|
|
||||||
>>> {
|
|
||||||
>>> "name": "Dan the Validator",
|
|
||||||
>>> "username": "mslm",
|
|
||||||
>>> "email": "mslm@gmail.com",
|
|
||||||
>>> "confirm_email": "mslm@gmail.com",
|
|
||||||
>>> "password": "password123",
|
|
||||||
>>> "country": "PK"
|
|
||||||
>>> }
|
|
||||||
where each key is the appropriate form field name and the value is
|
|
||||||
user input. One may enter individual inputs if needed. Some inputs
|
|
||||||
can get extra verification checks if entered along with others,
|
|
||||||
like when the password may not equal the username.
|
|
||||||
"""
|
|
||||||
validation_decisions = {}
|
|
||||||
for form_field_key in self.validation_handlers:
|
|
||||||
# For every field requiring validation from the client,
|
|
||||||
# request a decision for it from the appropriate handler.
|
|
||||||
if form_field_key in request.data:
|
|
||||||
handler = self.validation_handlers[form_field_key]
|
|
||||||
validation_decisions.update({
|
|
||||||
form_field_key: handler(self, request)
|
|
||||||
})
|
|
||||||
return Response({"validation_decisions": validation_decisions})
|
|
||||||
@@ -5,6 +5,9 @@ Note: The split between urls.py and urls_common.py is hopefully temporary.
|
|||||||
For now, this is needed because of difference in CMS and LMS that have
|
For now, this is needed because of difference in CMS and LMS that have
|
||||||
not yet been cleaned up.
|
not yet been cleaned up.
|
||||||
|
|
||||||
|
This is also home to urls for endpoints that have been consolidated from other djangoapps,
|
||||||
|
which leads to inconsistent prefixing.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
@@ -17,12 +20,27 @@ from .views import auto_auth, login, logout, password_reset, register
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Registration
|
# Registration
|
||||||
url(r'^create_account$', register.RegistrationView.as_view(), name='create_account'),
|
url(r'^create_account$', register.RegistrationView.as_view(), name='create_account'),
|
||||||
|
|
||||||
|
# Moved from user_api/legacy_urls.py
|
||||||
|
# `user_api` prefix is preserved for backwards compatibility.
|
||||||
url(r'^user_api/v1/account/registration/$', register.RegistrationView.as_view(),
|
url(r'^user_api/v1/account/registration/$', register.RegistrationView.as_view(),
|
||||||
name="user_api_registration"),
|
name="user_api_registration"),
|
||||||
|
|
||||||
|
# Moved from user_api/urls.py
|
||||||
|
# `api/user` prefix is preserved for backwards compatibility.
|
||||||
|
url(
|
||||||
|
r'^api/user/v1/validation/registration$',
|
||||||
|
register.RegistrationValidationView.as_view(),
|
||||||
|
name='registration_validation'
|
||||||
|
),
|
||||||
|
|
||||||
# Login
|
# Login
|
||||||
url(r'^login_post$', login.login_user, name='login_post'),
|
url(r'^login_post$', login.login_user, name='login_post'),
|
||||||
url(r'^login_ajax$', login.login_user, name="login"),
|
url(r'^login_ajax$', login.login_user, name="login"),
|
||||||
url(r'^login_ajax/(?P<error>[^/]*)$', login.login_user),
|
url(r'^login_ajax/(?P<error>[^/]*)$', login.login_user),
|
||||||
|
|
||||||
|
# Moved from user_api/legacy_urls.py
|
||||||
|
# `user_api` prefix is preserved for backwards compatibility.
|
||||||
url(r'^user_api/v1/account/login_session/$', login.LoginSessionView.as_view(),
|
url(r'^user_api/v1/account/login_session/$', login.LoginSessionView.as_view(),
|
||||||
name="user_api_login_session"),
|
name="user_api_login_session"),
|
||||||
|
|
||||||
@@ -31,7 +49,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
url(r'^logout$', logout.LogoutView.as_view(), name='logout'),
|
url(r'^logout$', logout.LogoutView.as_view(), name='logout'),
|
||||||
|
|
||||||
url(r'^v1/account/password_reset/$', password_reset.PasswordResetView.as_view(),
|
# Moved from user_api/legacy_urls.py
|
||||||
|
url(r'^user_api/v1/account/password_reset/$', password_reset.PasswordResetView.as_view(),
|
||||||
name="user_api_password_reset"),
|
name="user_api_password_reset"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ from django.utils.translation import ugettext as _
|
|||||||
from pytz import UTC
|
from pytz import UTC
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
from ipware.ip import get_ip
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from social_core.exceptions import AuthAlreadyAssociated, AuthException
|
from social_core.exceptions import AuthAlreadyAssociated, AuthException
|
||||||
from social_django import utils as social_utils
|
from social_django import utils as social_utils
|
||||||
@@ -36,6 +39,16 @@ from lms.djangoapps.discussion.notification_prefs.views import enable_notificati
|
|||||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||||
from openedx.core.djangoapps.user_api import accounts as accounts_settings
|
from openedx.core.djangoapps.user_api import accounts as accounts_settings
|
||||||
|
from openedx.core.djangoapps.user_api.accounts.api import (
|
||||||
|
get_confirm_email_validation_error,
|
||||||
|
get_country_validation_error,
|
||||||
|
get_email_existence_validation_error,
|
||||||
|
get_email_validation_error,
|
||||||
|
get_name_validation_error,
|
||||||
|
get_password_validation_error,
|
||||||
|
get_username_existence_validation_error,
|
||||||
|
get_username_validation_error
|
||||||
|
)
|
||||||
from openedx.core.djangoapps.user_authn.utils import generate_password
|
from openedx.core.djangoapps.user_authn.utils import generate_password
|
||||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||||
from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies
|
from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies
|
||||||
@@ -517,3 +530,183 @@ class RegistrationView(APIView):
|
|||||||
# keeping this `success` field in for now, as we have outstanding clients expecting this
|
# keeping this `success` field in for now, as we have outstanding clients expecting this
|
||||||
response_dict['success'] = True
|
response_dict['success'] = True
|
||||||
return JsonResponse(response_dict, status=status_code)
|
return JsonResponse(response_dict, status=status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationValidationThrottle(AnonRateThrottle):
|
||||||
|
"""
|
||||||
|
Custom throttle rate for /api/user/v1/validation/registration
|
||||||
|
endpoint's use case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
scope = 'registration_validation'
|
||||||
|
|
||||||
|
def get_ident(self, request):
|
||||||
|
client_ip = get_ip(request)
|
||||||
|
return client_ip
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
class RegistrationValidationView(APIView):
|
||||||
|
"""
|
||||||
|
**Use Cases**
|
||||||
|
|
||||||
|
Get validation information about user data during registration.
|
||||||
|
Client-side may request validation for any number of form fields,
|
||||||
|
and the API will return a conclusion from its analysis for each
|
||||||
|
input (i.e. valid or not valid, or a custom, detailed message).
|
||||||
|
|
||||||
|
**Example Requests and Responses**
|
||||||
|
|
||||||
|
- Checks the validity of the username and email inputs separately.
|
||||||
|
POST /api/user/v1/validation/registration/
|
||||||
|
>>> {
|
||||||
|
>>> "username": "hi_im_new",
|
||||||
|
>>> "email": "newguy101@edx.org"
|
||||||
|
>>> }
|
||||||
|
RESPONSE
|
||||||
|
>>> {
|
||||||
|
>>> "validation_decisions": {
|
||||||
|
>>> "username": "",
|
||||||
|
>>> "email": ""
|
||||||
|
>>> }
|
||||||
|
>>> }
|
||||||
|
Empty strings indicate that there was no problem with the input.
|
||||||
|
|
||||||
|
- Checks the validity of the password field (its validity depends
|
||||||
|
upon both the username and password fields, so we need both). If
|
||||||
|
only password is input, we don't check for password/username
|
||||||
|
compatibility issues.
|
||||||
|
POST /api/user/v1/validation/registration/
|
||||||
|
>>> {
|
||||||
|
>>> "username": "myname",
|
||||||
|
>>> "password": "myname"
|
||||||
|
>>> }
|
||||||
|
RESPONSE
|
||||||
|
>>> {
|
||||||
|
>>> "validation_decisions": {
|
||||||
|
>>> "username": "",
|
||||||
|
>>> "password": "Password cannot be the same as the username."
|
||||||
|
>>> }
|
||||||
|
>>> }
|
||||||
|
|
||||||
|
- Checks the validity of the username, email, and password fields
|
||||||
|
separately, and also tells whether an account exists. The password
|
||||||
|
field's validity depends upon both the username and password, and
|
||||||
|
the account's existence depends upon both the username and email.
|
||||||
|
POST /api/user/v1/validation/registration/
|
||||||
|
>>> {
|
||||||
|
>>> "username": "hi_im_new",
|
||||||
|
>>> "email": "cto@edx.org",
|
||||||
|
>>> "password": "p"
|
||||||
|
>>> }
|
||||||
|
RESPONSE
|
||||||
|
>>> {
|
||||||
|
>>> "validation_decisions": {
|
||||||
|
>>> "username": "",
|
||||||
|
>>> "email": "It looks like cto@edx.org belongs to an existing account. Try again with a different email address.",
|
||||||
|
>>> "password": "Password must be at least 2 characters long",
|
||||||
|
>>> }
|
||||||
|
>>> }
|
||||||
|
In this example, username is valid and (we assume) there is
|
||||||
|
a preexisting account with that email. The password also seems
|
||||||
|
to contain the username.
|
||||||
|
|
||||||
|
Note that a validation decision is returned *for all* inputs, whether
|
||||||
|
positive or negative.
|
||||||
|
|
||||||
|
**Available Handlers**
|
||||||
|
|
||||||
|
"name":
|
||||||
|
A handler to check the validity of the user's real name.
|
||||||
|
"username":
|
||||||
|
A handler to check the validity of usernames.
|
||||||
|
"email":
|
||||||
|
A handler to check the validity of emails.
|
||||||
|
"confirm_email":
|
||||||
|
A handler to check whether the confirmation email field matches
|
||||||
|
the email field.
|
||||||
|
"password":
|
||||||
|
A handler to check the validity of passwords; a compatibility
|
||||||
|
decision with the username is made if it exists in the input.
|
||||||
|
"country":
|
||||||
|
A handler to check whether the validity of country fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This end-point is available to anonymous users, so no authentication is needed.
|
||||||
|
authentication_classes = []
|
||||||
|
throttle_classes = (RegistrationValidationThrottle,)
|
||||||
|
|
||||||
|
def name_handler(self, request):
|
||||||
|
name = request.data.get('name')
|
||||||
|
return get_name_validation_error(name)
|
||||||
|
|
||||||
|
def username_handler(self, request):
|
||||||
|
""" Validates whether the username is valid. """
|
||||||
|
username = request.data.get('username')
|
||||||
|
invalid_username_error = get_username_validation_error(username)
|
||||||
|
username_exists_error = get_username_existence_validation_error(username)
|
||||||
|
# We prefer seeing for invalidity first.
|
||||||
|
# Some invalid usernames (like for superusers) may exist.
|
||||||
|
return invalid_username_error or username_exists_error
|
||||||
|
|
||||||
|
def email_handler(self, request):
|
||||||
|
""" Validates whether the email address is valid. """
|
||||||
|
email = request.data.get('email')
|
||||||
|
invalid_email_error = get_email_validation_error(email)
|
||||||
|
email_exists_error = get_email_existence_validation_error(email)
|
||||||
|
# We prefer seeing for invalidity first.
|
||||||
|
# Some invalid emails (like a blank one for superusers) may exist.
|
||||||
|
return invalid_email_error or email_exists_error
|
||||||
|
|
||||||
|
def confirm_email_handler(self, request):
|
||||||
|
email = request.data.get('email')
|
||||||
|
confirm_email = request.data.get('confirm_email')
|
||||||
|
return get_confirm_email_validation_error(confirm_email, email)
|
||||||
|
|
||||||
|
def password_handler(self, request):
|
||||||
|
username = request.data.get('username')
|
||||||
|
email = request.data.get('email')
|
||||||
|
password = request.data.get('password')
|
||||||
|
return get_password_validation_error(password, username, email)
|
||||||
|
|
||||||
|
def country_handler(self, request):
|
||||||
|
country = request.data.get('country')
|
||||||
|
return get_country_validation_error(country)
|
||||||
|
|
||||||
|
validation_handlers = {
|
||||||
|
"name": name_handler,
|
||||||
|
"username": username_handler,
|
||||||
|
"email": email_handler,
|
||||||
|
"confirm_email": confirm_email_handler,
|
||||||
|
"password": password_handler,
|
||||||
|
"country": country_handler
|
||||||
|
}
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
POST /api/user/v1/validation/registration/
|
||||||
|
|
||||||
|
Expects request of the form
|
||||||
|
>>> {
|
||||||
|
>>> "name": "Dan the Validator",
|
||||||
|
>>> "username": "mslm",
|
||||||
|
>>> "email": "mslm@gmail.com",
|
||||||
|
>>> "confirm_email": "mslm@gmail.com",
|
||||||
|
>>> "password": "password123",
|
||||||
|
>>> "country": "PK"
|
||||||
|
>>> }
|
||||||
|
where each key is the appropriate form field name and the value is
|
||||||
|
user input. One may enter individual inputs if needed. Some inputs
|
||||||
|
can get extra verification checks if entered along with others,
|
||||||
|
like when the password may not equal the username.
|
||||||
|
"""
|
||||||
|
validation_decisions = {}
|
||||||
|
for form_field_key in self.validation_handlers:
|
||||||
|
# For every field requiring validation from the client,
|
||||||
|
# request a decision for it from the appropriate handler.
|
||||||
|
if form_field_key in request.data:
|
||||||
|
handler = self.validation_handlers[form_field_key]
|
||||||
|
validation_decisions.update({
|
||||||
|
form_field_key: handler(self, request)
|
||||||
|
})
|
||||||
|
return Response({"validation_decisions": validation_decisions})
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from unittest import skipUnless
|
from unittest import skipIf, skipUnless
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
import httpretty
|
import httpretty
|
||||||
import mock
|
import mock
|
||||||
import six
|
import six
|
||||||
|
from six.moves import range
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
@@ -24,14 +26,22 @@ from social_django.models import Partial, UserSocialAuth
|
|||||||
from openedx.core.djangoapps.site_configuration.helpers import get_value
|
from openedx.core.djangoapps.site_configuration.helpers import get_value
|
||||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration
|
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration
|
||||||
from openedx.core.djangoapps.user_api.accounts import (
|
from openedx.core.djangoapps.user_api.accounts import (
|
||||||
|
EMAIL_BAD_LENGTH_MSG,
|
||||||
|
EMAIL_CONFLICT_MSG,
|
||||||
|
EMAIL_INVALID_MSG,
|
||||||
EMAIL_MAX_LENGTH,
|
EMAIL_MAX_LENGTH,
|
||||||
EMAIL_MIN_LENGTH,
|
EMAIL_MIN_LENGTH,
|
||||||
NAME_MAX_LENGTH,
|
NAME_MAX_LENGTH,
|
||||||
|
REQUIRED_FIELD_CONFIRM_EMAIL_MSG,
|
||||||
USERNAME_MAX_LENGTH,
|
USERNAME_MAX_LENGTH,
|
||||||
USERNAME_MIN_LENGTH,
|
USERNAME_MIN_LENGTH,
|
||||||
USERNAME_BAD_LENGTH_MSG,
|
USERNAME_BAD_LENGTH_MSG,
|
||||||
|
USERNAME_CONFLICT_MSG,
|
||||||
|
USERNAME_INVALID_CHARS_ASCII,
|
||||||
|
USERNAME_INVALID_CHARS_UNICODE,
|
||||||
)
|
)
|
||||||
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
|
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
|
||||||
|
from openedx.core.djangoapps.user_api.accounts.tests import testutils
|
||||||
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
|
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
|
||||||
RetirementTestCase,
|
RetirementTestCase,
|
||||||
fake_requested_retirement,
|
fake_requested_retirement,
|
||||||
@@ -40,7 +50,9 @@ from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import (
|
|||||||
from openedx.core.djangoapps.user_api.tests.test_helpers import TestCaseForm
|
from openedx.core.djangoapps.user_api.tests.test_helpers import TestCaseForm
|
||||||
from openedx.core.djangoapps.user_api.tests.test_constants import SORTED_COUNTRIES
|
from openedx.core.djangoapps.user_api.tests.test_constants import SORTED_COUNTRIES
|
||||||
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
|
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
|
||||||
|
from openedx.core.djangoapps.user_authn.views.register import RegistrationValidationThrottle
|
||||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
||||||
|
from openedx.core.lib.api import test_utils
|
||||||
from student.tests.factories import UserFactory
|
from student.tests.factories import UserFactory
|
||||||
from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline
|
from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline
|
||||||
from third_party_auth.tests.utils import (
|
from third_party_auth.tests.utils import (
|
||||||
@@ -49,6 +61,7 @@ from third_party_auth.tests.utils import (
|
|||||||
ThirdPartyOAuthTestMixinGoogle
|
ThirdPartyOAuthTestMixinGoogle
|
||||||
)
|
)
|
||||||
from util.password_policy_validators import (
|
from util.password_policy_validators import (
|
||||||
|
DEFAULT_MAX_PASSWORD_LENGTH,
|
||||||
create_validator_config,
|
create_validator_config,
|
||||||
password_validators_instruction_texts,
|
password_validators_instruction_texts,
|
||||||
password_validators_restrictions
|
password_validators_restrictions
|
||||||
@@ -1845,3 +1858,217 @@ class TestGoogleRegistrationView(
|
|||||||
):
|
):
|
||||||
"""Tests the User API registration endpoint with Google authentication."""
|
"""Tests the User API registration endpoint with Google authentication."""
|
||||||
__test__ = True
|
__test__ = True
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||||
|
"""
|
||||||
|
Tests for validity of user data in registration forms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
endpoint_name = 'registration_validation'
|
||||||
|
path = reverse(endpoint_name)
|
||||||
|
|
||||||
|
def get_validation_decision(self, data):
|
||||||
|
response = self.client.post(self.path, data)
|
||||||
|
return response.data.get('validation_decisions', {})
|
||||||
|
|
||||||
|
def assertValidationDecision(self, data, decision):
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_validation_decision(data),
|
||||||
|
decision
|
||||||
|
)
|
||||||
|
|
||||||
|
def assertNotValidationDecision(self, data, decision):
|
||||||
|
self.assertNotEqual(
|
||||||
|
self.get_validation_decision(data),
|
||||||
|
decision
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_decision_for_empty_request(self):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_decision_for_invalid_request(self):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'invalid_field': 'random_user_data'},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
['name', [name for name in testutils.VALID_NAMES]],
|
||||||
|
['email', [email for email in testutils.VALID_EMAILS]],
|
||||||
|
['password', [password for password in testutils.VALID_PASSWORDS]],
|
||||||
|
['username', [username for username in testutils.VALID_USERNAMES]],
|
||||||
|
['country', [country for country in testutils.VALID_COUNTRIES]]
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_positive_validation_decision(self, form_field_name, user_data):
|
||||||
|
"""
|
||||||
|
Test if {0} as any item in {1} gives a positive validation decision.
|
||||||
|
"""
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{form_field_name: user_data},
|
||||||
|
{form_field_name: ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
# Skip None type for invalidity checks.
|
||||||
|
['name', [name for name in testutils.INVALID_NAMES[1:]]],
|
||||||
|
['email', [email for email in testutils.INVALID_EMAILS[1:]]],
|
||||||
|
['password', [password for password in testutils.INVALID_PASSWORDS[1:]]],
|
||||||
|
['username', [username for username in testutils.INVALID_USERNAMES[1:]]],
|
||||||
|
['country', [country for country in testutils.INVALID_COUNTRIES[1:]]]
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_negative_validation_decision(self, form_field_name, user_data):
|
||||||
|
"""
|
||||||
|
Test if {0} as any item in {1} gives a negative validation decision.
|
||||||
|
"""
|
||||||
|
self.assertNotValidationDecision(
|
||||||
|
{form_field_name: user_data},
|
||||||
|
{form_field_name: ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
['username', 'username@email.com'], # No conflict
|
||||||
|
['user', 'username@email.com'], # Username conflict
|
||||||
|
['username', 'user@email.com'], # Email conflict
|
||||||
|
['user', 'user@email.com'] # Both conflict
|
||||||
|
)
|
||||||
|
@ddt.unpack
|
||||||
|
def test_existence_conflict(self, username, email):
|
||||||
|
"""
|
||||||
|
Test if username '{0}' and email '{1}' have conflicts with
|
||||||
|
username 'user' and email 'user@email.com'.
|
||||||
|
"""
|
||||||
|
user = User.objects.create_user(username='user', email='user@email.com')
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{
|
||||||
|
'username': username,
|
||||||
|
'email': email
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# pylint: disable=no-member
|
||||||
|
"username": USERNAME_CONFLICT_MSG.format(
|
||||||
|
username=user.username
|
||||||
|
) if username == user.username else '',
|
||||||
|
# pylint: disable=no-member
|
||||||
|
"email": EMAIL_CONFLICT_MSG.format(
|
||||||
|
email_address=user.email
|
||||||
|
) if email == user.email else ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data('', ('e' * EMAIL_MAX_LENGTH) + '@email.com')
|
||||||
|
def test_email_bad_length_validation_decision(self, email):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'email': email},
|
||||||
|
{'email': EMAIL_BAD_LENGTH_MSG}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_email_generically_invalid_validation_decision(self):
|
||||||
|
email = 'email'
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'email': email},
|
||||||
|
# pylint: disable=no-member
|
||||||
|
{'email': EMAIL_INVALID_MSG.format(email=email)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_confirm_email_matches_email(self):
|
||||||
|
email = 'user@email.com'
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'email': email, 'confirm_email': email},
|
||||||
|
{'email': '', 'confirm_email': ''}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data('', 'users@other.email')
|
||||||
|
def test_confirm_email_doesnt_equal_email(self, confirm_email):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'email': 'user@email.com', 'confirm_email': confirm_email},
|
||||||
|
{'email': '', 'confirm_email': six.text_type(REQUIRED_FIELD_CONFIRM_EMAIL_MSG)}
|
||||||
|
)
|
||||||
|
|
||||||
|
@ddt.data(
|
||||||
|
'u' * (USERNAME_MIN_LENGTH - 1),
|
||||||
|
'u' * (USERNAME_MAX_LENGTH + 1)
|
||||||
|
)
|
||||||
|
def test_username_bad_length_validation_decision(self, username):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'username': username},
|
||||||
|
{'username': six.text_type(USERNAME_BAD_LENGTH_MSG)}
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnless(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames disabled.")
|
||||||
|
@ddt.data(*testutils.INVALID_USERNAMES_UNICODE)
|
||||||
|
def test_username_invalid_unicode_validation_decision(self, username):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'username': username},
|
||||||
|
{'username': six.text_type(USERNAME_INVALID_CHARS_UNICODE)}
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipIf(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames enabled.")
|
||||||
|
@ddt.data(*testutils.INVALID_USERNAMES_ASCII)
|
||||||
|
def test_username_invalid_ascii_validation_decision(self, username):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'username': username},
|
||||||
|
{"username": six.text_type(USERNAME_INVALID_CHARS_ASCII)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_password_empty_validation_decision(self):
|
||||||
|
# 2 is the default setting for minimum length found in lms/envs/common.py
|
||||||
|
# under AUTH_PASSWORD_VALIDATORS.MinimumLengthValidator
|
||||||
|
msg = u'This password is too short. It must contain at least 2 characters.'
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'password': ''},
|
||||||
|
{"password": msg}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_password_bad_min_length_validation_decision(self):
|
||||||
|
password = 'p'
|
||||||
|
# 2 is the default setting for minimum length found in lms/envs/common.py
|
||||||
|
# under AUTH_PASSWORD_VALIDATORS.MinimumLengthValidator
|
||||||
|
msg = u'This password is too short. It must contain at least 2 characters.'
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'password': password},
|
||||||
|
{"password": msg}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_password_bad_max_length_validation_decision(self):
|
||||||
|
password = 'p' * DEFAULT_MAX_PASSWORD_LENGTH
|
||||||
|
# 75 is the default setting for maximum length found in lms/envs/common.py
|
||||||
|
# under AUTH_PASSWORD_VALIDATORS.MaximumLengthValidator
|
||||||
|
msg = u'This password is too long. It must contain no more than 75 characters.'
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{'password': password},
|
||||||
|
{"password": msg}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_password_equals_username_validation_decision(self):
|
||||||
|
self.assertValidationDecision(
|
||||||
|
{"username": "somephrase", "password": "somephrase"},
|
||||||
|
{"username": "", "password": u"The password is too similar to the username."}
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CACHES={
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
'LOCATION': 'registration_proxy',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_rate_limiting_registration_view(self):
|
||||||
|
"""
|
||||||
|
Confirm rate limits work as expected for registration
|
||||||
|
end point /api/user/v1/validation/registration/. Note
|
||||||
|
that drf's rate limiting makes use of the default cache
|
||||||
|
to enforce limits; that's why this test needs a "real"
|
||||||
|
default cache (as opposed to the usual-for-tests DummyCache)
|
||||||
|
"""
|
||||||
|
for _ in range(RegistrationValidationThrottle().num_requests):
|
||||||
|
self.request_without_auth('post', self.path)
|
||||||
|
response = self.request_without_auth('post', self.path)
|
||||||
|
self.assertEqual(response.status_code, 429)
|
||||||
|
|||||||
Reference in New Issue
Block a user