Move RegistrationValidationView into user_authn.

This commit is contained in:
Diana Huang
2019-11-20 10:30:01 -05:00
parent 44a70ff8cc
commit 898bd8a90e
8 changed files with 441 additions and 440 deletions

View File

@@ -19,7 +19,6 @@ from .accounts.views import (
UsernameReplacementView
)
from .preferences.views import PreferencesDetailView, PreferencesView
from .validation.views import RegistrationValidationView
from .verification_api.views import IDVerificationStatusView
ME = AccountViewSet.as_view({
@@ -158,11 +157,6 @@ urlpatterns = [
UsernameReplacementView.as_view(),
name='username_replacement'
),
url(
r'^v1/validation/registration$',
RegistrationValidationView.as_view(),
name='registration_validation'
),
url(
r'^v1/preferences/{}$'.format(settings.USERNAME_PATTERN),
PreferencesView.as_view(),

View File

@@ -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)

View File

@@ -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})

View File

@@ -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
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
@@ -17,12 +20,27 @@ from .views import auto_auth, login, logout, password_reset, register
urlpatterns = [
# Registration
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(),
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
url(r'^login_post$', login.login_user, name='login_post'),
url(r'^login_ajax$', login.login_user, name="login"),
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(),
name="user_api_login_session"),
@@ -31,7 +49,8 @@ urlpatterns = [
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"),
]

View File

@@ -25,6 +25,9 @@ from django.utils.translation import ugettext as _
from pytz import UTC
from requests import HTTPError
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 social_core.exceptions import AuthAlreadyAssociated, AuthException
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.site_configuration import helpers as configuration_helpers
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_api.preferences import api as preferences_api
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
response_dict['success'] = True
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})

View File

@@ -3,13 +3,15 @@
from __future__ import absolute_import
import json
from unittest import skipUnless
from unittest import skipIf, skipUnless
from datetime import datetime
import ddt
import httpretty
import mock
import six
from six.moves import range
from django.conf import settings
from django.contrib.auth.models import User
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.tests.test_util import with_site_configuration
from openedx.core.djangoapps.user_api.accounts import (
EMAIL_BAD_LENGTH_MSG,
EMAIL_CONFLICT_MSG,
EMAIL_INVALID_MSG,
EMAIL_MAX_LENGTH,
EMAIL_MIN_LENGTH,
NAME_MAX_LENGTH,
REQUIRED_FIELD_CONFIRM_EMAIL_MSG,
USERNAME_MAX_LENGTH,
USERNAME_MIN_LENGTH,
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.tests import testutils
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
RetirementTestCase,
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_constants import SORTED_COUNTRIES
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.lib.api import test_utils
from student.tests.factories import UserFactory
from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline
from third_party_auth.tests.utils import (
@@ -49,6 +61,7 @@ from third_party_auth.tests.utils import (
ThirdPartyOAuthTestMixinGoogle
)
from util.password_policy_validators import (
DEFAULT_MAX_PASSWORD_LENGTH,
create_validator_config,
password_validators_instruction_texts,
password_validators_restrictions
@@ -1845,3 +1858,217 @@ class TestGoogleRegistrationView(
):
"""Tests the User API registration endpoint with Google authentication."""
__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)