# -*- coding: utf-8 -*- """ Tests for student account views. """ import logging import re from unittest import skipUnless from urllib import urlencode import ddt import mock import pytest from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.messages.middleware import MessageMiddleware from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile from django.core.urlresolvers import reverse from django.http import HttpRequest from django.test import TestCase from django.test.utils import override_settings from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory, RefreshTokenFactory from edx_rest_api_client import exceptions from http.cookies import SimpleCookie from nose.plugins.attrib import attr from oauth2_provider.models import AccessToken as dot_access_token from oauth2_provider.models import RefreshToken as dot_refresh_token from provider.oauth2.models import AccessToken as dop_access_token from provider.oauth2.models import RefreshToken as dop_refresh_token from testfixtures import LogCapture from course_modes.models import CourseMode from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.tests import factories from lms.djangoapps.commerce.tests.mocks import mock_get_orders from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factories from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme_context from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account from openedx.core.djangolib.js_utils import dump_js_escaped_json from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.tests.factories import UserFactory from student_account.views import account_settings_context, get_user_orders from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline from util.testing import UrlResetMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from openedx.core.djangoapps.user_api.errors import UserAPIInternalError LOGGER_NAME = 'audit' User = get_user_model() # pylint:disable=invalid-name @ddt.ddt class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin): """ Tests for the student account views that update the user's account information. """ USERNAME = u"heisenberg" ALTERNATE_USERNAME = u"walt" OLD_PASSWORD = u"ḅḷüëṡḳÿ" NEW_PASSWORD = u"🄱🄸🄶🄱🄻🅄🄴" OLD_EMAIL = u"walter@graymattertech.com" NEW_EMAIL = u"walt@savewalterwhite.com" INVALID_ATTEMPTS = 100 INVALID_KEY = u"123abc" URLCONF_MODULES = ['student_accounts.urls'] ENABLED_CACHES = ['default'] def setUp(self): super(StudentAccountUpdateTest, self).setUp() # Create/activate a new account activation_key = create_account(self.USERNAME, self.OLD_PASSWORD, self.OLD_EMAIL) activate_account(activation_key) # Login result = self.client.login(username=self.USERNAME, password=self.OLD_PASSWORD) self.assertTrue(result) @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS') def test_password_change(self): # Request a password change while logged in, simulating # use of the password reset link from the account page response = self._change_password() self.assertEqual(response.status_code, 200) # Check that an email was sent self.assertEqual(len(mail.outbox), 1) # Retrieve the activation link from the email body email_body = mail.outbox[0].body result = re.search(r'(?P<url>https?://[^\s]+)', email_body) self.assertIsNot(result, None) activation_link = result.group('url') # Visit the activation link response = self.client.get(activation_link) self.assertEqual(response.status_code, 200) # Submit a new password and follow the redirect to the success page response = self.client.post( activation_link, # These keys are from the form on the current password reset confirmation page. {'new_password1': self.NEW_PASSWORD, 'new_password2': self.NEW_PASSWORD}, follow=True ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Your password has been reset.") # Log the user out to clear session data self.client.logout() # Verify that the new password can be used to log in result = self.client.login(username=self.USERNAME, password=self.NEW_PASSWORD) self.assertTrue(result) # Try reusing the activation link to change the password again # Visit the activation link again. response = self.client.get(activation_link) self.assertEqual(response.status_code, 200) self.assertContains(response, "This password reset link is invalid. It may have been used already.") self.client.logout() # Verify that the old password cannot be used to log in result = self.client.login(username=self.USERNAME, password=self.OLD_PASSWORD) self.assertFalse(result) # Verify that the new password continues to be valid result = self.client.login(username=self.USERNAME, password=self.NEW_PASSWORD) self.assertTrue(result) def test_password_change_failure(self): with mock.patch('openedx.core.djangoapps.user_api.accounts.api.request_password_change', side_effect=UserAPIInternalError): self._change_password() self.assertRaises(UserAPIInternalError) @ddt.data(True, False) def test_password_change_logged_out(self, send_email): # Log the user out self.client.logout() # Request a password change while logged out, simulating # use of the password reset link from the login page if send_email: response = self._change_password(email=self.OLD_EMAIL) self.assertEqual(response.status_code, 200) else: # Don't send an email in the POST data, simulating # its (potentially accidental) omission in the POST # data sent from the login page response = self._change_password() self.assertEqual(response.status_code, 400) def test_access_token_invalidation_logged_out(self): self.client.logout() user = User.objects.get(email=self.OLD_EMAIL) self._create_dop_tokens(user) self._create_dot_tokens(user) response = self._change_password(email=self.OLD_EMAIL) self.assertEqual(response.status_code, 200) self.assert_access_token_destroyed(user) def test_access_token_invalidation_logged_in(self): user = User.objects.get(email=self.OLD_EMAIL) self._create_dop_tokens(user) self._create_dot_tokens(user) response = self._change_password() self.assertEqual(response.status_code, 200) self.assert_access_token_destroyed(user) def test_password_change_inactive_user(self): # Log out the user created during test setup self.client.logout() # Create a second user, but do not activate it create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL) # Send the view the email address tied to the inactive user response = self._change_password(email=self.NEW_EMAIL) # Expect that the activation email is still sent, # since the user may have lost the original activation email. self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) def test_password_change_no_user(self): # Log out the user created during test setup self.client.logout() with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: # Send the view an email address not tied to any user response = self._change_password(email=self.NEW_EMAIL) self.assertEqual(response.status_code, 200) logger.check((LOGGER_NAME, 'INFO', 'Invalid password reset attempt')) def test_password_change_rate_limited(self): # Log out the user created during test setup, to prevent the view from # selecting the logged-in user's email address over the email provided # in the POST data self.client.logout() # Make many consecutive bad requests in an attempt to trigger the rate limiter for __ in xrange(self.INVALID_ATTEMPTS): self._change_password(email=self.NEW_EMAIL) response = self._change_password(email=self.NEW_EMAIL) self.assertEqual(response.status_code, 403) @ddt.data( ('post', 'password_change_request', []), ) @ddt.unpack def test_require_http_method(self, correct_method, url_name, args): wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - {correct_method} url = reverse(url_name, args=args) for method in wrong_methods: response = getattr(self.client, method)(url) self.assertEqual(response.status_code, 405) def _change_password(self, email=None): """Request to change the user's password. """ data = {} if email: data['email'] = email return self.client.post(path=reverse('password_change_request'), data=data) def _create_dop_tokens(self, user=None): """Create dop access token for given user if user provided else for default user.""" if not user: user = User.objects.get(email=self.OLD_EMAIL) client = ClientFactory() access_token = AccessTokenFactory(user=user, client=client) RefreshTokenFactory(user=user, client=client, access_token=access_token) def _create_dot_tokens(self, user=None): """Create dop access token for given user if user provided else for default user.""" if not user: user = User.objects.get(email=self.OLD_EMAIL) application = dot_factories.ApplicationFactory(user=user) access_token = dot_factories.AccessTokenFactory(user=user, application=application) dot_factories.RefreshTokenFactory(user=user, application=application, access_token=access_token) def assert_access_token_destroyed(self, user): """Assert all access tokens are destroyed.""" self.assertFalse(dot_access_token.objects.filter(user=user).exists()) self.assertFalse(dot_refresh_token.objects.filter(user=user).exists()) self.assertFalse(dop_access_token.objects.filter(user=user).exists()) self.assertFalse(dop_refresh_token.objects.filter(user=user).exists()) @attr(shard=3) @ddt.ddt class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleStoreTestCase): """ Tests for the student account views that update the user's account information. """ USERNAME = "bob" EMAIL = "bob@example.com" PASSWORD = "password" URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) def setUp(self): super(StudentAccountLoginAndRegistrationTest, self).setUp() # Several third party auth providers are created for these tests: self.google_provider = self.configure_google_provider(enabled=True, visible=True) self.configure_facebook_provider(enabled=True, visible=True) self.configure_dummy_provider( visible=True, enabled=True, icon_class='', icon_image=SimpleUploadedFile('icon.svg', '<svg><rect width="50" height="100"/></svg>'), ) self.hidden_enabled_provider = self.configure_linkedin_provider( visible=False, enabled=True, ) self.hidden_disabled_provider = self.configure_azure_ad_provider() @ddt.data( ("signin_user", "login"), ("register_user", "register"), ) @ddt.unpack def test_login_and_registration_form(self, url_name, initial_mode): response = self.client.get(reverse(url_name)) expected_data = '"initial_mode": "{mode}"'.format(mode=initial_mode) self.assertContains(response, expected_data) @ddt.data("signin_user", "register_user") def test_login_and_registration_form_already_authenticated(self, url_name): # Create/activate a new account and log in activation_key = create_account(self.USERNAME, self.PASSWORD, self.EMAIL) activate_account(activation_key) result = self.client.login(username=self.USERNAME, password=self.PASSWORD) self.assertTrue(result) # Verify that we're redirected to the dashboard response = self.client.get(reverse(url_name)) self.assertRedirects(response, reverse("dashboard")) @ddt.data( (None, "signin_user"), (None, "register_user"), ("edx.org", "signin_user"), ("edx.org", "register_user"), ) @ddt.unpack def test_login_and_registration_form_signin_not_preserves_params(self, theme, url_name): params = [ ('course_id', 'edX/DemoX/Demo_Course'), ('enrollment_action', 'enroll'), ] # The response should not have a "Sign In" button with the URL # that preserves the querystring params with with_comprehensive_theme_context(theme): response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')])) self.assertNotContains(response, expected_url) # Add additional parameters: params = [ ('course_id', 'edX/DemoX/Demo_Course'), ('enrollment_action', 'enroll'), ('course_mode', CourseMode.DEFAULT_MODE_SLUG), ('email_opt_in', 'true'), ('next', '/custom/final/destination') ] # Verify that this parameter is also preserved with with_comprehensive_theme_context(theme): response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") expected_url = '/login?{}'.format(self._finish_auth_url_param(params)) self.assertNotContains(response, expected_url) @mock.patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) @ddt.data("signin_user", "register_user") def test_third_party_auth_disabled(self, url_name): response = self.client.get(reverse(url_name)) self._assert_third_party_auth_data(response, None, None, [], None) @mock.patch('student_account.views.enterprise_customer_for_request') @ddt.data( ("signin_user", None, None, None), ("register_user", None, None, None), ("signin_user", "google-oauth2", "Google", None), ("register_user", "google-oauth2", "Google", None), ("signin_user", "facebook", "Facebook", None), ("register_user", "facebook", "Facebook", None), ("signin_user", "dummy", "Dummy", None), ("register_user", "dummy", "Dummy", None), ( "signin_user", "google-oauth2", "Google", { 'name': 'FakeName', 'logo': 'https://host.com/logo.jpg', 'welcome_msg': 'No message' } ) ) @ddt.unpack def test_third_party_auth( self, url_name, current_backend, current_provider, expected_enterprise_customer_mock_attrs, enterprise_customer_mock ): params = [ ('course_id', 'course-v1:Org+Course+Run'), ('enrollment_action', 'enroll'), ('course_mode', CourseMode.DEFAULT_MODE_SLUG), ('email_opt_in', 'true'), ('next', '/custom/final/destination'), ] if expected_enterprise_customer_mock_attrs: expected_ec = mock.MagicMock( branding_configuration=mock.MagicMock( logo=mock.MagicMock( url=expected_enterprise_customer_mock_attrs['logo'] ), welcome_message=expected_enterprise_customer_mock_attrs['welcome_msg'] ) ) expected_ec.name = expected_enterprise_customer_mock_attrs['name'] else: expected_ec = None enterprise_customer_mock.return_value = expected_ec # Simulate a running pipeline if current_backend is not None: pipeline_target = "student_account.views.third_party_auth.pipeline" with simulate_running_pipeline(pipeline_target, current_backend): response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") # Do NOT simulate a running pipeline else: response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") # This relies on the THIRD_PARTY_AUTH configuration in the test settings expected_providers = [ { "id": "oa2-dummy", "name": "Dummy", "iconClass": None, "iconImage": settings.MEDIA_URL + "icon.svg", "loginUrl": self._third_party_login_url("dummy", "login", params), "registerUrl": self._third_party_login_url("dummy", "register", params) }, { "id": "oa2-facebook", "name": "Facebook", "iconClass": "fa-facebook", "iconImage": None, "loginUrl": self._third_party_login_url("facebook", "login", params), "registerUrl": self._third_party_login_url("facebook", "register", params) }, { "id": "oa2-google-oauth2", "name": "Google", "iconClass": "fa-google-plus", "iconImage": None, "loginUrl": self._third_party_login_url("google-oauth2", "login", params), "registerUrl": self._third_party_login_url("google-oauth2", "register", params) }, ] self._assert_third_party_auth_data( response, current_backend, current_provider, expected_providers, expected_ec ) def test_hinted_login(self): params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html") self.assertContains(response, '"third_party_auth_hint": "oa2-google-oauth2"') tpa_hint = self.hidden_enabled_provider.provider_id params = [("next", "/courses/something/?tpa_hint={0}".format(tpa_hint))] response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html") self.assertContains(response, '"third_party_auth_hint": "{0}"'.format(tpa_hint)) tpa_hint = self.hidden_disabled_provider.provider_id params = [("next", "/courses/something/?tpa_hint={0}".format(tpa_hint))] response = self.client.get(reverse('signin_user'), params, HTTP_ACCEPT="text/html") self.assertNotIn(response.content, tpa_hint) @ddt.data( ('signin_user', 'login'), ('register_user', 'register'), ) @ddt.unpack @pytest.mark.django111_expected_failure def test_hinted_login_dialog_disabled(self, url_name, auth_entry): """Test that the dialog doesn't show up for hinted logins when disabled. """ self.google_provider.skip_hinted_login_dialog = True self.google_provider.save() params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") self.assertRedirects( response, 'auth/login/google-oauth2/?auth_entry={}&next=%2Fcourses%2Fsomething%2F%3Ftpa_hint%3Doa2-google-oauth2'.format(auth_entry), target_status_code=302 ) @override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2')) @ddt.data( 'signin_user', 'register_user', ) def test_settings_tpa_hinted_login(self, url_name): """ Ensure that settings.FEATURES['THIRD_PARTY_AUTH_HINT'] can set third_party_auth_hint. """ params = [("next", "/courses/something/")] response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") self.assertContains(response, '"third_party_auth_hint": "oa2-google-oauth2"') # THIRD_PARTY_AUTH_HINT can be overridden via the query string tpa_hint = self.hidden_enabled_provider.provider_id params = [("next", "/courses/something/?tpa_hint={0}".format(tpa_hint))] response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") self.assertContains(response, '"third_party_auth_hint": "{0}"'.format(tpa_hint)) # Even disabled providers in the query string will override THIRD_PARTY_AUTH_HINT tpa_hint = self.hidden_disabled_provider.provider_id params = [("next", "/courses/something/?tpa_hint={0}".format(tpa_hint))] response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") self.assertNotIn(response.content, tpa_hint) @override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2')) @ddt.data( ('signin_user', 'login'), ('register_user', 'register'), ) @ddt.unpack @pytest.mark.django111_expected_failure def test_settings_tpa_hinted_login_dialog_disabled(self, url_name, auth_entry): """Test that the dialog doesn't show up for hinted logins when disabled via settings.THIRD_PARTY_AUTH_HINT. """ self.google_provider.skip_hinted_login_dialog = True self.google_provider.save() params = [("next", "/courses/something/")] response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html") self.assertRedirects( response, 'auth/login/google-oauth2/?auth_entry={}&next=%2Fcourses%2Fsomething%2F%3Ftpa_hint%3Doa2-google-oauth2'.format(auth_entry), target_status_code=302 ) @mock.patch('student_account.views.enterprise_customer_for_request') @ddt.data( ('signin_user', False, None, None), ('register_user', False, None, None), ('signin_user', True, 'Fake EC', 'http://logo.com/logo.jpg'), ('register_user', True, 'Fake EC', 'http://logo.com/logo.jpg'), ('signin_user', True, 'Fake EC', None), ('register_user', True, 'Fake EC', None), ) @ddt.unpack def test_enterprise_register(self, url_name, ec_present, ec_name, logo_url, mock_get_ec): """ Verify that when an EnterpriseCustomer is received on the login and register views, the appropriate sidebar is rendered. """ if ec_present: mock_get_ec.return_value = { 'name': ec_name, 'branding_configuration': {'logo': logo_url} } else: mock_get_ec.return_value = None response = self.client.get(reverse(url_name), HTTP_ACCEPT="text/html") enterprise_sidebar_div_id = u'enterprise-content-container' if not ec_present: self.assertNotContains(response, text=enterprise_sidebar_div_id) else: self.assertContains(response, text=enterprise_sidebar_div_id) welcome_message = settings.ENTERPRISE_SPECIFIC_BRANDED_WELCOME_TEMPLATE expected_message = welcome_message.format( start_bold=u'<b>', end_bold=u'</b>', enterprise_name=ec_name, platform_name=settings.PLATFORM_NAME ) self.assertContains(response, expected_message) if logo_url: self.assertContains(response, logo_url) def test_enterprise_cookie_delete(self): """ Test that enterprise cookies are deleted in login/registration views. Cookies must be deleted in login/registration views so that *default* login/registration branding is displayed to subsequent requests from non-enterprise customers. """ cookies = SimpleCookie() cookies[settings.ENTERPRISE_CUSTOMER_COOKIE_NAME] = 'test-enterprise-customer' response = self.client.get(reverse('signin_user'), HTTP_ACCEPT="text/html", cookies=cookies) self.assertIn(settings.ENTERPRISE_CUSTOMER_COOKIE_NAME, response.cookies) # pylint:disable=no-member enterprise_cookie = response.cookies[settings.ENTERPRISE_CUSTOMER_COOKIE_NAME] # pylint:disable=no-member self.assertEqual(enterprise_cookie['domain'], settings.BASE_COOKIE_DOMAIN) self.assertEqual(enterprise_cookie.value, '') @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) def test_microsite_uses_old_login_page(self): # Retrieve the login page from a microsite domain # and verify that we're served the old page. resp = self.client.get( reverse("signin_user"), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertContains(resp, "Log into your Test Site Account") self.assertContains(resp, "login-form") def test_microsite_uses_old_register_page(self): # Retrieve the register page from a microsite domain # and verify that we're served the old page. resp = self.client.get( reverse("register_user"), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertContains(resp, "Register for Test Site") self.assertContains(resp, "register-form") def test_login_registration_xframe_protected(self): resp = self.client.get( reverse("register_user"), {}, HTTP_REFERER="http://localhost/iframe" ) self.assertEqual(resp['X-Frame-Options'], 'DENY') self.configure_lti_provider(name='Test', lti_hostname='localhost', lti_consumer_key='test_key', enabled=True) resp = self.client.get( reverse("register_user"), HTTP_REFERER="http://localhost/iframe" ) self.assertEqual(resp['X-Frame-Options'], 'ALLOW') def _assert_third_party_auth_data(self, response, current_backend, current_provider, providers, expected_ec): """Verify that third party auth info is rendered correctly in a DOM data attribute. """ finish_auth_url = None if current_backend: finish_auth_url = reverse("social:complete", kwargs={"backend": current_backend}) + "?" auth_info = { "currentProvider": current_provider, "providers": providers, "secondaryProviders": [], "finishAuthUrl": finish_auth_url, "errorMessage": None, "registerFormSubmitButtonText": "Create Account", "syncLearnerProfileData": False, } if expected_ec is not None: # If we set an EnterpriseCustomer, third-party auth providers ought to be hidden. auth_info['providers'] = [] auth_info = dump_js_escaped_json(auth_info) expected_data = '"third_party_auth": {auth_info}'.format( auth_info=auth_info ) self.assertContains(response, expected_data) def _third_party_login_url(self, backend_name, auth_entry, login_params): """Construct the login URL to start third party authentication. """ return u"{url}?auth_entry={auth_entry}&{param_str}".format( url=reverse("social:begin", kwargs={"backend": backend_name}), auth_entry=auth_entry, param_str=self._finish_auth_url_param(login_params), ) def _finish_auth_url_param(self, params): """ Make the next=... URL parameter that indicates where the user should go next. >>> _finish_auth_url_param([('next', '/dashboard')]) '/account/finish_auth?next=%2Fdashboard' """ return urlencode({ 'next': '/account/finish_auth?{}'.format(urlencode(params)) }) def test_english_by_default(self): response = self.client.get(reverse('signin_user'), [], HTTP_ACCEPT="text/html") self.assertEqual(response['Content-Language'], 'en') def test_unsupported_language(self): response = self.client.get(reverse('signin_user'), [], HTTP_ACCEPT="text/html", HTTP_ACCEPT_LANGUAGE="ts-zx") self.assertEqual(response['Content-Language'], 'en') def test_browser_language(self): response = self.client.get(reverse('signin_user'), [], HTTP_ACCEPT="text/html", HTTP_ACCEPT_LANGUAGE="es") self.assertEqual(response['Content-Language'], 'es-419') def test_browser_language_dialent(self): response = self.client.get(reverse('signin_user'), [], HTTP_ACCEPT="text/html", HTTP_ACCEPT_LANGUAGE="es-es") self.assertEqual(response['Content-Language'], 'es-es') class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConfigMixin): """ Tests for the account settings view. """ USERNAME = 'student' PASSWORD = 'password' FIELDS = [ 'country', 'gender', 'language', 'level_of_education', 'password', 'year_of_birth', 'preferred_language', 'time_zone', ] @mock.patch("django.conf.settings.MESSAGE_STORAGE", 'django.contrib.messages.storage.cookie.CookieStorage') def setUp(self): super(AccountSettingsViewTest, self).setUp() self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) CommerceConfiguration.objects.create(cache_ttl=10, enabled=True) self.client.login(username=self.USERNAME, password=self.PASSWORD) self.request = HttpRequest() self.request.user = self.user # For these tests, two third party auth providers are enabled by default: self.configure_google_provider(enabled=True, visible=True) self.configure_facebook_provider(enabled=True, visible=True) # Python-social saves auth failure notifcations in Django messages. # See pipeline.get_duplicate_provider() for details. self.request.COOKIES = {} MessageMiddleware().process_request(self.request) messages.error(self.request, 'Facebook is already in use.', extra_tags='Auth facebook') @mock.patch('student_account.views.get_enterprise_learner_data') def test_context(self, mock_get_enterprise_learner_data): self.request.site = SiteFactory.create() mock_get_enterprise_learner_data.return_value = [] context = account_settings_context(self.request) user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username}) self.assertEqual(context['user_accounts_api_url'], user_accounts_api_url) user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username}) self.assertEqual(context['user_preferences_api_url'], user_preferences_api_url) for attribute in self.FIELDS: self.assertIn(attribute, context['fields']) self.assertEqual( context['user_accounts_api_url'], reverse("accounts_api", kwargs={'username': self.user.username}) ) self.assertEqual( context['user_preferences_api_url'], reverse('preferences_api', kwargs={'username': self.user.username}) ) self.assertEqual(context['duplicate_provider'], 'facebook') self.assertEqual(context['auth']['providers'][0]['name'], 'Facebook') self.assertEqual(context['auth']['providers'][1]['name'], 'Google') self.assertEqual(context['sync_learner_profile_data'], False) self.assertEqual(context['edx_support_url'], settings.SUPPORT_SITE_LINK) self.assertEqual(context['enterprise_name'], None) self.assertEqual( context['enterprise_readonly_account_fields'], {'fields': settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS} ) @mock.patch('student_account.views.get_enterprise_learner_data') @mock.patch('student_account.views.third_party_auth.provider.Registry.get') def test_context_for_enterprise_learner( self, mock_get_auth_provider, mock_get_enterprise_learner_data ): dummy_enterprise_customer = { 'uuid': 'real-ent-uuid', 'name': 'Dummy Enterprise', 'identity_provider': 'saml-ubc' } mock_get_enterprise_learner_data.return_value = [ {'enterprise_customer': dummy_enterprise_customer} ] self.request.site = SiteFactory.create() mock_get_auth_provider.return_value.sync_learner_profile_data = True context = account_settings_context(self.request) user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username}) self.assertEqual(context['user_accounts_api_url'], user_accounts_api_url) user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username}) self.assertEqual(context['user_preferences_api_url'], user_preferences_api_url) for attribute in self.FIELDS: self.assertIn(attribute, context['fields']) self.assertEqual( context['user_accounts_api_url'], reverse("accounts_api", kwargs={'username': self.user.username}) ) self.assertEqual( context['user_preferences_api_url'], reverse('preferences_api', kwargs={'username': self.user.username}) ) self.assertEqual(context['duplicate_provider'], 'facebook') self.assertEqual(context['auth']['providers'][0]['name'], 'Facebook') self.assertEqual(context['auth']['providers'][1]['name'], 'Google') self.assertEqual( context['sync_learner_profile_data'], mock_get_auth_provider.return_value.sync_learner_profile_data ) self.assertEqual(context['edx_support_url'], settings.SUPPORT_SITE_LINK) self.assertEqual(context['enterprise_name'], dummy_enterprise_customer['name']) self.assertEqual( context['enterprise_readonly_account_fields'], {'fields': settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS} ) def test_view(self): """ Test that all fields are visible """ view_path = reverse('account_settings') response = self.client.get(path=view_path) for attribute in self.FIELDS: self.assertIn(attribute, response.content) def test_header_with_programs_listing_enabled(self): """ Verify that tabs header will be shown while program listing is enabled. """ self.create_programs_config() view_path = reverse('account_settings') response = self.client.get(path=view_path) self.assertContains(response, 'global-header') def test_header_with_programs_listing_disabled(self): """ Verify that nav header will be shown while program listing is disabled. """ self.create_programs_config(enabled=False) view_path = reverse('account_settings') response = self.client.get(path=view_path) self.assertContains(response, 'global-header') def test_commerce_order_detail(self): """ Verify that get_user_orders returns the correct order data. """ with mock_get_orders(): order_detail = get_user_orders(self.user) for i, order in enumerate(mock_get_orders.default_response['results']): expected = { 'number': order['number'], 'price': order['total_excl_tax'], 'order_date': 'Jan 01, 2016', 'receipt_url': '/checkout/receipt/?order_number=' + order['number'], 'lines': order['lines'], } self.assertEqual(order_detail[i], expected) def test_commerce_order_detail_exception(self): with mock_get_orders(exception=exceptions.HttpNotFoundError): order_detail = get_user_orders(self.user) self.assertEqual(order_detail, []) def test_incomplete_order_detail(self): response = { 'results': [ factories.OrderFactory( status='Incomplete', lines=[ factories.OrderLineFactory( product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory()]) ) ] ) ] } with mock_get_orders(response=response): order_detail = get_user_orders(self.user) self.assertEqual(order_detail, []) def test_order_history_with_no_product(self): response = { 'results': [ factories.OrderFactory( lines=[ factories.OrderLineFactory( product=None ), factories.OrderLineFactory( product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory( name='certificate_type', value='verified' )]) ) ] ) ] } with mock_get_orders(response=response): order_detail = get_user_orders(self.user) self.assertEqual(len(order_detail), 1) @override_settings(SITE_NAME=settings.MICROSITE_LOGISTRATION_HOSTNAME) class MicrositeLogistrationTests(TestCase): """ Test to validate that microsites can display the logistration page """ def test_login_page(self): """ Make sure that we get the expected logistration page on our specialized microsite """ resp = self.client.get( reverse('signin_user'), HTTP_HOST=settings.MICROSITE_LOGISTRATION_HOSTNAME ) self.assertEqual(resp.status_code, 200) self.assertIn('<div id="login-and-registration-container"', resp.content) def test_registration_page(self): """ Make sure that we get the expected logistration page on our specialized microsite """ resp = self.client.get( reverse('register_user'), HTTP_HOST=settings.MICROSITE_LOGISTRATION_HOSTNAME ) self.assertEqual(resp.status_code, 200) self.assertIn('<div id="login-and-registration-container"', resp.content) @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) def test_no_override(self): """ Make sure we get the old style login/registration if we don't override """ resp = self.client.get( reverse('signin_user'), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertEqual(resp.status_code, 200) self.assertNotIn('<div id="login-and-registration-container"', resp.content) resp = self.client.get( reverse('register_user'), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertEqual(resp.status_code, 200) self.assertNotIn('<div id="login-and-registration-container"', resp.content) class AccountCreationTestCaseWithSiteOverrides(SiteMixin, TestCase): """ Test cases for Feature flag ALLOW_PUBLIC_ACCOUNT_CREATION which when turned off disables the account creation options in lms """ def setUp(self): """Set up the tests""" super(AccountCreationTestCaseWithSiteOverrides, self).setUp() # Set the feature flag ALLOW_PUBLIC_ACCOUNT_CREATION to False self.site_configuration_values = { 'ALLOW_PUBLIC_ACCOUNT_CREATION': False } self.site_domain = 'testserver1.com' self.set_up_site(self.site_domain, self.site_configuration_values) def test_register_option_login_page(self): """ Navigate to the login page and check the Register option is hidden when ALLOW_PUBLIC_ACCOUNT_CREATION flag is turned off """ response = self.client.get(reverse('signin_user')) self.assertNotIn('<a class="btn-neutral" href="/register?next=%2Fdashboard">Register</a>', response.content)