diff --git a/common/djangoapps/student/tests/test_retirement.py b/common/djangoapps/student/tests/test_retirement.py
index 71c548559c..d09b624a9b 100644
--- a/common/djangoapps/student/tests/test_retirement.py
+++ b/common/djangoapps/student/tests/test_retirement.py
@@ -13,6 +13,7 @@ from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
+from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import (
_get_all_retired_emails_by_email,
_get_all_retired_usernames_by_username,
@@ -258,6 +259,7 @@ def test_get_potentially_retired_user_bad_hash():
@ddt.ddt
+@skip_unless_lms
class TestRegisterRetiredUsername(TestCase):
"""
Tests to ensure that retired usernames can no longer be used in registering new accounts.
diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py
index 6e29ff62f8..08dfc6d175 100644
--- a/common/djangoapps/third_party_auth/tests/specs/base.py
+++ b/common/djangoapps/third_party_auth/tests/specs/base.py
@@ -21,9 +21,9 @@ from social_django import utils as social_utils
from social_django import views as social_views
from lms.djangoapps.commerce.tests import TEST_API_URL
-from openedx.core.djangoapps.user_api.views import RegistrationView
from openedx.core.djangoapps.user_authn.views.login import login_user
from openedx.core.djangoapps.user_authn.views.login_form import login_and_registration_form
+from openedx.core.djangoapps.user_authn.views.register import RegistrationView
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context
from student import models as student_models
diff --git a/openedx/core/djangoapps/user_api/legacy_urls.py b/openedx/core/djangoapps/user_api/legacy_urls.py
index e1d12bd515..d371121e41 100644
--- a/openedx/core/djangoapps/user_api/legacy_urls.py
+++ b/openedx/core/djangoapps/user_api/legacy_urls.py
@@ -40,8 +40,6 @@ urlpatterns = [
urlpatterns += [
url(r'^v1/account/login_session/$', user_api_views.LoginSessionView.as_view(),
name="user_api_login_session"),
- url(r'^v1/account/registration/$', user_api_views.RegistrationView.as_view(),
- name="user_api_registration"),
url(r'^v1/account/password_reset/$', user_api_views.PasswordResetView.as_view(),
name="user_api_password_reset"),
]
diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py
index b1d6666f20..0c903b2246 100644
--- a/openedx/core/djangoapps/user_api/tests/test_views.py
+++ b/openedx/core/djangoapps/user_api/tests/test_views.py
@@ -57,7 +57,7 @@ from ..accounts.api import get_account_settings
from ..accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
RetirementTestCase,
fake_requested_retirement,
- setup_retirement_states
+ setup_retirement_states,
)
from ..models import UserOrgTag
from ..tests.factories import UserPreferenceFactory
@@ -774,1841 +774,6 @@ class PasswordResetViewTest(UserAPITestCase):
])
-@ddt.ddt
-@skip_unless_lms
-class RegistrationViewValidationErrorTest(ThirdPartyAuthTestMixin, UserAPITestCase, RetirementTestCase):
- """
- Tests for catching duplicate email and username validation errors within
- the registration end-points of the User API.
- """
-
- maxDiff = None
-
- USERNAME = "bob"
- EMAIL = "bob@example.com"
- PASSWORD = "password"
- NAME = "Bob Smith"
- EDUCATION = "m"
- YEAR_OF_BIRTH = "1998"
- ADDRESS = "123 Fake Street"
- CITY = "Springfield"
- COUNTRY = "us"
- GOALS = "Learn all the things!"
-
- def setUp(self):
- super(RegistrationViewValidationErrorTest, self).setUp()
- self.url = reverse("user_api_registration")
-
- @mock.patch('openedx.core.djangoapps.user_api.views.check_account_exists')
- def test_register_retired_email_validation_error(self, dummy_check_account_exists):
- dummy_check_account_exists.return_value = []
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Initiate retirement for the above user:
- fake_requested_retirement(User.objects.get(username=self.USERNAME))
-
- # Try to create a second user with the same email address as the retired user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": "Someone Else",
- "username": "someone_else",
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 400)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "email": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different email address."
- ).format(
- self.EMAIL
- )
- }]
- }
- )
-
- def test_register_retired_email_validation_error_no_bypass_check_account_exists(self):
- """
- This test is the same as above, except it doesn't bypass check_account_exists. Not bypassing this function
- results in the same error message, but a 409 status code rather than 400.
- """
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Initiate retirement for the above user:
- fake_requested_retirement(User.objects.get(username=self.USERNAME))
-
- # Try to create a second user with the same email address as the retired user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": "Someone Else",
- "username": "someone_else",
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 409)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "email": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different email address."
- ).format(
- self.EMAIL
- )
- }]
- }
- )
-
- def test_register_duplicate_retired_username_account_validation_error(self):
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Initiate retirement for the above user.
- fake_requested_retirement(User.objects.get(username=self.USERNAME))
-
- with mock.patch('openedx.core.djangoapps.user_authn.views.register.do_create_account') as dummy_do_create_acct:
- # do_create_account should *not* be called - the duplicate retired username
- # should be detected by check_account_exists before account creation is called.
- dummy_do_create_acct.side_effect = Exception('do_create_account should *not* have been called!')
- # Try to create a second user with the same username.
- response = self.client.post(self.url, {
- "email": "someone+else@example.com",
- "name": "Someone Else",
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 409)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "username": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different username."
- ).format(
- self.USERNAME
- )
- }]
- }
- )
-
- @mock.patch('openedx.core.djangoapps.user_api.views.check_account_exists')
- def test_register_duplicate_email_validation_error(self, dummy_check_account_exists):
- dummy_check_account_exists.return_value = []
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Try to create a second user with the same email address
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": "Someone Else",
- "username": "someone_else",
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 400)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "email": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different email address."
- ).format(
- self.EMAIL
- )
- }]
- }
- )
-
- @mock.patch('openedx.core.djangoapps.user_api.views.check_account_exists')
- def test_register_duplicate_username_account_validation_error(self, dummy_check_account_exists):
- dummy_check_account_exists.return_value = []
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Try to create a second user with the same username
- response = self.client.post(self.url, {
- "email": "someone+else@example.com",
- "name": "Someone Else",
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 409)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- u"success": False,
- u"username": [{
- u"user_message": (
- u"An account with the Public Username '{}' already exists."
- ).format(
- self.USERNAME
- )
- }]
- }
- )
-
- @mock.patch('openedx.core.djangoapps.user_api.views.check_account_exists')
- def test_register_duplicate_username_and_email_validation_errors(self, dummy_check_account_exists):
- dummy_check_account_exists.return_value = []
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Try to create a second user with the same username
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": "Someone Else",
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 400)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "email": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different email address."
- ).format(
- self.EMAIL
- )
- }]
- }
- )
-
-
-@ddt.ddt
-@skip_unless_lms
-class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
- """Tests for the registration end-points of the User API. """
-
- maxDiff = None
-
- USERNAME = "bob"
- EMAIL = "bob@example.com"
- PASSWORD = "password"
- NAME = "Bob Smith"
- EDUCATION = "m"
- YEAR_OF_BIRTH = "1998"
- ADDRESS = "123 Fake Street"
- CITY = "Springfield"
- COUNTRY = "us"
- GOALS = "Learn all the things!"
- PROFESSION_OPTIONS = [
- {
- "name": u'--',
- "value": u'',
- "default": True
-
- },
- {
- "value": u'software engineer',
- "name": u'Software Engineer',
- "default": False
- },
- {
- "value": u'teacher',
- "name": u'Teacher',
- "default": False
- },
- {
- "value": u'other',
- "name": u'Other',
- "default": False
- }
- ]
- SPECIALTY_OPTIONS = [
- {
- "name": u'--',
- "value": u'',
- "default": True
-
- },
- {
- "value": "aerospace",
- "name": "Aerospace",
- "default": False
- },
- {
- "value": u'early education',
- "name": u'Early Education',
- "default": False
- },
- {
- "value": u'n/a',
- "name": u'N/A',
- "default": False
- }
- ]
- link_template = u"{link_label}"
-
- def setUp(self):
- super(RegistrationViewTest, self).setUp()
- self.url = reverse("user_api_registration")
-
- @ddt.data("get", "post")
- def test_auth_disabled(self, method):
- self.assertAuthDisabled(method, self.url)
-
- def test_allowed_methods(self):
- self.assertAllowedMethods(self.url, ["GET", "POST", "HEAD", "OPTIONS"])
-
- def test_put_not_allowed(self):
- response = self.client.put(self.url)
- self.assertHttpMethodNotAllowed(response)
-
- def test_delete_not_allowed(self):
- response = self.client.delete(self.url)
- self.assertHttpMethodNotAllowed(response)
-
- def test_patch_not_allowed(self):
- response = self.client.patch(self.url)
- self.assertHttpMethodNotAllowed(response)
-
- def test_register_form_default_fields(self):
- no_extra_fields_setting = {}
-
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"email",
- u"type": u"email",
- u"required": True,
- u"label": u"Email",
- u"instructions": u"This is what you will use to login.",
- u"restrictions": {
- "min_length": EMAIL_MIN_LENGTH,
- "max_length": EMAIL_MAX_LENGTH
- },
- }
- )
-
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"name",
- u"type": u"text",
- u"required": True,
- u"label": u"Full Name",
- u"instructions": u"This name will be used on any certificates that you earn.",
- u"restrictions": {
- "max_length": 255
- },
- }
- )
-
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"username",
- u"type": u"text",
- u"required": True,
- u"label": u"Public Username",
- u"instructions": u"The name that will identify you in your courses. "
- u"It cannot be changed later.",
- u"restrictions": {
- "min_length": USERNAME_MIN_LENGTH,
- "max_length": USERNAME_MAX_LENGTH
- },
- }
- )
-
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"placeholder": "",
- u"name": u"password",
- u"type": u"password",
- u"required": True,
- u"label": u"Password",
- u"instructions": password_validators_instruction_texts(),
- u"restrictions": password_validators_restrictions(),
- }
- )
-
- @override_settings(AUTH_PASSWORD_VALIDATORS=[
- create_validator_config('util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}),
- create_validator_config('util.password_policy_validators.UppercaseValidator', {'min_upper': 3}),
- create_validator_config('util.password_policy_validators.SymbolValidator', {'min_symbol': 1}),
- ])
- def test_register_form_password_complexity(self):
- no_extra_fields_setting = {}
-
- # Without enabling password policy
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u'name': u'password',
- u'label': u'Password',
- u"instructions": password_validators_instruction_texts(),
- u"restrictions": password_validators_restrictions(),
- }
- )
-
- msg = u'Your password must contain at least 2 characters, including '\
- u'3 uppercase letters & 1 symbol.'
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u'name': u'password',
- u'label': u'Password',
- u'instructions': msg,
- u"restrictions": password_validators_restrictions(),
- }
- )
-
- @override_settings(REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm')
- def test_extension_form_fields(self):
- no_extra_fields_setting = {}
-
- # Verify other fields didn't disappear for some reason.
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"email",
- u"type": u"email",
- u"required": True,
- u"label": u"Email",
- u"instructions": u"This is what you will use to login.",
- u"restrictions": {
- "min_length": EMAIL_MIN_LENGTH,
- "max_length": EMAIL_MAX_LENGTH
- },
- }
- )
-
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"favorite_editor",
- u"type": u"select",
- u"required": False,
- u"label": u"Favorite Editor",
- u"placeholder": u"cat",
- u"defaultValue": u"vim",
- u"errorMessages": {
- u'required': u'This field is required.',
- u'invalid_choice': u'Select a valid choice. %(value)s is not one of the available choices.',
- }
- }
- )
-
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"favorite_movie",
- u"type": u"text",
- u"required": True,
- u"label": u"Fav Flick",
- u"placeholder": None,
- u"defaultValue": None,
- u"errorMessages": {
- u'required': u'Please tell us your favorite movie.',
- u'invalid': u"We're pretty sure you made that movie up."
- },
- u"restrictions": {
- "min_length": TestCaseForm.MOVIE_MIN_LEN,
- "max_length": TestCaseForm.MOVIE_MAX_LEN,
- }
- }
- )
-
- @ddt.data(
- ('pk', 'PK', 'Bob123', 'Bob123'),
- ('Pk', 'PK', None, ''),
- ('pK', 'PK', 'Bob123@edx.org', 'Bob123_edx_org'),
- ('PK', 'PK', 'Bob123123123123123123123123123123123123', 'Bob123123123123123123123123123'),
- ('us', 'US', 'Bob-1231231&23123+1231(2312312312@3123123123', 'Bob-1231231_23123_1231_2312312'),
- )
- @ddt.unpack
- def test_register_form_third_party_auth_running_google(
- self,
- input_country_code,
- expected_country_code,
- input_username,
- expected_username):
- no_extra_fields_setting = {}
- country_options = (
- [
- {
- "name": "--",
- "value": "",
- "default": False
- }
- ] + [
- {
- "value": country_code,
- "name": six.text_type(country_name),
- "default": True if country_code == expected_country_code else False
- }
- for country_code, country_name in SORTED_COUNTRIES
- ]
- )
-
- provider = self.configure_google_provider(enabled=True)
- with simulate_running_pipeline(
- "openedx.core.djangoapps.user_api.api.third_party_auth.pipeline", "google-oauth2",
- email="bob@example.com",
- fullname="Bob",
- username=input_username,
- country=input_country_code
- ):
- self._assert_password_field_hidden(no_extra_fields_setting)
- self._assert_social_auth_provider_present(no_extra_fields_setting, provider)
-
- # Email should be filled in
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"email",
- u"defaultValue": u"bob@example.com",
- u"type": u"email",
- u"required": True,
- u"label": u"Email",
- u"instructions": u"This is what you will use to login.",
- u"restrictions": {
- "min_length": EMAIL_MIN_LENGTH,
- "max_length": EMAIL_MAX_LENGTH
- },
- }
- )
-
- # Full Name should be filled in
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"name",
- u"defaultValue": u"Bob",
- u"type": u"text",
- u"required": True,
- u"label": u"Full Name",
- u"instructions": u"This name will be used on any certificates that you earn.",
- u"restrictions": {
- "max_length": NAME_MAX_LENGTH,
- }
- }
- )
-
- # Username should be filled in
- self._assert_reg_field(
- no_extra_fields_setting,
- {
- u"name": u"username",
- u"defaultValue": expected_username,
- u"type": u"text",
- u"required": True,
- u"label": u"Public Username",
- u"instructions": u"The name that will identify you in your courses. "
- u"It cannot be changed later.",
- u"restrictions": {
- "min_length": USERNAME_MIN_LENGTH,
- "max_length": USERNAME_MAX_LENGTH
- }
- }
- )
-
- # Country should be filled in.
- self._assert_reg_field(
- {u"country": u"required"},
- {
- u"label": u"Country or Region of Residence",
- u"name": u"country",
- u"defaultValue": expected_country_code,
- u"type": u"select",
- u"required": True,
- u"options": country_options,
- u"instructions": u"The country or region where you live.",
- u"errorMessages": {
- u"required": u"Select your country or region of residence."
- },
- }
- )
-
- def test_register_form_level_of_education(self):
- self._assert_reg_field(
- {"level_of_education": "optional"},
- {
- "name": "level_of_education",
- "type": "select",
- "required": False,
- "label": "Highest level of education completed",
- "options": [
- {"value": "", "name": "--", "default": True},
- {"value": "p", "name": "Doctorate", "default": False},
- {"value": "m", "name": "Master's or professional degree", "default": False},
- {"value": "b", "name": "Bachelor's degree", "default": False},
- {"value": "a", "name": "Associate degree", "default": False},
- {"value": "hs", "name": "Secondary/high school", "default": False},
- {"value": "jhs", "name": "Junior secondary/junior high/middle school", "default": False},
- {"value": "el", "name": "Elementary/primary school", "default": False},
- {"value": "none", "name": "No formal education", "default": False},
- {"value": "other", "name": "Other education", "default": False},
- ],
- "errorMessages": {
- "required": "Select the highest level of education you have completed."
- }
- }
- )
-
- @mock.patch('openedx.core.djangoapps.user_api.api._')
- def test_register_form_level_of_education_translations(self, fake_gettext):
- fake_gettext.side_effect = lambda text: text + ' TRANSLATED'
-
- self._assert_reg_field(
- {"level_of_education": "optional"},
- {
- "name": "level_of_education",
- "type": "select",
- "required": False,
- "label": "Highest level of education completed TRANSLATED",
- "options": [
- {"value": "", "name": "--", "default": True},
- {"value": "p", "name": "Doctorate TRANSLATED", "default": False},
- {"value": "m", "name": "Master's or professional degree TRANSLATED", "default": False},
- {"value": "b", "name": "Bachelor's degree TRANSLATED", "default": False},
- {"value": "a", "name": "Associate degree TRANSLATED", "default": False},
- {"value": "hs", "name": "Secondary/high school TRANSLATED", "default": False},
- {"value": "jhs", "name": "Junior secondary/junior high/middle school TRANSLATED", "default": False},
- {"value": "el", "name": "Elementary/primary school TRANSLATED", "default": False},
- {"value": "none", "name": "No formal education TRANSLATED", "default": False},
- {"value": "other", "name": "Other education TRANSLATED", "default": False},
- ],
- "errorMessages": {
- "required": "Select the highest level of education you have completed."
- }
- }
- )
-
- def test_register_form_gender(self):
- self._assert_reg_field(
- {"gender": "optional"},
- {
- "name": "gender",
- "type": "select",
- "required": False,
- "label": "Gender",
- "options": [
- {"value": "", "name": "--", "default": True},
- {"value": "m", "name": "Male", "default": False},
- {"value": "f", "name": "Female", "default": False},
- {"value": "o", "name": "Other/Prefer Not to Say", "default": False},
- ],
- }
- )
-
- @mock.patch('openedx.core.djangoapps.user_api.api._')
- def test_register_form_gender_translations(self, fake_gettext):
- fake_gettext.side_effect = lambda text: text + ' TRANSLATED'
-
- self._assert_reg_field(
- {"gender": "optional"},
- {
- "name": "gender",
- "type": "select",
- "required": False,
- "label": "Gender TRANSLATED",
- "options": [
- {"value": "", "name": "--", "default": True},
- {"value": "m", "name": "Male TRANSLATED", "default": False},
- {"value": "f", "name": "Female TRANSLATED", "default": False},
- {"value": "o", "name": "Other/Prefer Not to Say TRANSLATED", "default": False},
- ],
- }
- )
-
- def test_register_form_year_of_birth(self):
- this_year = datetime.datetime.now(UTC).year
- year_options = (
- [
- {
- "value": "",
- "name": "--",
- "default": True
- }
- ] + [
- {
- "value": six.text_type(year),
- "name": six.text_type(year),
- "default": False
- }
- for year in range(this_year, this_year - 120, -1)
- ]
- )
- self._assert_reg_field(
- {"year_of_birth": "optional"},
- {
- "name": "year_of_birth",
- "type": "select",
- "required": False,
- "label": "Year of birth",
- "options": year_options,
- }
- )
-
- def test_register_form_profession_without_profession_options(self):
- self._assert_reg_field(
- {"profession": "required"},
- {
- "name": "profession",
- "type": "text",
- "required": True,
- "label": "Profession",
- "errorMessages": {
- "required": "Enter your profession."
- }
- }
- )
-
- @with_site_configuration(configuration={"EXTRA_FIELD_OPTIONS": {"profession": ["Software Engineer", "Teacher", "Other"]}})
- def test_register_form_profession_with_profession_options(self):
- self._assert_reg_field(
- {"profession": "required"},
- {
- "name": "profession",
- "type": "select",
- "required": True,
- "label": "Profession",
- "options": self.PROFESSION_OPTIONS,
- "errorMessages": {
- "required": "Select your profession."
- },
- }
- )
-
- def test_register_form_specialty_without_specialty_options(self):
- self._assert_reg_field(
- {"specialty": "required"},
- {
- "name": "specialty",
- "type": "text",
- "required": True,
- "label": "Specialty",
- "errorMessages": {
- "required": "Enter your specialty."
- }
- }
- )
-
- @with_site_configuration(configuration={"EXTRA_FIELD_OPTIONS": {"specialty": ["Aerospace", "Early Education", "N/A"]}})
- def test_register_form_specialty_with_specialty_options(self):
- self._assert_reg_field(
- {"specialty": "required"},
- {
- "name": "specialty",
- "type": "select",
- "required": True,
- "label": "Specialty",
- "options": self.SPECIALTY_OPTIONS,
- "errorMessages": {
- "required": "Select your specialty."
- },
- }
- )
-
- def test_registration_form_mailing_address(self):
- self._assert_reg_field(
- {"mailing_address": "optional"},
- {
- "name": "mailing_address",
- "type": "textarea",
- "required": False,
- "label": "Mailing address",
- "errorMessages": {
- "required": "Enter your mailing address."
- }
- }
- )
-
- def test_registration_form_goals(self):
- self._assert_reg_field(
- {"goals": "optional"},
- {
- "name": "goals",
- "type": "textarea",
- "required": False,
- "label": u"Tell us why you're interested in {platform_name}".format(
- platform_name=settings.PLATFORM_NAME
- ),
- "errorMessages": {
- "required": "Tell us your goals."
- }
- }
- )
-
- def test_registration_form_city(self):
- self._assert_reg_field(
- {"city": "optional"},
- {
- "name": "city",
- "type": "text",
- "required": False,
- "label": "City",
- "errorMessages": {
- "required": "Enter your city."
- }
- }
- )
-
- def test_registration_form_state(self):
- self._assert_reg_field(
- {"state": "optional"},
- {
- "name": "state",
- "type": "text",
- "required": False,
- "label": "State/Province/Region",
- }
- )
-
- def test_registration_form_country(self):
- country_options = (
- [
- {
- "name": "--",
- "value": "",
- "default": True
- }
- ] + [
- {
- "value": country_code,
- "name": six.text_type(country_name),
- "default": False
- }
- for country_code, country_name in SORTED_COUNTRIES
- ]
- )
- self._assert_reg_field(
- {"country": "required"},
- {
- "label": "Country or Region of Residence",
- "name": "country",
- "type": "select",
- "instructions": "The country or region where you live.",
- "required": True,
- "options": country_options,
- "errorMessages": {
- "required": "Select your country or region of residence."
- },
- }
- )
-
- def test_registration_form_confirm_email(self):
- self._assert_reg_field(
- {"confirm_email": "required"},
- {
- "name": "confirm_email",
- "type": "text",
- "required": True,
- "label": "Confirm Email",
- "errorMessages": {
- "required": "The email addresses do not match.",
- }
- }
- )
-
- @override_settings(
- MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"},
- )
- @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
- def test_registration_honor_code_mktg_site_enabled(self):
- link_template = "{link_label}"
- link_template2 = u"{link_label}"
- link_label = "Terms of Service and Honor Code"
- link_label2 = "Privacy Policy"
- self._assert_reg_field(
- {"honor_code": "required"},
- {
- "label": (u"By creating an account, you agree to the {spacing}"
- u"{link_label} {spacing}"
- u"and you acknowledge that {platform_name} and each Member process your "
- u"personal data in accordance {spacing}"
- u"with the {link_label2}.").format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_template.format(link_label=link_label),
- link_label2=link_template2.format(link_label=link_label2),
- spacing=' ' * 18
- ),
- "name": "honor_code",
- "defaultValue": False,
- "type": "plaintext",
- "required": True,
- "errorMessages": {
- "required": u"You must agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_label
- )
- }
- }
- )
-
- @override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"})
- @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
- def test_registration_honor_code_mktg_site_disabled(self):
- link_template = "{link_label}"
- link_label = "Terms of Service and Honor Code"
- link_label2 = "Privacy Policy"
- self._assert_reg_field(
- {"honor_code": "required"},
- {
- "label": (u"By creating an account, you agree to the {spacing}"
- u"{link_label} {spacing}"
- u"and you acknowledge that {platform_name} and each Member process your "
- u"personal data in accordance {spacing}"
- u"with the {link_label2}.").format(
- platform_name=settings.PLATFORM_NAME,
- link_label=self.link_template.format(link_label=link_label),
- link_label2=link_template.format(link_label=link_label2),
- spacing=' ' * 18
- ),
- "name": "honor_code",
- "defaultValue": False,
- "type": "plaintext",
- "required": True,
- "errorMessages": {
- "required": u"You must agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_label
- )
- }
- }
- )
-
- @override_settings(MKTG_URLS={
- "ROOT": "https://www.test.com/",
- "HONOR": "honor",
- "TOS": "tos",
- })
- @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
- def test_registration_separate_terms_of_service_mktg_site_enabled(self):
- # Honor code field should say ONLY honor code,
- # not "terms of service and honor code"
- link_label = 'Honor Code'
- link_template = u"{link_label}"
- self._assert_reg_field(
- {"honor_code": "required", "terms_of_service": "required"},
- {
- "label": u"I agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_template.format(link_label=link_label)
- ),
- "name": "honor_code",
- "defaultValue": False,
- "type": "checkbox",
- "required": True,
- "errorMessages": {
- "required": u"You must agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_label
- )
- }
- }
- )
-
- # Terms of service field should also be present
- link_label = "Terms of Service"
- link_template = u"{link_label}"
- self._assert_reg_field(
- {"honor_code": "required", "terms_of_service": "required"},
- {
- "label": u"I agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_template.format(link_label=link_label)
- ),
- "name": "terms_of_service",
- "defaultValue": False,
- "type": "checkbox",
- "required": True,
- "errorMessages": {
- "required": u"You must agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_label
- )
- }
- }
- )
-
- @override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor", "TOS": "tos"})
- @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
- def test_registration_separate_terms_of_service_mktg_site_disabled(self):
- # Honor code field should say ONLY honor code,
- # not "terms of service and honor code"
- link_label = 'Honor Code'
- self._assert_reg_field(
- {"honor_code": "required", "terms_of_service": "required"},
- {
- "label": u"I agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=self.link_template.format(link_label=link_label)
- ),
- "name": "honor_code",
- "defaultValue": False,
- "type": "checkbox",
- "required": True,
- "errorMessages": {
- "required": u"You must agree to the {platform_name} Honor Code".format(
- platform_name=settings.PLATFORM_NAME
- )
- }
- }
- )
-
- link_label = 'Terms of Service'
- # Terms of service field should also be present
- link_template = u"{link_label}"
- self._assert_reg_field(
- {"honor_code": "required", "terms_of_service": "required"},
- {
- "label": u"I agree to the {platform_name} {link_label}".format(
- platform_name=settings.PLATFORM_NAME,
- link_label=link_template.format(link_label=link_label)
- ),
- "name": "terms_of_service",
- "defaultValue": False,
- "type": "checkbox",
- "required": True,
- "errorMessages": {
- "required": u"You must agree to the {platform_name} Terms of Service".format(
- platform_name=settings.PLATFORM_NAME
- )
- }
- }
- )
-
- @override_settings(
- REGISTRATION_EXTRA_FIELDS={
- "level_of_education": "optional",
- "gender": "optional",
- "year_of_birth": "optional",
- "mailing_address": "optional",
- "goals": "optional",
- "city": "optional",
- "state": "optional",
- "country": "required",
- "honor_code": "required",
- "confirm_email": "required",
- },
- REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
- )
- def test_field_order(self):
- response = self.client.get(self.url)
- self.assertHttpOK(response)
-
- # Verify that all fields render in the correct order
- form_desc = json.loads(response.content.decode('utf-8'))
- field_names = [field["name"] for field in form_desc["fields"]]
- self.assertEqual(field_names, [
- "email",
- "name",
- "username",
- "password",
- "favorite_movie",
- "favorite_editor",
- "confirm_email",
- "city",
- "state",
- "country",
- "gender",
- "year_of_birth",
- "level_of_education",
- "mailing_address",
- "goals",
- "honor_code",
- ])
-
- @override_settings(
- REGISTRATION_EXTRA_FIELDS={
- "level_of_education": "optional",
- "gender": "optional",
- "year_of_birth": "optional",
- "mailing_address": "optional",
- "goals": "optional",
- "city": "optional",
- "state": "optional",
- "country": "required",
- "honor_code": "required",
- "confirm_email": "required",
- },
- REGISTRATION_FIELD_ORDER=[
- "name",
- "username",
- "email",
- "confirm_email",
- "password",
- "first_name",
- "last_name",
- "city",
- "state",
- "country",
- "gender",
- "year_of_birth",
- "level_of_education",
- "company",
- "title",
- "job_title",
- "mailing_address",
- "goals",
- "honor_code",
- "terms_of_service",
- "specialty",
- "profession",
- ],
- )
- def test_field_order_override(self):
- response = self.client.get(self.url)
- self.assertHttpOK(response)
-
- # Verify that all fields render in the correct order
- form_desc = json.loads(response.content.decode('utf-8'))
- field_names = [field["name"] for field in form_desc["fields"]]
- self.assertEqual(field_names, [
- "name",
- "username",
- "email",
- "confirm_email",
- "password",
- "city",
- "state",
- "country",
- "gender",
- "year_of_birth",
- "level_of_education",
- "mailing_address",
- "goals",
- "honor_code",
- ])
-
- @override_settings(
- REGISTRATION_EXTRA_FIELDS={
- "level_of_education": "optional",
- "gender": "optional",
- "year_of_birth": "optional",
- "mailing_address": "optional",
- "goals": "optional",
- "city": "optional",
- "state": "optional",
- "country": "required",
- "honor_code": "required",
- "confirm_email": "required",
- },
- REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
- REGISTRATION_FIELD_ORDER=[
- "name",
- "confirm_email",
- "password",
- "first_name",
- "last_name",
- "gender",
- "year_of_birth",
- "level_of_education",
- "company",
- "title",
- "mailing_address",
- "goals",
- "honor_code",
- "terms_of_service",
- ],
- )
- def test_field_order_invalid_override(self):
- response = self.client.get(self.url)
- self.assertHttpOK(response)
-
- # Verify that all fields render in the correct order
- form_desc = json.loads(response.content.decode('utf-8'))
- field_names = [field["name"] for field in form_desc["fields"]]
- self.assertEqual(field_names, [
- "email",
- "name",
- "username",
- "password",
- "favorite_movie",
- "favorite_editor",
- "confirm_email",
- "city",
- "state",
- "country",
- "gender",
- "year_of_birth",
- "level_of_education",
- "mailing_address",
- "goals",
- "honor_code",
- ])
-
- def test_register(self):
- # Create a new registration
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
- self.assertIn(settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, self.client.cookies)
- self.assertIn(settings.EDXMKTG_USER_INFO_COOKIE_NAME, self.client.cookies)
-
- user = User.objects.get(username=self.USERNAME)
- request = RequestFactory().get('/url')
- request.user = user
- account_settings = get_account_settings(request)[0]
-
- self.assertEqual(self.USERNAME, account_settings["username"])
- self.assertEqual(self.EMAIL, account_settings["email"])
- self.assertFalse(account_settings["is_active"])
- self.assertEqual(self.NAME, account_settings["name"])
-
- # Verify that we've been logged in
- # by trying to access a page that requires authentication
- response = self.client.get(reverse("dashboard"))
- self.assertHttpOK(response)
-
- @override_settings(REGISTRATION_EXTRA_FIELDS={
- "level_of_education": "optional",
- "gender": "optional",
- "year_of_birth": "optional",
- "mailing_address": "optional",
- "goals": "optional",
- "country": "required",
- })
- def test_register_with_profile_info(self):
- # Register, providing lots of demographic info
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "level_of_education": self.EDUCATION,
- "mailing_address": self.ADDRESS,
- "year_of_birth": self.YEAR_OF_BIRTH,
- "goals": self.GOALS,
- "country": self.COUNTRY,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Verify the user's account
- user = User.objects.get(username=self.USERNAME)
- request = RequestFactory().get('/url')
- request.user = user
- account_settings = get_account_settings(request)[0]
-
- self.assertEqual(account_settings["level_of_education"], self.EDUCATION)
- self.assertEqual(account_settings["mailing_address"], self.ADDRESS)
- self.assertEqual(account_settings["year_of_birth"], int(self.YEAR_OF_BIRTH))
- self.assertEqual(account_settings["goals"], self.GOALS)
- self.assertEqual(account_settings["country"], self.COUNTRY)
-
- @override_settings(REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm')
- @mock.patch('openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm.DUMMY_STORAGE', new_callable=dict)
- @mock.patch(
- 'openedx.core.djangoapps.user_api.tests.test_helpers.DummyRegistrationExtensionModel',
- )
- def test_with_extended_form(self, dummy_model, storage_dict):
- dummy_model_instance = mock.Mock()
- dummy_model.return_value = dummy_model_instance
- # Create a new registration
- self.assertEqual(storage_dict, {})
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- "favorite_movie": "Inception",
- "favorite_editor": "cat",
- })
- self.assertHttpOK(response)
- self.assertIn(settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, self.client.cookies)
- self.assertIn(settings.EDXMKTG_USER_INFO_COOKIE_NAME, self.client.cookies)
-
- user = User.objects.get(username=self.USERNAME)
- request = RequestFactory().get('/url')
- request.user = user
- account_settings = get_account_settings(request)[0]
-
- self.assertEqual(self.USERNAME, account_settings["username"])
- self.assertEqual(self.EMAIL, account_settings["email"])
- self.assertFalse(account_settings["is_active"])
- self.assertEqual(self.NAME, account_settings["name"])
-
- self.assertEqual(storage_dict, {'favorite_movie': "Inception", "favorite_editor": "cat"})
- self.assertEqual(dummy_model_instance.user, user)
-
- # Verify that we've been logged in
- # by trying to access a page that requires authentication
- response = self.client.get(reverse("dashboard"))
- self.assertHttpOK(response)
-
- def test_activation_email(self):
- # Register, which should trigger an activation email
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Verify that the activation email was sent
- self.assertEqual(len(mail.outbox), 1)
- sent_email = mail.outbox[0]
- self.assertEqual(sent_email.to, [self.EMAIL])
- self.assertEqual(
- sent_email.subject,
- u"Action Required: Activate your {platform} account".format(platform=settings.PLATFORM_NAME)
- )
- self.assertIn(
- u"high-quality {platform} courses".format(platform=settings.PLATFORM_NAME),
- sent_email.body
- )
-
- @ddt.data(
- {"email": ""},
- {"email": "invalid"},
- {"name": ""},
- {"username": ""},
- {"username": "a"},
- {"password": ""},
- )
- def test_register_invalid_input(self, invalid_fields):
- # Initially, the field values are all valid
- data = {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- }
-
- # Override the valid fields, making the input invalid
- data.update(invalid_fields)
-
- # Attempt to create the account, expecting an error response
- response = self.client.post(self.url, data)
- self.assertHttpBadRequest(response)
-
- @override_settings(REGISTRATION_EXTRA_FIELDS={"country": "required"})
- @ddt.data("email", "name", "username", "password", "country")
- def test_register_missing_required_field(self, missing_field):
- data = {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "country": self.COUNTRY,
- }
-
- del data[missing_field]
-
- # Send a request missing a field
- response = self.client.post(self.url, data)
- self.assertHttpBadRequest(response)
-
- def test_register_duplicate_email(self):
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Try to create a second user with the same email address
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": "Someone Else",
- "username": "someone_else",
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 409)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "email": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different email address."
- ).format(
- self.EMAIL
- )
- }]
- }
- )
-
- def test_register_duplicate_username(self):
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Try to create a second user with the same username
- response = self.client.post(self.url, {
- "email": "someone+else@example.com",
- "name": "Someone Else",
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 409)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "username": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different username."
- ).format(
- self.USERNAME
- )
- }]
- }
- )
-
- def test_register_duplicate_username_and_email(self):
- # Register the first user
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertHttpOK(response)
-
- # Try to create a second user with the same username
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": "Someone Else",
- "username": self.USERNAME,
- "password": self.PASSWORD,
- "honor_code": "true",
- })
- self.assertEqual(response.status_code, 409)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "username": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different username."
- ).format(
- self.USERNAME
- )
- }],
- "email": [{
- "user_message": (
- u"It looks like {} belongs to an existing account. "
- "Try again with a different email address."
- ).format(
- self.EMAIL
- )
- }]
- }
- )
-
- @override_settings(REGISTRATION_EXTRA_FIELDS={"honor_code": "hidden", "terms_of_service": "hidden"})
- def test_register_hidden_honor_code_and_terms_of_service(self):
- response = self.client.post(self.url, {
- "email": self.EMAIL,
- "name": self.NAME,
- "username": self.USERNAME,
- "password": self.PASSWORD,
- })
- self.assertHttpOK(response)
-
- def test_missing_fields(self):
- response = self.client.post(
- self.url,
- {
- "email": self.EMAIL,
- "name": self.NAME,
- "honor_code": "true",
- }
- )
- self.assertEqual(response.status_code, 400)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- u"success": False,
- u"username": [{u"user_message": USERNAME_BAD_LENGTH_MSG}],
- u"password": [{u"user_message": u"This field is required."}],
- }
- )
-
- def test_country_overrides(self):
- """Test that overridden countries are available in country list."""
- # Retrieve the registration form description
- with override_settings(REGISTRATION_EXTRA_FIELDS={"country": "required"}):
- response = self.client.get(self.url)
- self.assertHttpOK(response)
-
- self.assertContains(response, 'Kosovo')
-
- def test_create_account_not_allowed(self):
- """
- Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
- """
- def _side_effect_for_get_value(value, default=None):
- """
- returns a side_effect with given return value for a given value
- """
- if value == 'ALLOW_PUBLIC_ACCOUNT_CREATION':
- return False
- else:
- return get_value(value, default)
-
- with mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_value') as mock_get_value:
- mock_get_value.side_effect = _side_effect_for_get_value
- response = self.client.post(self.url, {"email": self.EMAIL, "username": self.USERNAME})
- self.assertEqual(response.status_code, 403)
-
- def _assert_fields_match(self, actual_field, expected_field):
- self.assertIsNot(
- actual_field, None,
- msg=u"Could not find field {name}".format(name=expected_field["name"])
- )
-
- for key in expected_field:
- self.assertEqual(
- actual_field[key], expected_field[key],
- msg=u"Expected {expected} for {key} but got {actual} instead".format(
- key=key,
- actual=actual_field[key],
- expected=expected_field[key]
- )
- )
-
- def _populate_always_present_fields(self, field):
- defaults = [
- ("label", ""),
- ("instructions", ""),
- ("placeholder", ""),
- ("defaultValue", ""),
- ("restrictions", {}),
- ("errorMessages", {}),
- ]
- field.update({
- key: value
- for key, value in defaults if key not in field
- })
-
- def _assert_reg_field(self, extra_fields_setting, expected_field):
- """Retrieve the registration form description from the server and
- verify that it contains the expected field.
-
- Args:
- extra_fields_setting (dict): Override the Django setting controlling
- which extra fields are displayed in the form.
-
- expected_field (dict): The field definition we expect to find in the form.
-
- Raises:
- AssertionError
-
- """
- # Add in fields that are always present
- self._populate_always_present_fields(expected_field)
-
- # Retrieve the registration form description
- with override_settings(REGISTRATION_EXTRA_FIELDS=extra_fields_setting):
- response = self.client.get(self.url)
- self.assertHttpOK(response)
-
- # Verify that the form description matches what we'd expect
- form_desc = json.loads(response.content.decode('utf-8'))
-
- actual_field = None
- for field in form_desc["fields"]:
- if field["name"] == expected_field["name"]:
- actual_field = field
- break
-
- self._assert_fields_match(actual_field, expected_field)
-
- def _assert_password_field_hidden(self, field_settings):
- self._assert_reg_field(field_settings, {
- "name": "password",
- "type": "hidden",
- "required": False
- })
-
- def _assert_social_auth_provider_present(self, field_settings, backend):
- self._assert_reg_field(field_settings, {
- "name": "social_auth_provider",
- "type": "hidden",
- "required": False,
- "defaultValue": backend.name
- })
-
-
-@httpretty.activate
-@ddt.ddt
-class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin, CacheIsolationTestCase):
- """
- Tests for the User API registration endpoint with 3rd party authentication.
- """
- CREATE_USER = False
-
- ENABLED_CACHES = ['default']
-
- __test__ = False
-
- def setUp(self):
- super(ThirdPartyRegistrationTestMixin, self).setUp()
- self.url = reverse('user_api_registration')
-
- def tearDown(self):
- super(ThirdPartyRegistrationTestMixin, self).tearDown()
- Partial.objects.all().delete()
-
- def data(self, user=None):
- """Returns the request data for the endpoint."""
- return {
- "provider": self.BACKEND,
- "access_token": self.access_token,
- "client_id": self.client_id,
- "honor_code": "true",
- "country": "US",
- "username": user.username if user else "test_username",
- "name": user.first_name if user else "test name",
- "email": user.email if user else "test@test.com"
- }
-
- def _assert_existing_user_error(self, response):
- """Assert that the given response was an error with the given status_code and error code."""
- self.assertEqual(response.status_code, 409)
- errors = json.loads(response.content.decode('utf-8'))
- for conflict_attribute in ["username", "email"]:
- self.assertIn(conflict_attribute, errors)
- self.assertIn("belongs to an existing account", errors[conflict_attribute][0]["user_message"])
-
- def _assert_access_token_error(self, response, expected_error_message):
- """Assert that the given response was an error for the access_token field with the given error message."""
- self.assertEqual(response.status_code, 400)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "access_token": [{"user_message": expected_error_message}],
- }
- )
-
- def _assert_third_party_session_expired_error(self, response, expected_error_message):
- """Assert that given response is an error due to third party session expiry"""
- self.assertEqual(response.status_code, 400)
- response_json = json.loads(response.content.decode('utf-8'))
- self.assertDictEqual(
- response_json,
- {
- "success": False,
- "session_expired": [{"user_message": expected_error_message}],
- }
- )
-
- def _verify_user_existence(self, user_exists, social_link_exists, user_is_active=None, username=None):
- """Verifies whether the user object exists."""
- users = User.objects.filter(username=(username if username else "test_username"))
- self.assertEquals(users.exists(), user_exists)
- if user_exists:
- self.assertEquals(users[0].is_active, user_is_active)
- self.assertEqual(
- UserSocialAuth.objects.filter(user=users[0], provider=self.BACKEND).exists(),
- social_link_exists
- )
- else:
- self.assertEquals(UserSocialAuth.objects.count(), 0)
-
- def test_success(self):
- self._verify_user_existence(user_exists=False, social_link_exists=False)
-
- self._setup_provider_response(success=True)
- response = self.client.post(self.url, self.data())
- self.assertEqual(response.status_code, 200)
-
- self._verify_user_existence(user_exists=True, social_link_exists=True, user_is_active=False)
-
- def test_unlinked_active_user(self):
- user = UserFactory()
- response = self.client.post(self.url, self.data(user))
- self._assert_existing_user_error(response)
- self._verify_user_existence(
- user_exists=True, social_link_exists=False, user_is_active=True, username=user.username
- )
-
- def test_unlinked_inactive_user(self):
- user = UserFactory(is_active=False)
- response = self.client.post(self.url, self.data(user))
- self._assert_existing_user_error(response)
- self._verify_user_existence(
- user_exists=True, social_link_exists=False, user_is_active=False, username=user.username
- )
-
- def test_user_already_registered(self):
- self._setup_provider_response(success=True)
- user = UserFactory()
- UserSocialAuth.objects.create(user=user, provider=self.BACKEND, uid=self.social_uid)
- response = self.client.post(self.url, self.data(user))
- self._assert_existing_user_error(response)
- self._verify_user_existence(
- user_exists=True, social_link_exists=True, user_is_active=True, username=user.username
- )
-
- def test_social_user_conflict(self):
- self._setup_provider_response(success=True)
- user = UserFactory()
- UserSocialAuth.objects.create(user=user, provider=self.BACKEND, uid=self.social_uid)
- response = self.client.post(self.url, self.data())
- self._assert_access_token_error(response, "The provided access_token is already associated with another user.")
- self._verify_user_existence(
- user_exists=True, social_link_exists=True, user_is_active=True, username=user.username
- )
-
- def test_invalid_token(self):
- self._setup_provider_response(success=False)
- response = self.client.post(self.url, self.data())
- self._assert_access_token_error(response, "The provided access_token is not valid.")
- self._verify_user_existence(user_exists=False, social_link_exists=False)
-
- def test_missing_token(self):
- data = self.data()
- data.pop("access_token")
- response = self.client.post(self.url, data)
- self._assert_access_token_error(
- response,
- u"An access_token is required when passing value ({}) for provider.".format(self.BACKEND)
- )
- self._verify_user_existence(user_exists=False, social_link_exists=False)
-
- def test_expired_pipeline(self):
-
- """
- Test that there is an error and account is not created
- when request is made for account creation using third (Google, Facebook etc) party with pipeline
- getting expired using browser (not mobile application).
-
- NOTE: We are NOT using actual pipeline here so pipeline is always expired in this environment.
- we don't have to explicitly expire pipeline.
-
- """
-
- data = self.data()
- # provider is sent along request when request is made from mobile application
- data.pop("provider")
- # to identify that request is made using browser
- data.update({"social_auth_provider": "Google"})
- response = self.client.post(self.url, data)
- self._assert_third_party_session_expired_error(
- response,
- u"Registration using {provider} has timed out.".format(provider="Google")
- )
- self._verify_user_existence(user_exists=False, social_link_exists=False)
-
-
-@skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
-class TestFacebookRegistrationView(
- ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase
-):
- """Tests the User API registration endpoint with Facebook authentication."""
- __test__ = True
-
- def test_social_auth_exception(self):
- """
- According to the do_auth method in social_core.backends.facebook.py,
- the Facebook API sometimes responds back a JSON with just False as value.
- """
- self._setup_provider_response_with_body(200, json.dumps("false"))
- response = self.client.post(self.url, self.data())
- self._assert_access_token_error(response, "The provided access_token is not valid.")
- self._verify_user_existence(user_exists=False, social_link_exists=False)
-
-
-@skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
-class TestGoogleRegistrationView(
- ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase
-):
- """Tests the User API registration endpoint with Google authentication."""
- __test__ = True
-
-
@ddt.ddt
@skip_unless_lms
class UpdateEmailOptInTestCase(UserAPITestCase, SharedModuleStoreTestCase):
diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py
index d4f674ed67..08535c3d87 100644
--- a/openedx/core/djangoapps/user_api/views.py
+++ b/openedx/core/djangoapps/user_api/views.py
@@ -37,8 +37,6 @@ from openedx.core.djangoapps.user_api.serializers import (
UserPreferenceSerializer,
UserSerializer
)
-from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies
-from openedx.core.djangoapps.user_authn.views.register import create_account_with_params
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
from student.helpers import AccountValidationError
from util.json_request import JsonResponse
@@ -97,114 +95,6 @@ class LoginSessionView(APIView):
return super(LoginSessionView, self).dispatch(request, *args, **kwargs)
-class RegistrationView(APIView):
- # pylint: disable=missing-docstring
- """HTTP end-points for creating a new user. """
-
- # This end-point is available to anonymous users,
- # so do not require authentication.
- authentication_classes = []
-
- @method_decorator(transaction.non_atomic_requests)
- @method_decorator(sensitive_post_parameters("password"))
- def dispatch(self, request, *args, **kwargs):
- return super(RegistrationView, self).dispatch(request, *args, **kwargs)
-
- @method_decorator(ensure_csrf_cookie)
- def get(self, request):
- return HttpResponse(RegistrationFormFactory().get_registration_form(request).to_json(),
- content_type="application/json")
-
- @method_decorator(csrf_exempt)
- def post(self, request):
- """Create the user's account.
-
- You must send all required form fields with the request.
-
- You can optionally send a "course_id" param to indicate in analytics
- events that the user registered while enrolling in a particular course.
-
- Arguments:
- request (HTTPRequest)
-
- Returns:
- HttpResponse: 200 on success
- HttpResponse: 400 if the request is not valid.
- HttpResponse: 409 if an account with the given username or email
- address already exists
- HttpResponse: 403 operation not allowed
- """
- data = request.POST.copy()
- self._handle_terms_of_service(data)
-
- response = self._handle_duplicate_email_username(data)
- if response:
- return response
-
- response, user = self._create_account(request, data)
- if response:
- return response
-
- response = self._create_response({}, status_code=200)
- set_logged_in_cookies(request, response, user)
- return response
-
- def _handle_duplicate_email_username(self, data):
- # TODO Verify whether this check is needed here - it may be duplicated in user_api.
- email = data.get('email')
- username = data.get('username')
-
- conflicts = check_account_exists(email=email, username=username)
- if conflicts:
- conflict_messages = {
- "email": accounts.EMAIL_CONFLICT_MSG.format(email_address=email),
- "username": accounts.USERNAME_CONFLICT_MSG.format(username=username),
- }
- errors = {
- field: [{"user_message": conflict_messages[field]}]
- for field in conflicts
- }
- return self._create_response(errors, status_code=409)
-
- def _handle_terms_of_service(self, data):
- # Backwards compatibility: the student view expects both
- # terms of service and honor code values. Since we're combining
- # these into a single checkbox, the only value we may get
- # from the new view is "honor_code".
- # Longer term, we will need to make this more flexible to support
- # open source installations that may have separate checkboxes
- # for TOS, privacy policy, etc.
- if data.get("honor_code") and "terms_of_service" not in data:
- data["terms_of_service"] = data["honor_code"]
-
- def _create_account(self, request, data):
- response, user = None, None
- try:
- user = create_account_with_params(request, data)
- except AccountValidationError as err:
- errors = {
- err.field: [{"user_message": text_type(err)}]
- }
- response = self._create_response(errors, status_code=409)
- except ValidationError as err:
- # Should only get field errors from this exception
- assert NON_FIELD_ERRORS not in err.message_dict
- # Only return first error for each field
- errors = {
- field: [{"user_message": error} for error in error_list]
- for field, error_list in err.message_dict.items()
- }
- response = self._create_response(errors, status_code=400)
- except PermissionDenied:
- response = HttpResponseForbidden(_("Account creation not allowed."))
-
- return response, user
-
- def _create_response(self, response_dict, status_code):
- response_dict['success'] = (status_code == 200)
- return JsonResponse(response_dict, status=status_code)
-
-
class PasswordResetView(APIView):
"""HTTP end-point for GETting a description of the password reset form. """
diff --git a/openedx/core/djangoapps/user_authn/urls_common.py b/openedx/core/djangoapps/user_authn/urls_common.py
index ab3fa911c8..314c1fbc49 100644
--- a/openedx/core/djangoapps/user_authn/urls_common.py
+++ b/openedx/core/djangoapps/user_authn/urls_common.py
@@ -11,7 +11,7 @@ from __future__ import absolute_import
from django.conf import settings
from django.conf.urls import url
-from openedx.core.djangoapps.user_api.views import RegistrationView
+from .views.register import RegistrationView
from .views import auto_auth, login, logout
@@ -21,6 +21,8 @@ urlpatterns = [
url(r'^login_ajax$', login.login_user, name="login"),
url(r'^login_ajax/(?P[^/]*)$', login.login_user),
url(r'^login_refresh$', login.login_refresh, name="login_refresh"),
+ url(r'^user_api/v1/account/registration/$', RegistrationView.as_view(),
+ name="user_api_registration"),
url(r'^logout$', logout.LogoutView.as_view(), name='logout'),
]
diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py
index f3b4f33ee8..fb327ea3ee 100644
--- a/openedx/core/djangoapps/user_authn/views/register.py
+++ b/openedx/core/djangoapps/user_authn/views/register.py
@@ -11,15 +11,21 @@ import logging
from django.conf import settings
from django.contrib.auth import login as django_login
from django.contrib.auth.models import User
+from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied
from django.core.validators import ValidationError, validate_email
from django.db import transaction
from django.dispatch import Signal
+from django.http import HttpResponse, HttpResponseForbidden
+from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
+from django.views.decorators.debug import sensitive_post_parameters
from django.urls import reverse
+from django.utils.decorators import method_decorator
from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from pytz import UTC
from requests import HTTPError
from six import text_type
+from rest_framework.views import APIView
from social_core.exceptions import AuthAlreadyAssociated, AuthException
from social_django import utils as social_utils
@@ -30,16 +36,25 @@ 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.api import RegistrationFormFactory
+from openedx.core.djangoapps.user_api.accounts.api import check_account_exists
from openedx.core.djangoapps.user_api.accounts.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
from student.forms import AccountCreationForm, get_registration_extension_form
-from student.helpers import authenticate_new_user, create_or_set_user_attribute_created_on_site, do_create_account
+from student.helpers import (
+ authenticate_new_user,
+ create_or_set_user_attribute_created_on_site,
+ do_create_account,
+ AccountValidationError,
+)
from student.models import RegistrationCookieConfiguration, UserAttribute, create_comments_service_user
from student.views import compose_and_send_activation_email
from third_party_auth import pipeline, provider
from third_party_auth.saml import SAP_SUCCESSFACTORS_SAML_KEY
from track import segment
from util.db import outer_atomic
+from util.json_request import JsonResponse
log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
@@ -386,3 +401,111 @@ def _record_utm_registration_attribution(request, user):
REGISTRATION_UTM_CREATED_AT,
created_at_datetime
)
+
+
+class RegistrationView(APIView):
+ # pylint: disable=missing-docstring
+ """HTTP end-points for creating a new user. """
+
+ # This end-point is available to anonymous users,
+ # so do not require authentication.
+ authentication_classes = []
+
+ @method_decorator(transaction.non_atomic_requests)
+ @method_decorator(sensitive_post_parameters("password"))
+ def dispatch(self, request, *args, **kwargs):
+ return super(RegistrationView, self).dispatch(request, *args, **kwargs)
+
+ @method_decorator(ensure_csrf_cookie)
+ def get(self, request):
+ return HttpResponse(RegistrationFormFactory().get_registration_form(request).to_json(),
+ content_type="application/json")
+
+ @method_decorator(csrf_exempt)
+ def post(self, request):
+ """Create the user's account.
+
+ You must send all required form fields with the request.
+
+ You can optionally send a "course_id" param to indicate in analytics
+ events that the user registered while enrolling in a particular course.
+
+ Arguments:
+ request (HTTPRequest)
+
+ Returns:
+ HttpResponse: 200 on success
+ HttpResponse: 400 if the request is not valid.
+ HttpResponse: 409 if an account with the given username or email
+ address already exists
+ HttpResponse: 403 operation not allowed
+ """
+ data = request.POST.copy()
+ self._handle_terms_of_service(data)
+
+ response = self._handle_duplicate_email_username(data)
+ if response:
+ return response
+
+ response, user = self._create_account(request, data)
+ if response:
+ return response
+
+ response = self._create_response({}, status_code=200)
+ set_logged_in_cookies(request, response, user)
+ return response
+
+ def _handle_duplicate_email_username(self, data):
+ # TODO Verify whether this check is needed here - it may be duplicated in user_api.
+ email = data.get('email')
+ username = data.get('username')
+
+ conflicts = check_account_exists(email=email, username=username)
+ if conflicts:
+ conflict_messages = {
+ "email": accounts_settings.EMAIL_CONFLICT_MSG.format(email_address=email), # pylint: disable=no-member
+ "username": accounts_settings.USERNAME_CONFLICT_MSG.format(username=username), # pylint: disable=no-member
+ }
+ errors = {
+ field: [{"user_message": conflict_messages[field]}]
+ for field in conflicts
+ }
+ return self._create_response(errors, status_code=409)
+
+ def _handle_terms_of_service(self, data):
+ # Backwards compatibility: the student view expects both
+ # terms of service and honor code values. Since we're combining
+ # these into a single checkbox, the only value we may get
+ # from the new view is "honor_code".
+ # Longer term, we will need to make this more flexible to support
+ # open source installations that may have separate checkboxes
+ # for TOS, privacy policy, etc.
+ if data.get("honor_code") and "terms_of_service" not in data:
+ data["terms_of_service"] = data["honor_code"]
+
+ def _create_account(self, request, data):
+ response, user = None, None
+ try:
+ user = create_account_with_params(request, data)
+ except AccountValidationError as err:
+ errors = {
+ err.field: [{"user_message": text_type(err)}]
+ }
+ response = self._create_response(errors, status_code=409)
+ except ValidationError as err:
+ # Should only get field errors from this exception
+ assert NON_FIELD_ERRORS not in err.message_dict
+ # Only return first error for each field
+ errors = {
+ field: [{"user_message": error} for error in error_list]
+ for field, error_list in err.message_dict.items()
+ }
+ response = self._create_response(errors, status_code=400)
+ except PermissionDenied:
+ response = HttpResponseForbidden(_("Account creation not allowed."))
+
+ return response, user
+
+ def _create_response(self, response_dict, status_code):
+ response_dict['success'] = (status_code == 200)
+ return JsonResponse(response_dict, status=status_code)
diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py
new file mode 100644
index 0000000000..5767eefbac
--- /dev/null
+++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py
@@ -0,0 +1,1900 @@
+# -*- coding: utf-8 -*-
+"""Tests for account creation"""
+from __future__ import absolute_import
+
+import json
+from unittest import skipUnless
+from datetime import datetime
+
+import ddt
+import httpretty
+import mock
+import six
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core import mail
+from django.test import TransactionTestCase
+from django.test.client import RequestFactory
+from django.test.utils import override_settings
+from django.urls import reverse
+from pytz import UTC
+
+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_MAX_LENGTH,
+ EMAIL_MIN_LENGTH,
+ NAME_MAX_LENGTH,
+ USERNAME_MAX_LENGTH,
+ USERNAME_MIN_LENGTH,
+ USERNAME_BAD_LENGTH_MSG,
+)
+from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
+from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
+ RetirementTestCase,
+ fake_requested_retirement,
+ setup_retirement_states,
+)
+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.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
+from student.tests.factories import UserFactory
+from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline
+from third_party_auth.tests.utils import (
+ ThirdPartyOAuthTestMixin,
+ ThirdPartyOAuthTestMixinFacebook,
+ ThirdPartyOAuthTestMixinGoogle
+)
+from util.password_policy_validators import (
+ create_validator_config,
+ password_validators_instruction_texts,
+ password_validators_restrictions
+)
+
+
+@ddt.ddt
+@skip_unless_lms
+class RegistrationViewValidationErrorTest(ThirdPartyAuthTestMixin, UserAPITestCase, RetirementTestCase):
+ """
+ Tests for catching duplicate email and username validation errors within
+ the registration end-points of the User API.
+ """
+
+ maxDiff = None
+
+ USERNAME = "bob"
+ EMAIL = "bob@example.com"
+ PASSWORD = "password"
+ NAME = "Bob Smith"
+ EDUCATION = "m"
+ YEAR_OF_BIRTH = "1998"
+ ADDRESS = "123 Fake Street"
+ CITY = "Springfield"
+ COUNTRY = "us"
+ GOALS = "Learn all the things!"
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super(RegistrationViewValidationErrorTest, self).setUp()
+ self.url = reverse("user_api_registration")
+
+ @mock.patch('openedx.core.djangoapps.user_authn.views.register.check_account_exists')
+ def test_register_retired_email_validation_error(self, dummy_check_account_exists):
+ dummy_check_account_exists.return_value = []
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Initiate retirement for the above user:
+ fake_requested_retirement(User.objects.get(username=self.USERNAME))
+
+ # Try to create a second user with the same email address as the retired user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": "Someone Else",
+ "username": "someone_else",
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 400)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "email": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different email address."
+ ).format(
+ self.EMAIL
+ )
+ }]
+ }
+ )
+
+ def test_register_retired_email_validation_error_no_bypass_check_account_exists(self):
+ """
+ This test is the same as above, except it doesn't bypass check_account_exists. Not bypassing this function
+ results in the same error message, but a 409 status code rather than 400.
+ """
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Initiate retirement for the above user:
+ fake_requested_retirement(User.objects.get(username=self.USERNAME))
+
+ # Try to create a second user with the same email address as the retired user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": "Someone Else",
+ "username": "someone_else",
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 409)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "email": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different email address."
+ ).format(
+ self.EMAIL
+ )
+ }]
+ }
+ )
+
+ def test_register_duplicate_retired_username_account_validation_error(self):
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Initiate retirement for the above user.
+ fake_requested_retirement(User.objects.get(username=self.USERNAME))
+
+ with mock.patch('openedx.core.djangoapps.user_authn.views.register.do_create_account') as dummy_do_create_acct:
+ # do_create_account should *not* be called - the duplicate retired username
+ # should be detected by check_account_exists before account creation is called.
+ dummy_do_create_acct.side_effect = Exception('do_create_account should *not* have been called!')
+ # Try to create a second user with the same username.
+ response = self.client.post(self.url, {
+ "email": "someone+else@example.com",
+ "name": "Someone Else",
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 409)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "username": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different username."
+ ).format(
+ self.USERNAME
+ )
+ }]
+ }
+ )
+
+ @mock.patch('openedx.core.djangoapps.user_authn.views.register.check_account_exists')
+ def test_register_duplicate_email_validation_error(self, dummy_check_account_exists):
+ dummy_check_account_exists.return_value = []
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Try to create a second user with the same email address
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": "Someone Else",
+ "username": "someone_else",
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 400)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "email": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different email address."
+ ).format(
+ self.EMAIL
+ )
+ }]
+ }
+ )
+
+ @mock.patch('openedx.core.djangoapps.user_authn.views.register.check_account_exists')
+ def test_register_duplicate_username_account_validation_error(self, dummy_check_account_exists):
+ dummy_check_account_exists.return_value = []
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Try to create a second user with the same username
+ response = self.client.post(self.url, {
+ "email": "someone+else@example.com",
+ "name": "Someone Else",
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 409)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ u"success": False,
+ u"username": [{
+ u"user_message": (
+ u"An account with the Public Username '{}' already exists."
+ ).format(
+ self.USERNAME
+ )
+ }]
+ }
+ )
+
+ @mock.patch('openedx.core.djangoapps.user_authn.views.register.check_account_exists')
+ def test_register_duplicate_username_and_email_validation_errors(self, dummy_check_account_exists):
+ dummy_check_account_exists.return_value = []
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Try to create a second user with the same username
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": "Someone Else",
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 400)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "email": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different email address."
+ ).format(
+ self.EMAIL
+ )
+ }]
+ }
+ )
+
+
+@ddt.ddt
+@skip_unless_lms
+class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
+ """Tests for the registration end-points of the User API. """
+
+ maxDiff = None
+
+ USERNAME = "bob"
+ EMAIL = "bob@example.com"
+ PASSWORD = "password"
+ NAME = "Bob Smith"
+ EDUCATION = "m"
+ YEAR_OF_BIRTH = "1998"
+ ADDRESS = "123 Fake Street"
+ CITY = "Springfield"
+ COUNTRY = "us"
+ GOALS = "Learn all the things!"
+ PROFESSION_OPTIONS = [
+ {
+ "name": u'--',
+ "value": u'',
+ "default": True
+
+ },
+ {
+ "value": u'software engineer',
+ "name": u'Software Engineer',
+ "default": False
+ },
+ {
+ "value": u'teacher',
+ "name": u'Teacher',
+ "default": False
+ },
+ {
+ "value": u'other',
+ "name": u'Other',
+ "default": False
+ }
+ ]
+ SPECIALTY_OPTIONS = [
+ {
+ "name": u'--',
+ "value": u'',
+ "default": True
+
+ },
+ {
+ "value": "aerospace",
+ "name": "Aerospace",
+ "default": False
+ },
+ {
+ "value": u'early education',
+ "name": u'Early Education',
+ "default": False
+ },
+ {
+ "value": u'n/a',
+ "name": u'N/A',
+ "default": False
+ }
+ ]
+ link_template = u"{link_label}"
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super(RegistrationViewTest, self).setUp()
+ self.url = reverse("user_api_registration")
+
+ @ddt.data("get", "post")
+ def test_auth_disabled(self, method):
+ self.assertAuthDisabled(method, self.url)
+
+ def test_allowed_methods(self):
+ self.assertAllowedMethods(self.url, ["GET", "POST", "HEAD", "OPTIONS"])
+
+ def test_put_not_allowed(self):
+ response = self.client.put(self.url)
+ self.assertHttpMethodNotAllowed(response)
+
+ def test_delete_not_allowed(self):
+ response = self.client.delete(self.url)
+ self.assertHttpMethodNotAllowed(response)
+
+ def test_patch_not_allowed(self):
+ response = self.client.patch(self.url)
+ self.assertHttpMethodNotAllowed(response)
+
+ def test_register_form_default_fields(self):
+ no_extra_fields_setting = {}
+
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"email",
+ u"type": u"email",
+ u"required": True,
+ u"label": u"Email",
+ u"instructions": u"This is what you will use to login.",
+ u"restrictions": {
+ "min_length": EMAIL_MIN_LENGTH,
+ "max_length": EMAIL_MAX_LENGTH
+ },
+ }
+ )
+
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"name",
+ u"type": u"text",
+ u"required": True,
+ u"label": u"Full Name",
+ u"instructions": u"This name will be used on any certificates that you earn.",
+ u"restrictions": {
+ "max_length": 255
+ },
+ }
+ )
+
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"username",
+ u"type": u"text",
+ u"required": True,
+ u"label": u"Public Username",
+ u"instructions": u"The name that will identify you in your courses. "
+ u"It cannot be changed later.",
+ u"restrictions": {
+ "min_length": USERNAME_MIN_LENGTH,
+ "max_length": USERNAME_MAX_LENGTH
+ },
+ }
+ )
+
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"placeholder": "",
+ u"name": u"password",
+ u"type": u"password",
+ u"required": True,
+ u"label": u"Password",
+ u"instructions": password_validators_instruction_texts(),
+ u"restrictions": password_validators_restrictions(),
+ }
+ )
+
+ @override_settings(AUTH_PASSWORD_VALIDATORS=[
+ create_validator_config('util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}),
+ create_validator_config('util.password_policy_validators.UppercaseValidator', {'min_upper': 3}),
+ create_validator_config('util.password_policy_validators.SymbolValidator', {'min_symbol': 1}),
+ ])
+ def test_register_form_password_complexity(self):
+ no_extra_fields_setting = {}
+
+ # Without enabling password policy
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u'name': u'password',
+ u'label': u'Password',
+ u"instructions": password_validators_instruction_texts(),
+ u"restrictions": password_validators_restrictions(),
+ }
+ )
+
+ msg = u'Your password must contain at least 2 characters, including '\
+ u'3 uppercase letters & 1 symbol.'
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u'name': u'password',
+ u'label': u'Password',
+ u'instructions': msg,
+ u"restrictions": password_validators_restrictions(),
+ }
+ )
+
+ @override_settings(REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm')
+ def test_extension_form_fields(self):
+ no_extra_fields_setting = {}
+
+ # Verify other fields didn't disappear for some reason.
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"email",
+ u"type": u"email",
+ u"required": True,
+ u"label": u"Email",
+ u"instructions": u"This is what you will use to login.",
+ u"restrictions": {
+ "min_length": EMAIL_MIN_LENGTH,
+ "max_length": EMAIL_MAX_LENGTH
+ },
+ }
+ )
+
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"favorite_editor",
+ u"type": u"select",
+ u"required": False,
+ u"label": u"Favorite Editor",
+ u"placeholder": u"cat",
+ u"defaultValue": u"vim",
+ u"errorMessages": {
+ u'required': u'This field is required.',
+ u'invalid_choice': u'Select a valid choice. %(value)s is not one of the available choices.',
+ }
+ }
+ )
+
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"favorite_movie",
+ u"type": u"text",
+ u"required": True,
+ u"label": u"Fav Flick",
+ u"placeholder": None,
+ u"defaultValue": None,
+ u"errorMessages": {
+ u'required': u'Please tell us your favorite movie.',
+ u'invalid': u"We're pretty sure you made that movie up."
+ },
+ u"restrictions": {
+ "min_length": TestCaseForm.MOVIE_MIN_LEN,
+ "max_length": TestCaseForm.MOVIE_MAX_LEN,
+ }
+ }
+ )
+
+ @ddt.data(
+ ('pk', 'PK', 'Bob123', 'Bob123'),
+ ('Pk', 'PK', None, ''),
+ ('pK', 'PK', 'Bob123@edx.org', 'Bob123_edx_org'),
+ ('PK', 'PK', 'Bob123123123123123123123123123123123123', 'Bob123123123123123123123123123'),
+ ('us', 'US', 'Bob-1231231&23123+1231(2312312312@3123123123', 'Bob-1231231_23123_1231_2312312'),
+ )
+ @ddt.unpack
+ def test_register_form_third_party_auth_running_google(
+ self,
+ input_country_code,
+ expected_country_code,
+ input_username,
+ expected_username):
+ no_extra_fields_setting = {}
+ country_options = (
+ [
+ {
+ "name": "--",
+ "value": "",
+ "default": False
+ }
+ ] + [
+ {
+ "value": country_code,
+ "name": six.text_type(country_name),
+ "default": True if country_code == expected_country_code else False
+ }
+ for country_code, country_name in SORTED_COUNTRIES
+ ]
+ )
+
+ provider = self.configure_google_provider(enabled=True)
+ with simulate_running_pipeline(
+ "openedx.core.djangoapps.user_api.api.third_party_auth.pipeline", "google-oauth2",
+ email="bob@example.com",
+ fullname="Bob",
+ username=input_username,
+ country=input_country_code
+ ):
+ self._assert_password_field_hidden(no_extra_fields_setting)
+ self._assert_social_auth_provider_present(no_extra_fields_setting, provider)
+
+ # Email should be filled in
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"email",
+ u"defaultValue": u"bob@example.com",
+ u"type": u"email",
+ u"required": True,
+ u"label": u"Email",
+ u"instructions": u"This is what you will use to login.",
+ u"restrictions": {
+ "min_length": EMAIL_MIN_LENGTH,
+ "max_length": EMAIL_MAX_LENGTH
+ },
+ }
+ )
+
+ # Full Name should be filled in
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"name",
+ u"defaultValue": u"Bob",
+ u"type": u"text",
+ u"required": True,
+ u"label": u"Full Name",
+ u"instructions": u"This name will be used on any certificates that you earn.",
+ u"restrictions": {
+ "max_length": NAME_MAX_LENGTH,
+ }
+ }
+ )
+
+ # Username should be filled in
+ self._assert_reg_field(
+ no_extra_fields_setting,
+ {
+ u"name": u"username",
+ u"defaultValue": expected_username,
+ u"type": u"text",
+ u"required": True,
+ u"label": u"Public Username",
+ u"instructions": u"The name that will identify you in your courses. "
+ u"It cannot be changed later.",
+ u"restrictions": {
+ "min_length": USERNAME_MIN_LENGTH,
+ "max_length": USERNAME_MAX_LENGTH
+ }
+ }
+ )
+
+ # Country should be filled in.
+ self._assert_reg_field(
+ {u"country": u"required"},
+ {
+ u"label": u"Country or Region of Residence",
+ u"name": u"country",
+ u"defaultValue": expected_country_code,
+ u"type": u"select",
+ u"required": True,
+ u"options": country_options,
+ u"instructions": u"The country or region where you live.",
+ u"errorMessages": {
+ u"required": u"Select your country or region of residence."
+ },
+ }
+ )
+
+ def test_register_form_level_of_education(self):
+ self._assert_reg_field(
+ {"level_of_education": "optional"},
+ {
+ "name": "level_of_education",
+ "type": "select",
+ "required": False,
+ "label": "Highest level of education completed",
+ "options": [
+ {"value": "", "name": "--", "default": True},
+ {"value": "p", "name": "Doctorate", "default": False},
+ {"value": "m", "name": "Master's or professional degree", "default": False},
+ {"value": "b", "name": "Bachelor's degree", "default": False},
+ {"value": "a", "name": "Associate degree", "default": False},
+ {"value": "hs", "name": "Secondary/high school", "default": False},
+ {"value": "jhs", "name": "Junior secondary/junior high/middle school", "default": False},
+ {"value": "el", "name": "Elementary/primary school", "default": False},
+ {"value": "none", "name": "No formal education", "default": False},
+ {"value": "other", "name": "Other education", "default": False},
+ ],
+ "errorMessages": {
+ "required": "Select the highest level of education you have completed."
+ }
+ }
+ )
+
+ @mock.patch('openedx.core.djangoapps.user_api.api._')
+ def test_register_form_level_of_education_translations(self, fake_gettext):
+ fake_gettext.side_effect = lambda text: text + ' TRANSLATED'
+
+ self._assert_reg_field(
+ {"level_of_education": "optional"},
+ {
+ "name": "level_of_education",
+ "type": "select",
+ "required": False,
+ "label": "Highest level of education completed TRANSLATED",
+ "options": [
+ {"value": "", "name": "--", "default": True},
+ {"value": "p", "name": "Doctorate TRANSLATED", "default": False},
+ {"value": "m", "name": "Master's or professional degree TRANSLATED", "default": False},
+ {"value": "b", "name": "Bachelor's degree TRANSLATED", "default": False},
+ {"value": "a", "name": "Associate degree TRANSLATED", "default": False},
+ {"value": "hs", "name": "Secondary/high school TRANSLATED", "default": False},
+ {"value": "jhs", "name": "Junior secondary/junior high/middle school TRANSLATED", "default": False},
+ {"value": "el", "name": "Elementary/primary school TRANSLATED", "default": False},
+ {"value": "none", "name": "No formal education TRANSLATED", "default": False},
+ {"value": "other", "name": "Other education TRANSLATED", "default": False},
+ ],
+ "errorMessages": {
+ "required": "Select the highest level of education you have completed."
+ }
+ }
+ )
+
+ def test_register_form_gender(self):
+ self._assert_reg_field(
+ {"gender": "optional"},
+ {
+ "name": "gender",
+ "type": "select",
+ "required": False,
+ "label": "Gender",
+ "options": [
+ {"value": "", "name": "--", "default": True},
+ {"value": "m", "name": "Male", "default": False},
+ {"value": "f", "name": "Female", "default": False},
+ {"value": "o", "name": "Other/Prefer Not to Say", "default": False},
+ ],
+ }
+ )
+
+ @mock.patch('openedx.core.djangoapps.user_api.api._')
+ def test_register_form_gender_translations(self, fake_gettext):
+ fake_gettext.side_effect = lambda text: text + ' TRANSLATED'
+
+ self._assert_reg_field(
+ {"gender": "optional"},
+ {
+ "name": "gender",
+ "type": "select",
+ "required": False,
+ "label": "Gender TRANSLATED",
+ "options": [
+ {"value": "", "name": "--", "default": True},
+ {"value": "m", "name": "Male TRANSLATED", "default": False},
+ {"value": "f", "name": "Female TRANSLATED", "default": False},
+ {"value": "o", "name": "Other/Prefer Not to Say TRANSLATED", "default": False},
+ ],
+ }
+ )
+
+ def test_register_form_year_of_birth(self):
+ this_year = datetime.now(UTC).year
+ year_options = (
+ [
+ {
+ "value": "",
+ "name": "--",
+ "default": True
+ }
+ ] + [
+ {
+ "value": six.text_type(year),
+ "name": six.text_type(year),
+ "default": False
+ }
+ for year in range(this_year, this_year - 120, -1)
+ ]
+ )
+ self._assert_reg_field(
+ {"year_of_birth": "optional"},
+ {
+ "name": "year_of_birth",
+ "type": "select",
+ "required": False,
+ "label": "Year of birth",
+ "options": year_options,
+ }
+ )
+
+ def test_register_form_profession_without_profession_options(self):
+ self._assert_reg_field(
+ {"profession": "required"},
+ {
+ "name": "profession",
+ "type": "text",
+ "required": True,
+ "label": "Profession",
+ "errorMessages": {
+ "required": "Enter your profession."
+ }
+ }
+ )
+
+ @with_site_configuration(
+ configuration={
+ "EXTRA_FIELD_OPTIONS": {"profession": ["Software Engineer", "Teacher", "Other"]}
+ }
+ )
+ def test_register_form_profession_with_profession_options(self):
+ self._assert_reg_field(
+ {"profession": "required"},
+ {
+ "name": "profession",
+ "type": "select",
+ "required": True,
+ "label": "Profession",
+ "options": self.PROFESSION_OPTIONS,
+ "errorMessages": {
+ "required": "Select your profession."
+ },
+ }
+ )
+
+ def test_register_form_specialty_without_specialty_options(self):
+ self._assert_reg_field(
+ {"specialty": "required"},
+ {
+ "name": "specialty",
+ "type": "text",
+ "required": True,
+ "label": "Specialty",
+ "errorMessages": {
+ "required": "Enter your specialty."
+ }
+ }
+ )
+
+ @with_site_configuration(
+ configuration={
+ "EXTRA_FIELD_OPTIONS": {"specialty": ["Aerospace", "Early Education", "N/A"]}
+ }
+ )
+ def test_register_form_specialty_with_specialty_options(self):
+ self._assert_reg_field(
+ {"specialty": "required"},
+ {
+ "name": "specialty",
+ "type": "select",
+ "required": True,
+ "label": "Specialty",
+ "options": self.SPECIALTY_OPTIONS,
+ "errorMessages": {
+ "required": "Select your specialty."
+ },
+ }
+ )
+
+ def test_registration_form_mailing_address(self):
+ self._assert_reg_field(
+ {"mailing_address": "optional"},
+ {
+ "name": "mailing_address",
+ "type": "textarea",
+ "required": False,
+ "label": "Mailing address",
+ "errorMessages": {
+ "required": "Enter your mailing address."
+ }
+ }
+ )
+
+ def test_registration_form_goals(self):
+ self._assert_reg_field(
+ {"goals": "optional"},
+ {
+ "name": "goals",
+ "type": "textarea",
+ "required": False,
+ "label": u"Tell us why you're interested in {platform_name}".format(
+ platform_name=settings.PLATFORM_NAME
+ ),
+ "errorMessages": {
+ "required": "Tell us your goals."
+ }
+ }
+ )
+
+ def test_registration_form_city(self):
+ self._assert_reg_field(
+ {"city": "optional"},
+ {
+ "name": "city",
+ "type": "text",
+ "required": False,
+ "label": "City",
+ "errorMessages": {
+ "required": "Enter your city."
+ }
+ }
+ )
+
+ def test_registration_form_state(self):
+ self._assert_reg_field(
+ {"state": "optional"},
+ {
+ "name": "state",
+ "type": "text",
+ "required": False,
+ "label": "State/Province/Region",
+ }
+ )
+
+ def test_registration_form_country(self):
+ country_options = (
+ [
+ {
+ "name": "--",
+ "value": "",
+ "default": True
+ }
+ ] + [
+ {
+ "value": country_code,
+ "name": six.text_type(country_name),
+ "default": False
+ }
+ for country_code, country_name in SORTED_COUNTRIES
+ ]
+ )
+ self._assert_reg_field(
+ {"country": "required"},
+ {
+ "label": "Country or Region of Residence",
+ "name": "country",
+ "type": "select",
+ "instructions": "The country or region where you live.",
+ "required": True,
+ "options": country_options,
+ "errorMessages": {
+ "required": "Select your country or region of residence."
+ },
+ }
+ )
+
+ def test_registration_form_confirm_email(self):
+ self._assert_reg_field(
+ {"confirm_email": "required"},
+ {
+ "name": "confirm_email",
+ "type": "text",
+ "required": True,
+ "label": "Confirm Email",
+ "errorMessages": {
+ "required": "The email addresses do not match.",
+ }
+ }
+ )
+
+ @override_settings(
+ MKTG_URLS={"ROOT": "https://www.test.com/", "HONOR": "honor"},
+ )
+ @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
+ def test_registration_honor_code_mktg_site_enabled(self):
+ link_template = "{link_label}"
+ link_template2 = u"{link_label}"
+ link_label = "Terms of Service and Honor Code"
+ link_label2 = "Privacy Policy"
+ self._assert_reg_field(
+ {"honor_code": "required"},
+ {
+ "label": (u"By creating an account, you agree to the {spacing}"
+ u"{link_label} {spacing}"
+ u"and you acknowledge that {platform_name} and each Member process your "
+ u"personal data in accordance {spacing}"
+ u"with the {link_label2}.").format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_template.format(link_label=link_label),
+ link_label2=link_template2.format(link_label=link_label2),
+ spacing=' ' * 18
+ ),
+ "name": "honor_code",
+ "defaultValue": False,
+ "type": "plaintext",
+ "required": True,
+ "errorMessages": {
+ "required": u"You must agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_label
+ )
+ }
+ }
+ )
+
+ @override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"})
+ @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
+ def test_registration_honor_code_mktg_site_disabled(self):
+ link_template = "{link_label}"
+ link_label = "Terms of Service and Honor Code"
+ link_label2 = "Privacy Policy"
+ self._assert_reg_field(
+ {"honor_code": "required"},
+ {
+ "label": (u"By creating an account, you agree to the {spacing}"
+ u"{link_label} {spacing}"
+ u"and you acknowledge that {platform_name} and each Member process your "
+ u"personal data in accordance {spacing}"
+ u"with the {link_label2}.").format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=self.link_template.format(link_label=link_label),
+ link_label2=link_template.format(link_label=link_label2),
+ spacing=' ' * 18
+ ),
+ "name": "honor_code",
+ "defaultValue": False,
+ "type": "plaintext",
+ "required": True,
+ "errorMessages": {
+ "required": u"You must agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_label
+ )
+ }
+ }
+ )
+
+ @override_settings(MKTG_URLS={
+ "ROOT": "https://www.test.com/",
+ "HONOR": "honor",
+ "TOS": "tos",
+ })
+ @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
+ def test_registration_separate_terms_of_service_mktg_site_enabled(self):
+ # Honor code field should say ONLY honor code,
+ # not "terms of service and honor code"
+ link_label = 'Honor Code'
+ link_template = u"{link_label}"
+ self._assert_reg_field(
+ {"honor_code": "required", "terms_of_service": "required"},
+ {
+ "label": u"I agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_template.format(link_label=link_label)
+ ),
+ "name": "honor_code",
+ "defaultValue": False,
+ "type": "checkbox",
+ "required": True,
+ "errorMessages": {
+ "required": u"You must agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_label
+ )
+ }
+ }
+ )
+
+ # Terms of service field should also be present
+ link_label = "Terms of Service"
+ link_template = u"{link_label}"
+ self._assert_reg_field(
+ {"honor_code": "required", "terms_of_service": "required"},
+ {
+ "label": u"I agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_template.format(link_label=link_label)
+ ),
+ "name": "terms_of_service",
+ "defaultValue": False,
+ "type": "checkbox",
+ "required": True,
+ "errorMessages": {
+ "required": u"You must agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_label
+ )
+ }
+ }
+ )
+
+ @override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor", "TOS": "tos"})
+ @mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
+ def test_registration_separate_terms_of_service_mktg_site_disabled(self):
+ # Honor code field should say ONLY honor code,
+ # not "terms of service and honor code"
+ link_label = 'Honor Code'
+ self._assert_reg_field(
+ {"honor_code": "required", "terms_of_service": "required"},
+ {
+ "label": u"I agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=self.link_template.format(link_label=link_label)
+ ),
+ "name": "honor_code",
+ "defaultValue": False,
+ "type": "checkbox",
+ "required": True,
+ "errorMessages": {
+ "required": u"You must agree to the {platform_name} Honor Code".format(
+ platform_name=settings.PLATFORM_NAME
+ )
+ }
+ }
+ )
+
+ link_label = 'Terms of Service'
+ # Terms of service field should also be present
+ link_template = u"{link_label}"
+ self._assert_reg_field(
+ {"honor_code": "required", "terms_of_service": "required"},
+ {
+ "label": u"I agree to the {platform_name} {link_label}".format(
+ platform_name=settings.PLATFORM_NAME,
+ link_label=link_template.format(link_label=link_label)
+ ),
+ "name": "terms_of_service",
+ "defaultValue": False,
+ "type": "checkbox",
+ "required": True,
+ "errorMessages": {
+ "required": u"You must agree to the {platform_name} Terms of Service".format(
+ platform_name=settings.PLATFORM_NAME
+ )
+ }
+ }
+ )
+
+ @override_settings(
+ REGISTRATION_EXTRA_FIELDS={
+ "level_of_education": "optional",
+ "gender": "optional",
+ "year_of_birth": "optional",
+ "mailing_address": "optional",
+ "goals": "optional",
+ "city": "optional",
+ "state": "optional",
+ "country": "required",
+ "honor_code": "required",
+ "confirm_email": "required",
+ },
+ REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
+ )
+ def test_field_order(self):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ # Verify that all fields render in the correct order
+ form_desc = json.loads(response.content.decode('utf-8'))
+ field_names = [field["name"] for field in form_desc["fields"]]
+ self.assertEqual(field_names, [
+ "email",
+ "name",
+ "username",
+ "password",
+ "favorite_movie",
+ "favorite_editor",
+ "confirm_email",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ ])
+
+ @override_settings(
+ REGISTRATION_EXTRA_FIELDS={
+ "level_of_education": "optional",
+ "gender": "optional",
+ "year_of_birth": "optional",
+ "mailing_address": "optional",
+ "goals": "optional",
+ "city": "optional",
+ "state": "optional",
+ "country": "required",
+ "honor_code": "required",
+ "confirm_email": "required",
+ },
+ REGISTRATION_FIELD_ORDER=[
+ "name",
+ "username",
+ "email",
+ "confirm_email",
+ "password",
+ "first_name",
+ "last_name",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "company",
+ "title",
+ "job_title",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ "terms_of_service",
+ "specialty",
+ "profession",
+ ],
+ )
+ def test_field_order_override(self):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ # Verify that all fields render in the correct order
+ form_desc = json.loads(response.content.decode('utf-8'))
+ field_names = [field["name"] for field in form_desc["fields"]]
+ self.assertEqual(field_names, [
+ "name",
+ "username",
+ "email",
+ "confirm_email",
+ "password",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ ])
+
+ @override_settings(
+ REGISTRATION_EXTRA_FIELDS={
+ "level_of_education": "optional",
+ "gender": "optional",
+ "year_of_birth": "optional",
+ "mailing_address": "optional",
+ "goals": "optional",
+ "city": "optional",
+ "state": "optional",
+ "country": "required",
+ "honor_code": "required",
+ "confirm_email": "required",
+ },
+ REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm',
+ REGISTRATION_FIELD_ORDER=[
+ "name",
+ "confirm_email",
+ "password",
+ "first_name",
+ "last_name",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "company",
+ "title",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ "terms_of_service",
+ ],
+ )
+ def test_field_order_invalid_override(self):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ # Verify that all fields render in the correct order
+ form_desc = json.loads(response.content.decode('utf-8'))
+ field_names = [field["name"] for field in form_desc["fields"]]
+ self.assertEqual(field_names, [
+ "email",
+ "name",
+ "username",
+ "password",
+ "favorite_movie",
+ "favorite_editor",
+ "confirm_email",
+ "city",
+ "state",
+ "country",
+ "gender",
+ "year_of_birth",
+ "level_of_education",
+ "mailing_address",
+ "goals",
+ "honor_code",
+ ])
+
+ def test_register(self):
+ # Create a new registration
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+ self.assertIn(settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, self.client.cookies)
+ self.assertIn(settings.EDXMKTG_USER_INFO_COOKIE_NAME, self.client.cookies)
+
+ user = User.objects.get(username=self.USERNAME)
+ request = RequestFactory().get('/url')
+ request.user = user
+ account_settings = get_account_settings(request)[0]
+
+ self.assertEqual(self.USERNAME, account_settings["username"])
+ self.assertEqual(self.EMAIL, account_settings["email"])
+ self.assertFalse(account_settings["is_active"])
+ self.assertEqual(self.NAME, account_settings["name"])
+
+ # Verify that we've been logged in
+ # by trying to access a page that requires authentication
+ response = self.client.get(reverse("dashboard"))
+ self.assertHttpOK(response)
+
+ @override_settings(REGISTRATION_EXTRA_FIELDS={
+ "level_of_education": "optional",
+ "gender": "optional",
+ "year_of_birth": "optional",
+ "mailing_address": "optional",
+ "goals": "optional",
+ "country": "required",
+ })
+ def test_register_with_profile_info(self):
+ # Register, providing lots of demographic info
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "level_of_education": self.EDUCATION,
+ "mailing_address": self.ADDRESS,
+ "year_of_birth": self.YEAR_OF_BIRTH,
+ "goals": self.GOALS,
+ "country": self.COUNTRY,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Verify the user's account
+ user = User.objects.get(username=self.USERNAME)
+ request = RequestFactory().get('/url')
+ request.user = user
+ account_settings = get_account_settings(request)[0]
+
+ self.assertEqual(account_settings["level_of_education"], self.EDUCATION)
+ self.assertEqual(account_settings["mailing_address"], self.ADDRESS)
+ self.assertEqual(account_settings["year_of_birth"], int(self.YEAR_OF_BIRTH))
+ self.assertEqual(account_settings["goals"], self.GOALS)
+ self.assertEqual(account_settings["country"], self.COUNTRY)
+
+ @override_settings(REGISTRATION_EXTENSION_FORM='openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm')
+ @mock.patch('openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm.DUMMY_STORAGE', new_callable=dict)
+ @mock.patch(
+ 'openedx.core.djangoapps.user_api.tests.test_helpers.DummyRegistrationExtensionModel',
+ )
+ def test_with_extended_form(self, dummy_model, storage_dict):
+ dummy_model_instance = mock.Mock()
+ dummy_model.return_value = dummy_model_instance
+ # Create a new registration
+ self.assertEqual(storage_dict, {})
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ "favorite_movie": "Inception",
+ "favorite_editor": "cat",
+ })
+ self.assertHttpOK(response)
+ self.assertIn(settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, self.client.cookies)
+ self.assertIn(settings.EDXMKTG_USER_INFO_COOKIE_NAME, self.client.cookies)
+
+ user = User.objects.get(username=self.USERNAME)
+ request = RequestFactory().get('/url')
+ request.user = user
+ account_settings = get_account_settings(request)[0]
+
+ self.assertEqual(self.USERNAME, account_settings["username"])
+ self.assertEqual(self.EMAIL, account_settings["email"])
+ self.assertFalse(account_settings["is_active"])
+ self.assertEqual(self.NAME, account_settings["name"])
+
+ self.assertEqual(storage_dict, {'favorite_movie': "Inception", "favorite_editor": "cat"})
+ self.assertEqual(dummy_model_instance.user, user)
+
+ # Verify that we've been logged in
+ # by trying to access a page that requires authentication
+ response = self.client.get(reverse("dashboard"))
+ self.assertHttpOK(response)
+
+ def test_activation_email(self):
+ # Register, which should trigger an activation email
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Verify that the activation email was sent
+ self.assertEqual(len(mail.outbox), 1)
+ sent_email = mail.outbox[0]
+ self.assertEqual(sent_email.to, [self.EMAIL])
+ self.assertEqual(
+ sent_email.subject,
+ u"Action Required: Activate your {platform} account".format(platform=settings.PLATFORM_NAME)
+ )
+ self.assertIn(
+ u"high-quality {platform} courses".format(platform=settings.PLATFORM_NAME),
+ sent_email.body
+ )
+
+ @ddt.data(
+ {"email": ""},
+ {"email": "invalid"},
+ {"name": ""},
+ {"username": ""},
+ {"username": "a"},
+ {"password": ""},
+ )
+ def test_register_invalid_input(self, invalid_fields):
+ # Initially, the field values are all valid
+ data = {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ }
+
+ # Override the valid fields, making the input invalid
+ data.update(invalid_fields)
+
+ # Attempt to create the account, expecting an error response
+ response = self.client.post(self.url, data)
+ self.assertHttpBadRequest(response)
+
+ @override_settings(REGISTRATION_EXTRA_FIELDS={"country": "required"})
+ @ddt.data("email", "name", "username", "password", "country")
+ def test_register_missing_required_field(self, missing_field):
+ data = {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "country": self.COUNTRY,
+ }
+
+ del data[missing_field]
+
+ # Send a request missing a field
+ response = self.client.post(self.url, data)
+ self.assertHttpBadRequest(response)
+
+ def test_register_duplicate_email(self):
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Try to create a second user with the same email address
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": "Someone Else",
+ "username": "someone_else",
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 409)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "email": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different email address."
+ ).format(
+ self.EMAIL
+ )
+ }]
+ }
+ )
+
+ def test_register_duplicate_username(self):
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Try to create a second user with the same username
+ response = self.client.post(self.url, {
+ "email": "someone+else@example.com",
+ "name": "Someone Else",
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 409)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "username": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different username."
+ ).format(
+ self.USERNAME
+ )
+ }]
+ }
+ )
+
+ def test_register_duplicate_username_and_email(self):
+ # Register the first user
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertHttpOK(response)
+
+ # Try to create a second user with the same username
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": "Someone Else",
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ "honor_code": "true",
+ })
+ self.assertEqual(response.status_code, 409)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "username": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different username."
+ ).format(
+ self.USERNAME
+ )
+ }],
+ "email": [{
+ "user_message": (
+ u"It looks like {} belongs to an existing account. "
+ "Try again with a different email address."
+ ).format(
+ self.EMAIL
+ )
+ }]
+ }
+ )
+
+ @override_settings(REGISTRATION_EXTRA_FIELDS={"honor_code": "hidden", "terms_of_service": "hidden"})
+ def test_register_hidden_honor_code_and_terms_of_service(self):
+ response = self.client.post(self.url, {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "username": self.USERNAME,
+ "password": self.PASSWORD,
+ })
+ self.assertHttpOK(response)
+
+ def test_missing_fields(self):
+ response = self.client.post(
+ self.url,
+ {
+ "email": self.EMAIL,
+ "name": self.NAME,
+ "honor_code": "true",
+ }
+ )
+ self.assertEqual(response.status_code, 400)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ u"success": False,
+ u"username": [{u"user_message": USERNAME_BAD_LENGTH_MSG}],
+ u"password": [{u"user_message": u"This field is required."}],
+ }
+ )
+
+ def test_country_overrides(self):
+ """Test that overridden countries are available in country list."""
+ # Retrieve the registration form description
+ with override_settings(REGISTRATION_EXTRA_FIELDS={"country": "required"}):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ self.assertContains(response, 'Kosovo')
+
+ def test_create_account_not_allowed(self):
+ """
+ Test case to check user creation is forbidden when ALLOW_PUBLIC_ACCOUNT_CREATION feature flag is turned off
+ """
+ def _side_effect_for_get_value(value, default=None):
+ """
+ returns a side_effect with given return value for a given value
+ """
+ if value == 'ALLOW_PUBLIC_ACCOUNT_CREATION':
+ return False
+ else:
+ return get_value(value, default)
+
+ with mock.patch('openedx.core.djangoapps.site_configuration.helpers.get_value') as mock_get_value:
+ mock_get_value.side_effect = _side_effect_for_get_value
+ response = self.client.post(self.url, {"email": self.EMAIL, "username": self.USERNAME})
+ self.assertEqual(response.status_code, 403)
+
+ def _assert_fields_match(self, actual_field, expected_field):
+ """
+ Assert that the actual field and the expected field values match.
+ """
+ self.assertIsNot(
+ actual_field, None,
+ msg=u"Could not find field {name}".format(name=expected_field["name"])
+ )
+
+ for key in expected_field:
+ self.assertEqual(
+ actual_field[key], expected_field[key],
+ msg=u"Expected {expected} for {key} but got {actual} instead".format(
+ key=key,
+ actual=actual_field[key],
+ expected=expected_field[key]
+ )
+ )
+
+ def _populate_always_present_fields(self, field):
+ """
+ Populate field dictionary with keys and values that are always present.
+ """
+ defaults = [
+ ("label", ""),
+ ("instructions", ""),
+ ("placeholder", ""),
+ ("defaultValue", ""),
+ ("restrictions", {}),
+ ("errorMessages", {}),
+ ]
+ field.update({
+ key: value
+ for key, value in defaults if key not in field
+ })
+
+ def _assert_reg_field(self, extra_fields_setting, expected_field):
+ """
+ Retrieve the registration form description from the server and
+ verify that it contains the expected field.
+
+ Args:
+ extra_fields_setting (dict): Override the Django setting controlling
+ which extra fields are displayed in the form.
+ expected_field (dict): The field definition we expect to find in the form.
+
+ Raises:
+ AssertionError
+ """
+ # Add in fields that are always present
+ self._populate_always_present_fields(expected_field)
+
+ # Retrieve the registration form description
+ with override_settings(REGISTRATION_EXTRA_FIELDS=extra_fields_setting):
+ response = self.client.get(self.url)
+ self.assertHttpOK(response)
+
+ # Verify that the form description matches what we'd expect
+ form_desc = json.loads(response.content.decode('utf-8'))
+
+ actual_field = None
+ for field in form_desc["fields"]:
+ if field["name"] == expected_field["name"]:
+ actual_field = field
+ break
+
+ self._assert_fields_match(actual_field, expected_field)
+
+ def _assert_password_field_hidden(self, field_settings):
+ self._assert_reg_field(field_settings, {
+ "name": "password",
+ "type": "hidden",
+ "required": False
+ })
+
+ def _assert_social_auth_provider_present(self, field_settings, backend):
+ self._assert_reg_field(field_settings, {
+ "name": "social_auth_provider",
+ "type": "hidden",
+ "required": False,
+ "defaultValue": backend.name
+ })
+
+
+@httpretty.activate
+@ddt.ddt
+class ThirdPartyRegistrationTestMixin(ThirdPartyOAuthTestMixin, CacheIsolationTestCase):
+ """
+ Tests for the User API registration endpoint with 3rd party authentication.
+ """
+ CREATE_USER = False
+
+ ENABLED_CACHES = ['default']
+
+ __test__ = False
+
+ def setUp(self):
+ super(ThirdPartyRegistrationTestMixin, self).setUp()
+ self.url = reverse('user_api_registration')
+
+ def tearDown(self):
+ super(ThirdPartyRegistrationTestMixin, self).tearDown()
+ Partial.objects.all().delete()
+
+ def data(self, user=None):
+ """Returns the request data for the endpoint."""
+ return {
+ "provider": self.BACKEND,
+ "access_token": self.access_token,
+ "client_id": self.client_id,
+ "honor_code": "true",
+ "country": "US",
+ "username": user.username if user else "test_username",
+ "name": user.first_name if user else "test name",
+ "email": user.email if user else "test@test.com"
+ }
+
+ def _assert_existing_user_error(self, response):
+ """Assert that the given response was an error with the given status_code and error code."""
+ self.assertEqual(response.status_code, 409)
+ errors = json.loads(response.content.decode('utf-8'))
+ for conflict_attribute in ["username", "email"]:
+ self.assertIn(conflict_attribute, errors)
+ self.assertIn("belongs to an existing account", errors[conflict_attribute][0]["user_message"])
+
+ def _assert_access_token_error(self, response, expected_error_message):
+ """Assert that the given response was an error for the access_token field with the given error message."""
+ self.assertEqual(response.status_code, 400)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "access_token": [{"user_message": expected_error_message}],
+ }
+ )
+
+ def _assert_third_party_session_expired_error(self, response, expected_error_message):
+ """Assert that given response is an error due to third party session expiry"""
+ self.assertEqual(response.status_code, 400)
+ response_json = json.loads(response.content.decode('utf-8'))
+ self.assertDictEqual(
+ response_json,
+ {
+ "success": False,
+ "session_expired": [{"user_message": expected_error_message}],
+ }
+ )
+
+ def _verify_user_existence(self, user_exists, social_link_exists, user_is_active=None, username=None):
+ """Verifies whether the user object exists."""
+ users = User.objects.filter(username=(username if username else "test_username"))
+ self.assertEquals(users.exists(), user_exists)
+ if user_exists:
+ self.assertEquals(users[0].is_active, user_is_active)
+ self.assertEqual(
+ UserSocialAuth.objects.filter(user=users[0], provider=self.BACKEND).exists(),
+ social_link_exists
+ )
+ else:
+ self.assertEquals(UserSocialAuth.objects.count(), 0)
+
+ def test_success(self):
+ self._verify_user_existence(user_exists=False, social_link_exists=False)
+
+ self._setup_provider_response(success=True)
+ response = self.client.post(self.url, self.data())
+ self.assertEqual(response.status_code, 200)
+
+ self._verify_user_existence(user_exists=True, social_link_exists=True, user_is_active=False)
+
+ def test_unlinked_active_user(self):
+ user = UserFactory()
+ response = self.client.post(self.url, self.data(user))
+ self._assert_existing_user_error(response)
+ self._verify_user_existence(
+ user_exists=True, social_link_exists=False, user_is_active=True, username=user.username
+ )
+
+ def test_unlinked_inactive_user(self):
+ user = UserFactory(is_active=False)
+ response = self.client.post(self.url, self.data(user))
+ self._assert_existing_user_error(response)
+ self._verify_user_existence(
+ user_exists=True, social_link_exists=False, user_is_active=False, username=user.username
+ )
+
+ def test_user_already_registered(self):
+ self._setup_provider_response(success=True)
+ user = UserFactory()
+ UserSocialAuth.objects.create(user=user, provider=self.BACKEND, uid=self.social_uid)
+ response = self.client.post(self.url, self.data(user))
+ self._assert_existing_user_error(response)
+ self._verify_user_existence(
+ user_exists=True, social_link_exists=True, user_is_active=True, username=user.username
+ )
+
+ def test_social_user_conflict(self):
+ self._setup_provider_response(success=True)
+ user = UserFactory()
+ UserSocialAuth.objects.create(user=user, provider=self.BACKEND, uid=self.social_uid)
+ response = self.client.post(self.url, self.data())
+ self._assert_access_token_error(response, "The provided access_token is already associated with another user.")
+ self._verify_user_existence(
+ user_exists=True, social_link_exists=True, user_is_active=True, username=user.username
+ )
+
+ def test_invalid_token(self):
+ self._setup_provider_response(success=False)
+ response = self.client.post(self.url, self.data())
+ self._assert_access_token_error(response, "The provided access_token is not valid.")
+ self._verify_user_existence(user_exists=False, social_link_exists=False)
+
+ def test_missing_token(self):
+ data = self.data()
+ data.pop("access_token")
+ response = self.client.post(self.url, data)
+ self._assert_access_token_error(
+ response,
+ u"An access_token is required when passing value ({}) for provider.".format(self.BACKEND)
+ )
+ self._verify_user_existence(user_exists=False, social_link_exists=False)
+
+ def test_expired_pipeline(self):
+ """
+ Test that there is an error and account is not created
+ when request is made for account creation using third (Google, Facebook etc) party with pipeline
+ getting expired using browser (not mobile application).
+
+ NOTE: We are NOT using actual pipeline here so pipeline is always expired in this environment.
+ we don't have to explicitly expire pipeline.
+ """
+ data = self.data()
+ # provider is sent along request when request is made from mobile application
+ data.pop("provider")
+ # to identify that request is made using browser
+ data.update({"social_auth_provider": "Google"})
+ response = self.client.post(self.url, data)
+ self._assert_third_party_session_expired_error(
+ response,
+ u"Registration using {provider} has timed out.".format(provider="Google")
+ )
+ self._verify_user_existence(user_exists=False, social_link_exists=False)
+
+
+@skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
+class TestFacebookRegistrationView(
+ ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase
+):
+ """Tests the User API registration endpoint with Facebook authentication."""
+ __test__ = True
+
+ def test_social_auth_exception(self):
+ """
+ According to the do_auth method in social_core.backends.facebook.py,
+ the Facebook API sometimes responds back a JSON with just False as value.
+ """
+ self._setup_provider_response_with_body(200, json.dumps("false"))
+ response = self.client.post(self.url, self.data())
+ self._assert_access_token_error(response, "The provided access_token is not valid.")
+ self._verify_user_existence(user_exists=False, social_link_exists=False)
+
+
+@skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled")
+class TestGoogleRegistrationView(
+ ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase
+):
+ """Tests the User API registration endpoint with Google authentication."""
+ __test__ = True