Ejemplo n.º 1
0
class SendAccountActivationEmail(UserAPITestCase):
    """
    Test for send activation email view
    """

    def setUp(self):
        """
        Create a user, then log in.
        """
        super().setUp()
        self.user = UserFactory()
        Registration().register(self.user)
        result = self.client.login(username=self.user.username, password="******")
        assert result, 'Could not log in'
        self.path = reverse('send_account_activation_email')

    @patch('common.djangoapps.student.views.management.compose_activation_email')
    def test_send_email_to_inactive_user_via_cta_dialog(self, email):
        """
        Tests when user clicks on resend activation email on CTA dialog box, system
        sends an activation email to the user.
        """
        self.user.is_active = False
        self.user.save()
        self.client.post(self.path)
        assert email.called is True, 'method should have been called'
Ejemplo n.º 2
0
    def test_enroll_inactive_user_again(self, auto_enroll):
        course_key = CourseLocator('Robot', 'fAKE', 'C--se--ID')
        before_ideal = SettableEnrollmentState(
            user=True,
            enrollment=False,
            allowed=True,
            auto_enroll=auto_enroll,
        )
        print("checking initialization...")
        user = UserFactory()
        user.is_active = False
        user.save()
        eobjs = EnrollmentObjects(
            user.email, None, None,
            CourseEnrollmentAllowed.objects.create(email=user.email,
                                                   course_id=course_key,
                                                   auto_enroll=auto_enroll))
        before = EmailEnrollmentState(course_key, eobjs.email)
        self.assertEqual(before, before_ideal)

        print('running action...')
        enroll_email(self.course_key, eobjs.email, auto_enroll=auto_enroll)

        print('checking effects...')

        after_ideal = SettableEnrollmentState(
            user=True,
            enrollment=False,
            allowed=True,
            auto_enroll=auto_enroll,
        )
        after = EmailEnrollmentState(self.course_key, eobjs.email)
        self.assertEqual(after, after_ideal)
Ejemplo n.º 3
0
    def test_create_permissions(self):
        """ Users should only be allowed to create data for themselves. """
        url = reverse('api_experiments:v0:data-list')

        # Authentication is required
        response = self.client.post(url, {})
        assert response.status_code == 401

        user = UserFactory()
        data = {
            'experiment_id': 1,
            'key': 'foo',
            'value': 'bar',
        }
        self.client.login(username=user.username,
                          password=UserFactory._DEFAULT_PASSWORD)  # lint-amnesty, pylint: disable=protected-access

        # Users can create data for themselves
        response = self.client.post(url, data)
        assert response.status_code == 201
        ExperimentData.objects.get(user=user)

        # A non-staff user cannot create data for another user
        other_user = UserFactory()
        data['user'] = other_user.username
        response = self.client.post(url, data)
        assert response.status_code == 403
        assert not ExperimentData.objects.filter(user=other_user).exists()

        # A staff user can create data for other users
        user.is_staff = True
        user.save()
        response = self.client.post(url, data)
        assert response.status_code == 201
        ExperimentData.objects.get(user=other_user)
Ejemplo n.º 4
0
    def test_identify_call_on_user_change(self):
        user = UserFactory()

        with patch('openedx.core.djangoapps.user_authn.signals.segment') as mock_segment:
            user.email = '*****@*****.**'
            user.save()
        assert mock_segment.identify.call_count == 1
        assert mock_segment.identify.call_args[0] == (user.id, {'email': '*****@*****.**'})
Ejemplo n.º 5
0
def test_send_user_info_to_mailchimp_user_updated(mocker, mailchimp_handlers):
    """
    Assert that `send_user_info_to_mailchimp` called with appropriate params and celery task for sync is called
    specific number of times and is not called for User update.
    """
    mock_task = mocker.patch.object(mailchimp_handlers,
                                    'task_send_user_info_to_mailchimp')
    user = UserFactory()
    user.username = '******'
    user.save()

    assert mock_task.delay.call_count == 2
Ejemplo n.º 6
0
 def test_successful_handle(self):
     """
     Tests a UserTour is created when a new User is created.
     Then ensures a new UserTour is not created when the user is updated.
     """
     assert UserTour.objects.count() == 0
     user = UserFactory()
     tour = UserTour.objects.get(user=user)
     assert tour.course_home_tour_status == UserTour.CourseHomeChoices.NEW_USER_TOUR
     assert tour.show_courseware_tour is True
     user.username = '******'
     user.save()
     assert UserTour.objects.count() == 1
Ejemplo n.º 7
0
    def test_session_auth(self):
        """ Verify the endpoint supports session authentication, and only allows authorization for staff users. """
        user = UserFactory(password=self.password, is_staff=False)
        self.client.login(username=user.username, password=self.password)

        # Non-staff users should not have access to the API
        response = self.client.get(self.path)
        assert response.status_code == 403

        # Staff users should have access to the API
        user.is_staff = True
        user.save()
        response = self.client.get(self.path)
        assert response.status_code == 200
Ejemplo n.º 8
0
    def test_retired_username(self):
        """
        Ensure that a retired username cannot be registered again.
        """
        user = UserFactory()
        orig_username = user.username

        # Fake retirement of the username.
        user.username = get_retired_username_by_username(orig_username)
        user.save()

        # Attempt to create another account with the same username that's been retired.
        self.url_params['username'] = orig_username
        response = self.client.post(self.url, self.url_params)
        self._validate_exiting_username_response(orig_username, response, self.INVALID_ERR_MSG[0], self.INVALID_ERR_MSG[1])  # lint-amnesty, pylint: disable=line-too-long
Ejemplo n.º 9
0
    def test_oauth(self):
        """ Verify the endpoint supports OAuth, and only allows authorization for staff users. """
        user = UserFactory(is_staff=False)
        oauth_client = ApplicationFactory.create()
        access_token = AccessTokenFactory.create(
            user=user, application=oauth_client).token
        headers = {'HTTP_AUTHORIZATION': 'Bearer ' + access_token}

        # Non-staff users should not have access to the API
        response = self.client.get(self.path, **headers)
        assert response.status_code == 403

        # Staff users should have access to the API
        user.is_staff = True
        user.save()
        response = self.client.get(self.path, **headers)
        assert response.status_code == 200
Ejemplo n.º 10
0
    def test_oauth_list(self, path_name):
        """ Verify the endpoints supports OAuth, and only allows authorization for staff users. """
        path = reverse(path_name,
                       kwargs={'course_key_string': self.course_str})
        user = UserFactory(is_staff=False)
        oauth_client = ApplicationFactory.create()
        access_token = AccessTokenFactory.create(
            user=user, application=oauth_client).token
        headers = {'HTTP_AUTHORIZATION': 'Bearer ' + access_token}

        # Non-staff users should not have access to the API
        response = self.client.get(path=path, **headers)
        self.assertEqual(response.status_code, 403)

        # Staff users should have access to the API
        user.is_staff = True
        user.save()
        response = self.client.get(path=path, **headers)
        self.assertEqual(response.status_code, 200)
Ejemplo n.º 11
0
    def test_oauth_csv(self):
        """ Verify the endpoint supports OAuth, and only allows authorization for staff users. """
        cohorts.add_cohort(self.course_key, "DEFAULT", "random")
        path = reverse('api_cohorts:cohort_users_csv',
                       kwargs={'course_key_string': self.course_str})
        user = UserFactory(is_staff=False)
        oauth_client = ApplicationFactory.create()
        access_token = AccessTokenFactory.create(
            user=user, application=oauth_client).token
        headers = {'HTTP_AUTHORIZATION': 'Bearer ' + access_token}

        # Non-staff users should not have access to the API
        response = self.client.post(path=path, **headers)
        assert response.status_code == 403

        # Staff users should have access to the API
        user.is_staff = True
        user.save()
        response = self.client.post(path=path, **headers)
        assert response.status_code == 400
Ejemplo n.º 12
0
class ZendeskProxyTestCase(ApiTestCase):
    """Tests for zendesk_proxy views."""
    def setUp(self):
        self.url = reverse('zendesk_proxy_v1')
        self.user = UserFactory(username='******', password='******')
        self.client.login(username='******', password='******')
        self.request_data = {
            'requester': {
                'email': self.user.email,
                'name': self.user.username
            },
            'subject': 'Python Unit Test Help Request',
            'comment': {
                'body':
                "Help! I'm trapped in a unit test factory and I can't get out!",
            },
            'tags': ['python_unit_test'],
            'custom_fields': [{
                'id': '001',
                'value': 'demo-course'
            }],
        }
        return super(ZendeskProxyTestCase, self).setUp()  # lint-amnesty, pylint: disable=super-with-arguments

    @ddt.data(True, False)
    def test_post(self, user_activation_status):
        """
        Test both active and inactive users can request Zendesk Proxy for the
        submission of support tickets.
        """
        self.user.is_active = user_activation_status
        self.user.save()
        with patch('requests.post',
                   return_value=MagicMock(status_code=201)) as mock_post:
            response = self.request_without_auth(
                'post',
                self.url,
                data=json.dumps(self.request_data),
                content_type='application/json')
            self.assertHttpCreated(response)
            (mock_args, mock_kwargs) = mock_post.call_args
            assert mock_args == (
                'https://www.superrealurlsthataredefinitelynotfake.com/api/v2/tickets.json',
            )
            six.assertCountEqual(self, mock_kwargs.keys(), ['headers', 'data'])
            assert mock_kwargs['headers'] == {
                'content-type': 'application/json',
                'Authorization': 'Bearer abcdefghijklmnopqrstuvwxyz1234567890'
            }
            assert json.loads(mock_kwargs['data']) == {
                'ticket': {
                    'comment': {
                        'body':
                        "Help! I'm trapped in a unit test factory and I can't get out!",
                        'uploads': None
                    },
                    'custom_fields': [{
                        'id': '001',
                        'value': 'demo-course'
                    }],
                    'requester': {
                        'email': self.user.email,
                        'name': self.user.username
                    },
                    'subject': 'Python Unit Test Help Request',
                    'tags': ['python_unit_test']
                }
            }

    @ddt.data('subject', 'tags')
    def test_bad_request(self, key_to_delete):
        test_data = deepcopy(self.request_data)
        _ = test_data.pop(key_to_delete)

        response = self.request_without_auth('post',
                                             self.url,
                                             data=json.dumps(test_data),
                                             content_type='application/json')
        self.assertHttpBadRequest(response)

    @override_settings(
        CACHES={
            'default': {
                'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
                'LOCATION': 'zendesk_proxy',
            }
        })
    def test_rate_limiting(self):
        """
        Confirm rate limits work as expected. Note that drf's rate limiting makes use of the default cache to enforce
        limits; that's why this test needs a "real" default cache (as opposed to the usual-for-tests DummyCache)
        """

        for _ in range(ZendeskProxyThrottle().num_requests):
            self.request_without_auth('post', self.url)
        response = self.request_without_auth('post', self.url)
        assert response.status_code == 429
Ejemplo n.º 13
0
class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, CompletionWaffleTestMixin):
    """
    Tests for the student dashboard.
    """

    EMAIL_SETTINGS_ELEMENT_ID = "#actions-item-email-settings-0"
    ENABLED_SIGNALS = ['course_published']
    TOMORROW = now() + timedelta(days=1)
    THREE_YEARS_FROM_NOW = now() + timedelta(days=(365 * 3))
    THREE_YEARS_AGO = now() - timedelta(days=(365 * 3))
    MOCK_SETTINGS = {
        'FEATURES': {
            'DISABLE_START_DATES': False,
            'ENABLE_MKTG_SITE': True,
            'DISABLE_SET_JWT_COOKIES_FOR_TESTS': True,
        },
        'SOCIAL_SHARING_SETTINGS': {
            'CUSTOM_COURSE_URLS': True,
            'DASHBOARD_FACEBOOK': True,
            'DASHBOARD_TWITTER': True,
        },
    }
    MOCK_SETTINGS_HIDE_COURSES = {
        'FEATURES': {
            'HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED': True,
            'DISABLE_SET_JWT_COOKIES_FOR_TESTS': True,
        }
    }

    def setUp(self):
        """
        Create a course and user, then log in.
        """
        super(StudentDashboardTests, self).setUp()
        self.user = UserFactory()
        self.client.login(username=self.user.username, password=PASSWORD)
        self.path = reverse('dashboard')

    def set_course_sharing_urls(self, set_marketing, set_social_sharing):
        """
        Set course sharing urls (i.e. social_sharing_url, marketing_url)
        """
        course_overview = self.course_enrollment.course_overview
        if set_marketing:
            course_overview.marketing_url = 'http://www.testurl.com/marketing/url/'

        if set_social_sharing:
            course_overview.social_sharing_url = 'http://www.testurl.com/social/url/'

        course_overview.save()

    def test_redirect_account_settings(self):
        """
        Verify if user does not have profile he/she is redirected to account_settings.
        """
        UserProfile.objects.get(user=self.user).delete()
        response = self.client.get(self.path)
        self.assertRedirects(response, reverse('account_settings'))

    def test_grade_appears_before_course_end_date(self):
        """
        Verify that learners are not able to see their final grade before the end
        of course in the learner dashboard
        """
        self.course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
        self.course = CourseOverviewFactory.create(id=self.course_key, end_date=self.TOMORROW,
                                                   certificate_available_date=self.THREE_YEARS_AGO,
                                                   lowest_passing_grade=0.3)
        self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
        GeneratedCertificateFactory(status='notpassing', course_id=self.course.id, user=self.user, grade=0.45)

        response = self.client.get(reverse('dashboard'))
        # The final grade does not appear before the course has ended
        self.assertContains(response, 'Your final grade:')
        self.assertContains(response, '<span class="grade-value">45%</span>')

    def test_grade_not_appears_before_cert_available_date(self):
        """
        Verify that learners are able to see their final grade of the course in
        the learner dashboard after the course had ended
        """
        self.course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
        self.course = CourseOverviewFactory.create(id=self.course_key, end_date=self.THREE_YEARS_AGO,
                                                   certificate_available_date=self.TOMORROW,
                                                   lowest_passing_grade=0.3)
        self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
        GeneratedCertificateFactory(status='notpassing', course_id=self.course.id, user=self.user, grade=0.45)

        response = self.client.get(reverse('dashboard'))
        self.assertNotContains(response, 'Your final grade:')
        self.assertNotContains(response, '<span class="grade-value">45%</span>')

    @patch.multiple('django.conf.settings', **MOCK_SETTINGS)
    @ddt.data(
        *itertools.product(
            [True, False],
            [True, False],
            [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split],
        )
    )
    @ddt.unpack
    def test_sharing_icons_for_future_course(self, set_marketing, set_social_sharing, modulestore_type):
        """
        Verify that the course sharing icons show up if course is starting in future and
        any of marketing or social sharing urls are set.
        """
        self.course = CourseFactory.create(start=self.TOMORROW, emit_signals=True, default_store=modulestore_type)
        self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
        self.set_course_sharing_urls(set_marketing, set_social_sharing)

        # Assert course sharing icons
        response = self.client.get(reverse('dashboard'))
        self.assertEqual('Share on Twitter' in response.content.decode('utf-8'), set_marketing or set_social_sharing)
        self.assertEqual('Share on Facebook' in response.content.decode('utf-8'), set_marketing or set_social_sharing)

    @patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
    def test_pre_requisites_appear_on_dashboard(self):
        """
        When a course has a prerequisite, the dashboard should display the prerequisite.
        If we remove the prerequisite and access the dashboard again, the prerequisite
        should not appear.
        """
        self.pre_requisite_course = CourseFactory.create(org='edx', number='999', display_name='Pre requisite Course')
        self.course = CourseFactory.create(
            org='edx',
            number='998',
            display_name='Test Course',
            pre_requisite_courses=[six.text_type(self.pre_requisite_course.id)]
        )
        self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user)

        set_prerequisite_courses(self.course.id, [six.text_type(self.pre_requisite_course.id)])
        response = self.client.get(reverse('dashboard'))
        self.assertContains(response, '<div class="prerequisites">')

        remove_prerequisite_course(self.course.id, get_course_milestones(self.course.id)[0])
        response = self.client.get(reverse('dashboard'))
        self.assertNotContains(response, '<div class="prerequisites">')

    @patch('openedx.core.djangoapps.programs.utils.get_programs')
    @patch('common.djangoapps.student.views.dashboard.get_visible_sessions_for_entitlement')
    @patch('common.djangoapps.student.views.dashboard.get_pseudo_session_for_entitlement')
    @patch.object(CourseOverview, 'get_from_id')
    def test_unfulfilled_entitlement(self, mock_course_overview, mock_pseudo_session,
                                     mock_course_runs, mock_get_programs):
        """
        When a learner has an unfulfilled entitlement, their course dashboard should have:
            - a hidden 'View Course' button
            - the text 'In order to view the course you must select a session:'
            - an unhidden course-entitlement-selection-container
            - a related programs message
        """
        program = ProgramFactory()
        CourseEntitlementFactory.create(user=self.user, course_uuid=program['courses'][0]['uuid'])
        mock_get_programs.return_value = [program]
        course_key = CourseKey.from_string('course-v1:FAKE+FA1-MA1.X+3T2017')
        mock_course_overview.return_value = CourseOverviewFactory.create(start=self.TOMORROW, id=course_key)
        mock_course_runs.return_value = [
            {
                'key': six.text_type(course_key),
                'enrollment_end': str(self.TOMORROW),
                'pacing_type': 'instructor_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        mock_pseudo_session.return_value = {
            'key': six.text_type(course_key),
            'type': 'verified'
        }
        response = self.client.get(self.path)
        self.assertContains(response, 'class="course-target-link enter-course hidden"')
        self.assertContains(response, 'You must select a session to access the course.')
        self.assertContains(response, '<div class="course-entitlement-selection-container ">')
        self.assertContains(response, 'Related Programs:')

        # If an entitlement has already been redeemed by the user for a course run, do not let the run be selectable
        enrollment = CourseEnrollmentFactory(
            user=self.user, course=mock_course_overview.return_value, mode=CourseMode.VERIFIED
        )
        CourseEntitlementFactory.create(
            user=self.user, course_uuid=program['courses'][0]['uuid'], enrollment_course_run=enrollment
        )

        mock_course_runs.return_value = [
            {
                'key': 'course-v1:edX+toy+2012_Fall',
                'enrollment_end': str(self.TOMORROW),
                'pacing_type': 'instructor_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        response = self.client.get(self.path)
        # There should be two entitlements on the course page, one prompting for a mandatory session, but no
        # select option for the courses as there is only the single course run which has already been redeemed
        self.assertContains(response, '<li class="course-item">', count=2)
        self.assertContains(response, 'You must select a session to access the course.')
        self.assertNotContains(response, 'To access the course, select a session.')

    @patch('common.djangoapps.student.views.dashboard.get_visible_sessions_for_entitlement')
    @patch.object(CourseOverview, 'get_from_id')
    def test_unfulfilled_expired_entitlement(self, mock_course_overview, mock_course_runs):
        """
        When a learner has an unfulfilled, expired entitlement, a card should NOT appear on the dashboard.
        This use case represents either an entitlement that the user waited too long to fulfill, or an entitlement
        for which they received a refund.
        """
        CourseEntitlementFactory(
            user=self.user,
            created=self.THREE_YEARS_AGO,
            expired_at=now()
        )
        mock_course_overview.return_value = CourseOverviewFactory(start=self.TOMORROW)
        mock_course_runs.return_value = [
            {
                'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
                'enrollment_end': str(self.TOMORROW),
                'pacing_type': 'instructor_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        response = self.client.get(self.path)
        self.assertNotContains(response, '<li class="course-item">')

    @patch('common.djangoapps.entitlements.rest_api.v1.views.get_course_runs_for_course')
    @patch.object(CourseOverview, 'get_from_id')
    def test_sessions_for_entitlement_course_runs(self, mock_course_overview, mock_course_runs):
        """
        When a learner has a fulfilled entitlement for a course run in the past, there should be no availableSession
        data passed to the JS view. When a learner has a fulfilled entitlement for a course run enrollment ending in the
        future, there should not be an empty availableSession variable. When a learner has a fulfilled entitlement
        for a course that doesn't have an enrollment ending, there should not be an empty availableSession variable.

        NOTE: We commented out the assertions to move this to the catalog utils test suite.
        """
        # noAvailableSessions = "availableSessions: '[]'"

        # Test an enrollment end in the past
        mocked_course_overview = CourseOverviewFactory.create(
            start=self.TOMORROW, end=self.THREE_YEARS_FROM_NOW, self_paced=True, enrollment_end=self.THREE_YEARS_AGO
        )
        mock_course_overview.return_value = mocked_course_overview
        course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=six.text_type(mocked_course_overview.id))
        mock_course_runs.return_value = [
            {
                'key': str(mocked_course_overview.id),
                'enrollment_end': str(mocked_course_overview.enrollment_end),
                'pacing_type': 'self_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
        # response = self.client.get(self.path)
        # self.assertIn(noAvailableSessions, response.content)

        # Test an enrollment end in the future sets an availableSession
        mocked_course_overview.enrollment_end = self.TOMORROW
        mocked_course_overview.save()

        mock_course_overview.return_value = mocked_course_overview
        mock_course_runs.return_value = [
            {
                'key': str(mocked_course_overview.id),
                'enrollment_end': str(mocked_course_overview.enrollment_end),
                'pacing_type': 'self_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        # response = self.client.get(self.path)
        # self.assertNotIn(noAvailableSessions, response.content)

        # Test an enrollment end that doesn't exist sets an availableSession
        mocked_course_overview.enrollment_end = None
        mocked_course_overview.save()

        mock_course_overview.return_value = mocked_course_overview
        mock_course_runs.return_value = [
            {
                'key': str(mocked_course_overview.id),
                'enrollment_end': None,
                'pacing_type': 'self_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        # response = self.client.get(self.path)
        # self.assertNotIn(noAvailableSessions, response.content)

    @patch('openedx.core.djangoapps.programs.utils.get_programs')
    @patch('common.djangoapps.student.views.dashboard.get_visible_sessions_for_entitlement')
    @patch.object(CourseOverview, 'get_from_id')
    def test_fulfilled_entitlement(self, mock_course_overview, mock_course_runs, mock_get_programs):
        """
        When a learner has a fulfilled entitlement, their course dashboard should have:
            - exactly one course item, meaning it:
                - has an entitlement card
                - does NOT have a course card referencing the selected session
            - an unhidden Change or Leave Session button
            - a related programs message
        """
        mocked_course_overview = CourseOverviewFactory(
            start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
        )
        mock_course_overview.return_value = mocked_course_overview
        course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=six.text_type(mocked_course_overview.id))
        mock_course_runs.return_value = [
            {
                'key': str(mocked_course_overview.id),
                'enrollment_end': str(mocked_course_overview.enrollment_end),
                'pacing_type': 'self_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
        program = ProgramFactory()
        program['courses'][0]['course_runs'] = [{'key': six.text_type(mocked_course_overview.id)}]
        program['courses'][0]['uuid'] = entitlement.course_uuid
        mock_get_programs.return_value = [program]
        response = self.client.get(self.path)
        self.assertContains(response, '<li class="course-item">', count=1)
        self.assertContains(response, '<button class="change-session btn-link "')
        self.assertContains(response, 'Related Programs:')

    @patch('openedx.core.djangoapps.programs.utils.get_programs')
    @patch('common.djangoapps.student.views.dashboard.get_visible_sessions_for_entitlement')
    @patch.object(CourseOverview, 'get_from_id')
    def test_fulfilled_expired_entitlement(self, mock_course_overview, mock_course_runs, mock_get_programs):
        """
        When a learner has a fulfilled entitlement that is expired, their course dashboard should have:
            - exactly one course item, meaning it:
                - has an entitlement card
            - Message that the learner can no longer change sessions
            - a related programs message
        """
        mocked_course_overview = CourseOverviewFactory(
            start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
        )
        mock_course_overview.return_value = mocked_course_overview
        course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=six.text_type(mocked_course_overview.id), created=self.THREE_YEARS_AGO)
        mock_course_runs.return_value = [
            {
                'key': str(mocked_course_overview.id),
                'enrollment_end': str(mocked_course_overview.enrollment_end),
                'pacing_type': 'self_paced',
                'type': 'verified',
                'status': 'published'
            }
        ]
        entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=self.THREE_YEARS_AGO)
        program = ProgramFactory()
        program['courses'][0]['course_runs'] = [{'key': six.text_type(mocked_course_overview.id)}]
        program['courses'][0]['uuid'] = entitlement.course_uuid
        mock_get_programs.return_value = [program]
        response = self.client.get(self.path)
        self.assertContains(response, '<li class="course-item">', count=1)
        self.assertContains(response, 'You can no longer change sessions.')
        self.assertContains(response, 'Related Programs:')

    @patch('openedx.core.djangoapps.catalog.utils.get_course_runs_for_course')
    @patch('common.djangoapps.student.views.dashboard.is_bulk_email_feature_enabled')
    def test_email_settings_fulfilled_entitlement(self, mock_email_feature, mock_get_course_runs):
        """
        Assert that the Email Settings action is shown when the user has a fulfilled entitlement.
        """
        mock_email_feature.return_value = True
        course_overview = CourseOverviewFactory(
            start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
        )
        course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=course_overview.id)
        entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
        course_runs = [{
            'key': six.text_type(course_overview.id),
            'uuid': entitlement.course_uuid
        }]
        mock_get_course_runs.return_value = course_runs

        response = self.client.get(self.path)
        self.assertEqual(pq(response.content)(self.EMAIL_SETTINGS_ELEMENT_ID).length, 1)

    @patch.object(CourseOverview, 'get_from_id')
    @patch('common.djangoapps.student.views.dashboard.is_bulk_email_feature_enabled')
    def test_email_settings_unfulfilled_entitlement(self, mock_email_feature, mock_course_overview):
        """
        Assert that the Email Settings action is not shown when the entitlement is not fulfilled.
        """
        mock_email_feature.return_value = True
        mock_course_overview.return_value = CourseOverviewFactory(start=self.TOMORROW)
        CourseEntitlementFactory(user=self.user)
        response = self.client.get(self.path)
        self.assertEqual(pq(response.content)(self.EMAIL_SETTINGS_ELEMENT_ID).length, 0)

    @patch.multiple('django.conf.settings', **MOCK_SETTINGS_HIDE_COURSES)
    def test_hide_dashboard_courses_until_activated(self):
        """
        Verify that when the HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED feature is enabled,
        inactive users don't see the Courses list, but active users still do.
        """
        # Ensure active users see the course list
        self.assertTrue(self.user.is_active)
        response = self.client.get(reverse('dashboard'))
        self.assertContains(response, 'You are not enrolled in any courses yet.')

        # Ensure inactive users don't see the course list
        self.user.is_active = False
        self.user.save()
        response = self.client.get(reverse('dashboard'))
        self.assertNotContains(response, 'You are not enrolled in any courses yet.')

    def test_show_empty_dashboard_message(self):
        """
        Verify that when the EMPTY_DASHBOARD_MESSAGE feature is set,
        its text is displayed in an empty courses list.
        """
        empty_dashboard_message = "Check out our lovely <i>free</i> courses!"
        response = self.client.get(reverse('dashboard'))
        self.assertContains(response, 'You are not enrolled in any courses yet.')
        self.assertNotContains(response, empty_dashboard_message)

        with with_site_configuration_context(configuration={
            "EMPTY_DASHBOARD_MESSAGE": empty_dashboard_message,
        }):
            response = self.client.get(reverse('dashboard'))
            self.assertContains(response, 'You are not enrolled in any courses yet.')
            self.assertContains(response, empty_dashboard_message)

    @patch('django.conf.settings.DASHBOARD_COURSE_LIMIT', 1)
    def test_course_limit_on_dashboard(self):
        course = CourseFactory.create()
        CourseEnrollmentFactory(
            user=self.user,
            course_id=course.id
        )

        course_v1 = CourseFactory.create()
        CourseEnrollmentFactory(
            user=self.user,
            course_id=course_v1.id
        )

        response = self.client.get(reverse('dashboard'))
        self.assertContains(response, '1 results successfully populated')

    @staticmethod
    def _remove_whitespace_from_html_string(html):
        return ''.join(html.split())

    @staticmethod
    def _remove_whitespace_from_response(response):
        return ''.join(response.content.decode('utf-8').split())

    @staticmethod
    def _pull_course_run_from_course_key(course_key_string):
        search_results = re.search(r'Run_[0-9]+$', course_key_string)
        assert search_results
        course_run_string = search_results.group(0).replace('_', ' ')
        return course_run_string

    @staticmethod
    def _get_html_for_view_course_button(course_key_string, course_run_string):
        return '''
            <a href="/courses/{course_key}/course/"
               class="course-target-link enter-course"
               data-course-key="{course_key}">
              View Course
              <span class="sr">
                &nbsp;{course_run}
              </span>
            </a>
        '''.format(course_key=course_key_string, course_run=course_run_string)

    @staticmethod
    def _get_html_for_resume_course_button(course_key_string, resume_block_key_string, course_run_string):
        return '''
            <a href="/courses/{course_key}/jump_to/{url_to_block}"
               class="course-target-link enter-course"
               data-course-key="{course_key}">
              Resume Course
              <span class="sr">
                &nbsp;{course_run}
              </span>
            </a>
        '''.format(
            course_key=course_key_string,
            url_to_block=resume_block_key_string,
            course_run=course_run_string
        )

    @staticmethod
    def _get_html_for_entitlement_button(course_key_string):
        return'''
            <div class="course-info">
            <span class="info-university">{org} - </span>
            <span class="info-course-id">{course}</span>
            <span class="info-date-block-container">
            <button class="change-session btn-link ">Change or Leave Session</button>
            </span>
            </div>
        '''.format(
            org=course_key_string.split('/')[0],
            course=course_key_string.split('/')[1]
        )

    def test_view_course_appears_on_dashboard(self):
        """
        When a course doesn't have completion data, its course card should
        display a "View Course" button.
        """
        self.override_waffle_switch(True)

        course = CourseFactory.create()
        CourseEnrollmentFactory.create(
            user=self.user,
            course_id=course.id
        )

        response = self.client.get(reverse('dashboard'))

        course_key_string = str(course.id)
        # No completion data means there's no block from which to resume.
        resume_block_key_string = ''
        course_run_string = self._pull_course_run_from_course_key(course_key_string)

        view_button_html = self._get_html_for_view_course_button(
            course_key_string,
            course_run_string
        )
        resume_button_html = self._get_html_for_resume_course_button(
            course_key_string,
            resume_block_key_string,
            course_run_string
        )

        view_button_html = self._remove_whitespace_from_html_string(view_button_html)
        resume_button_html = self._remove_whitespace_from_html_string(resume_button_html)
        dashboard_html = self._remove_whitespace_from_response(response)

        self.assertIn(
            view_button_html,
            dashboard_html
        )
        self.assertNotIn(
            resume_button_html,
            dashboard_html
        )

    def test_resume_course_appears_on_dashboard(self):
        """
        When a course has completion data, its course card should display a
        "Resume Course" button.
        """
        self.override_waffle_switch(True)

        course = CourseFactory.create()
        CourseEnrollmentFactory.create(
            user=self.user,
            course_id=course.id
        )

        course_key = course.id
        block_keys = [
            ItemFactory.create(
                category='video',
                parent_location=course.location,
                display_name='Video {0}'.format(six.text_type(number))
            ).location
            for number in range(5)
        ]

        submit_completions_for_testing(self.user, block_keys)

        response = self.client.get(reverse('dashboard'))

        course_key_string = str(course_key)
        resume_block_key_string = str(block_keys[-1])
        course_run_string = self._pull_course_run_from_course_key(course_key_string)

        view_button_html = self._get_html_for_view_course_button(
            course_key_string,
            course_run_string
        )
        resume_button_html = self._get_html_for_resume_course_button(
            course_key_string,
            resume_block_key_string,
            course_run_string
        )

        view_button_html = self._remove_whitespace_from_html_string(view_button_html)
        resume_button_html = self._remove_whitespace_from_html_string(resume_button_html)
        dashboard_html = self._remove_whitespace_from_response(response)

        self.assertIn(
            resume_button_html,
            dashboard_html
        )
        self.assertNotIn(
            view_button_html,
            dashboard_html
        )

    @override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
    def test_content_gating_course_card_changes(self):
        """
        When a course is expired, the links on the course card should be removed.
        Links will be removed from the course title, course image and button (View Course/Resume Course).
        The course card should have an access expired message.
        """
        CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=self.THREE_YEARS_AGO - timedelta(days=30))
        self.override_waffle_switch(True)

        course = CourseFactory.create(start=self.THREE_YEARS_AGO)
        add_course_mode(course, mode_slug=CourseMode.AUDIT)
        add_course_mode(course)
        enrollment = CourseEnrollmentFactory.create(
            user=self.user,
            course_id=course.id
        )
        enrollment.created = self.THREE_YEARS_AGO + timedelta(days=1)
        enrollment.save()

        # pylint: disable=unused-variable
        schedule = ScheduleFactory(enrollment=enrollment)

        response = self.client.get(reverse('dashboard'))
        dashboard_html = self._remove_whitespace_from_response(response)
        access_expired_substring = 'Accessexpired'
        course_link_class = 'course-target-link'

        self.assertNotIn(
            course_link_class,
            dashboard_html
        )

        self.assertIn(
            access_expired_substring,
            dashboard_html
        )

    def test_dashboard_with_resume_buttons_and_view_buttons(self):
        '''
        The Test creates a four-course-card dashboard. The user completes course
        blocks in the even-numbered course cards. The test checks that courses
        with completion data have course cards with "Resume Course" buttons;
        those without have "View Course" buttons.

        '''
        self.override_waffle_switch(True)

        isEven = lambda n: n % 2 == 0

        num_course_cards = 4

        html_for_view_buttons = []
        html_for_resume_buttons = []
        html_for_entitlement = []

        for i in range(num_course_cards):

            course = CourseFactory.create()
            course_enrollment = CourseEnrollmentFactory(
                user=self.user,
                course_id=course.id
            )

            course_key = course_enrollment.course_id
            course_key_string = str(course_key)

            if i == 1:
                CourseEntitlementFactory.create(user=self.user, enrollment_course_run=course_enrollment)

            else:
                last_completed_block_string = ''
                course_run_string = self._pull_course_run_from_course_key(
                    course_key_string)

            # Submit completed course blocks in even-numbered courses.
            if isEven(i):
                block_keys = [
                    ItemFactory.create(
                        category='video',
                        parent_location=course.location,
                        display_name='Video {0}'.format(six.text_type(number))
                    ).location
                    for number in range(5)
                ]
                last_completed_block_string = str(block_keys[-1])

                submit_completions_for_testing(self.user, block_keys)

            html_for_view_buttons.append(
                self._get_html_for_view_course_button(
                    course_key_string,
                    course_run_string
                )
            )
            html_for_resume_buttons.append(
                self._get_html_for_resume_course_button(
                    course_key_string,
                    last_completed_block_string,
                    course_run_string
                )
            )
            html_for_entitlement.append(
                self._get_html_for_entitlement_button(
                    course_key_string
                )
            )

        response = self.client.get(reverse('dashboard'))

        html_for_view_buttons = [
            self._remove_whitespace_from_html_string(button)
            for button in html_for_view_buttons
        ]
        html_for_resume_buttons = [
            self._remove_whitespace_from_html_string(button)
            for button in html_for_resume_buttons
        ]
        html_for_entitlement = [
            self._remove_whitespace_from_html_string(button)
            for button in html_for_entitlement
        ]

        dashboard_html = self._remove_whitespace_from_response(response)

        for i in range(num_course_cards):
            expected_button = None
            unexpected_button = None

            if i == 1:
                expected_button = html_for_entitlement[i]
                unexpected_button = html_for_view_buttons[i] + html_for_resume_buttons[i]

            elif isEven(i):
                expected_button = html_for_resume_buttons[i]
                unexpected_button = html_for_view_buttons[i] + html_for_entitlement[i]
            else:
                expected_button = html_for_view_buttons[i]
                unexpected_button = html_for_resume_buttons[i] + html_for_entitlement[i]

            self.assertIn(
                expected_button,
                dashboard_html
            )
            self.assertNotIn(
                unexpected_button,
                dashboard_html
            )
Ejemplo n.º 14
0
class TestCreateJWTs(AccessTokenMixin, TestCase):
    """ Tests for oauth_dispatch's jwt creation functionality. """
    def setUp(self):
        super(TestCreateJWTs, self).setUp()
        self.user = UserFactory()
        self.default_scopes = ['email', 'profile']

    def _create_client(self, oauth_adapter, client_restricted):
        """
        Creates and returns an OAuth client using the given oauth_adapter.
        Configures the client as a RestrictedApplication if client_restricted is
        True.
        """
        client = oauth_adapter.create_public_client(
            name='public app',
            user=self.user,
            redirect_uri='',
            client_id='public-client-id',
        )
        if client_restricted:
            RestrictedApplication.objects.create(application=client)
        return client

    def _create_jwt_for_token(
        self,
        oauth_adapter,
        use_asymmetric_key,
        client_restricted=False,
    ):
        """ Creates and returns the jwt returned by jwt_api.create_jwt_from_token. """
        client = self._create_client(oauth_adapter, client_restricted)
        expires_in = 60 * 60
        expires = now() + timedelta(seconds=expires_in)
        token_dict = dict(
            access_token=oauth_adapter.create_access_token_for_test(
                'token', client, self.user, expires),
            expires_in=expires_in,
            scope=' '.join(self.default_scopes))
        return jwt_api.create_jwt_from_token(
            token_dict, oauth_adapter, use_asymmetric_key=use_asymmetric_key)

    def _assert_jwt_is_valid(self, jwt_token, should_be_asymmetric_key):
        """ Asserts the given jwt_token is valid and meets expectations. """
        self.assert_valid_jwt_access_token(
            jwt_token,
            self.user,
            self.default_scopes,
            should_be_asymmetric_key=should_be_asymmetric_key,
        )

    def test_create_jwt_for_token(self):
        oauth_adapter = DOTAdapter()
        jwt_token = self._create_jwt_for_token(oauth_adapter,
                                               use_asymmetric_key=False)
        self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=False)

    def test_dot_create_jwt_for_token_with_asymmetric(self):
        jwt_token = self._create_jwt_for_token(DOTAdapter(),
                                               use_asymmetric_key=True)
        self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=True)

    @ddt.data((True, False))
    def test_dot_create_jwt_for_token(self, client_restricted):
        jwt_token = self._create_jwt_for_token(
            DOTAdapter(),
            use_asymmetric_key=None,
            client_restricted=client_restricted,
        )
        self._assert_jwt_is_valid(jwt_token,
                                  should_be_asymmetric_key=client_restricted)

    @patch(
        'openedx.core.djangoapps.oauth_dispatch.jwt.create_role_auth_claim_for_user'
    )
    @ddt.data(True, False)
    def test_create_jwt_for_user(self, user_email_verified, mock_create_roles):
        mock_create_roles.return_value = ['superuser', 'enterprise-admin']
        self.user.is_active = user_email_verified
        self.user.save()

        aud = 'test_aud'
        secret = 'test_secret'
        additional_claims = {'claim1_key': 'claim1_val'}
        jwt_token = jwt_api.create_jwt_for_user(
            self.user,
            secret=secret,
            aud=aud,
            additional_claims=additional_claims)
        token_payload = self.assert_valid_jwt_access_token(
            jwt_token,
            self.user,
            self.default_scopes,
            aud=aud,
            secret=secret,
        )
        self.assertDictContainsSubset(additional_claims, token_payload)
        self.assertEqual(user_email_verified, token_payload['email_verified'])
        self.assertEqual(token_payload['roles'],
                         mock_create_roles.return_value)

    def test_scopes(self):
        """
        Ensure the requested scopes are used.
        """
        scopes = [
            'user_id',
        ]
        aud = 'test_aud'
        secret = 'test_secret'

        jwt = jwt_api.create_jwt_for_user(self.user, secret=secret, aud=aud)
        jwt_scopes = jwt_api.create_jwt_for_user(self.user,
                                                 secret=secret,
                                                 aud=aud,
                                                 scopes=scopes)

        jwt_payload = self.assert_valid_jwt_access_token(
            jwt,
            self.user,
            self.default_scopes,
            aud=aud,
            secret=secret,
        )
        jwt_scopes_payload = self.assert_valid_jwt_access_token(
            jwt_scopes,
            self.user,
            scopes,
            aud=aud,
            secret=secret,
        )
        self.assertEqual(jwt_payload['scopes'], self.default_scopes)
        self.assertEqual(jwt_scopes_payload['scopes'], scopes)
        self.assertEqual(jwt_scopes_payload['user_id'], self.user.id)
class TestRecentEnrollments(ModuleStoreTestCase, XssTestMixin):
    """
    Unit tests for getting the list of courses for a logged in user
    """
    PASSWORD = '******'

    def setUp(self):
        """
        Add a student
        """
        super(TestRecentEnrollments, self).setUp()  # lint-amnesty, pylint: disable=super-with-arguments
        self.student = UserFactory()
        self.student.set_password(self.PASSWORD)
        self.student.save()

        # Old Course
        old_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0')
        __, enrollment = self._create_course_and_enrollment(old_course_location)
        enrollment.created = datetime.datetime(1900, 12, 31, 0, 0, 0, 0, tzinfo=UTC)
        enrollment.save()

        # New Course
        course_location = locator.CourseLocator('Org1', 'Course1', 'Run1')
        self.course, self.enrollment = self._create_course_and_enrollment(course_location)

    def _create_course_and_enrollment(self, course_location):
        """ Creates a course and associated enrollment. """
        course = CourseFactory.create(
            org=course_location.org,
            number=course_location.course,
            run=course_location.run
        )
        enrollment = CourseEnrollment.enroll(self.student, course.id)
        return course, enrollment

    def _configure_message_timeout(self, timeout):
        """Configure the amount of time the enrollment message will be displayed. """
        config = DashboardConfiguration(recent_enrollment_time_delta=timeout)
        config.save()

    def test_recently_enrolled_courses(self):
        """
        Test if the function for filtering recent enrollments works appropriately.
        """
        self._configure_message_timeout(60)

        # get courses through iterating all courses
        courses_list = list(get_course_enrollments(self.student, None, []))
        self.assertEqual(len(courses_list), 2)

        recent_course_list = _get_recently_enrolled_courses(courses_list)
        self.assertEqual(len(recent_course_list), 1)

    def test_zero_second_delta(self):
        """
        Tests that the recent enrollment list is empty if configured to zero seconds.
        """
        self._configure_message_timeout(0)
        courses_list = list(get_course_enrollments(self.student, None, []))
        self.assertEqual(len(courses_list), 2)

        recent_course_list = _get_recently_enrolled_courses(courses_list)
        self.assertEqual(len(recent_course_list), 0)

    def test_enrollments_sorted_most_recent(self):
        """
        Test that the list of newly created courses are properly sorted to show the most
        recent enrollments first.
        Also test recent enrollment message rendered appropriately for more than two courses.
        """
        self._configure_message_timeout(600)

        # Create a number of new enrollments and courses, and force their creation behind
        # the first enrollment
        courses = []
        for idx, seconds_past in zip(list(range(2, 6)), [5, 10, 15, 20]):
            course_location = locator.CourseLocator(
                'Org{num}'.format(num=idx),
                'Course{num}'.format(num=idx),
                'Run{num}'.format(num=idx)
            )
            course, enrollment = self._create_course_and_enrollment(course_location)
            enrollment.created = now() - datetime.timedelta(seconds=seconds_past)
            enrollment.save()
            courses.append(course)

        courses_list = list(get_course_enrollments(self.student, None, []))
        self.assertEqual(len(courses_list), 6)

        recent_course_list = _get_recently_enrolled_courses(courses_list)
        self.assertEqual(len(recent_course_list), 5)

        self.assertEqual(recent_course_list[1].course.id, courses[0].id)
        self.assertEqual(recent_course_list[2].course.id, courses[1].id)
        self.assertEqual(recent_course_list[3].course.id, courses[2].id)
        self.assertEqual(recent_course_list[4].course.id, courses[3].id)

        self.client.login(username=self.student.username, password=self.PASSWORD)
        response = self.client.get(reverse("dashboard"))

        # verify recent enrollment message
        self.assertContains(
            response,
            'Thank you for enrolling in:'.format(course_name=self.course.display_name)
        )
        self.assertContains(
            response,
            ', '.join(enrollment.course.display_name for enrollment in recent_course_list)
        )

    def test_dashboard_rendering_with_single_course(self):
        """
        Tests that the dashboard renders the recent enrollment message appropriately for single course.
        """
        self._configure_message_timeout(600)
        self.client.login(username=self.student.username, password=self.PASSWORD)
        response = self.client.get(reverse("dashboard"))
        self.assertContains(
            response,
            "Thank you for enrolling in {course_name}".format(course_name=self.course.display_name)
        )

    def test_dashboard_rendering_with_two_courses(self):
        """
        Tests that the dashboard renders the recent enrollment message appropriately for two courses.
        """
        self._configure_message_timeout(600)
        course_location = locator.CourseLocator(
            'Org2',
            'Course2',
            'Run2'
        )
        course, _ = self._create_course_and_enrollment(course_location)  # lint-amnesty, pylint: disable=unused-variable

        self.client.login(username=self.student.username, password=self.PASSWORD)
        response = self.client.get(reverse("dashboard"))

        courses_enrollments = list(get_course_enrollments(self.student, None, []))
        courses_enrollments.sort(key=lambda x: x.created, reverse=True)
        self.assertEqual(len(courses_enrollments), 3)

        recent_course_enrollments = _get_recently_enrolled_courses(courses_enrollments)
        self.assertEqual(len(recent_course_enrollments), 2)

        self.assertContains(
            response,
            "Thank you for enrolling in:".format(course_name=self.course.display_name)
        )
        self.assertContains(
            response,
            ' and '.join(enrollment.course.display_name for enrollment in recent_course_enrollments)
        )

    def test_dashboard_escaped_rendering(self):
        """
        Tests that the dashboard renders the escaped recent enrollment messages appropriately.
        """
        self._configure_message_timeout(600)
        self.client.login(username=self.student.username, password=self.PASSWORD)

        # New Course
        course_location = locator.CourseLocator('TestOrg', 'TestCourse', 'TestRun')
        xss_content = "<script>alert('XSS')</script>"
        course = CourseFactory.create(
            org=course_location.org,
            number=course_location.course,
            run=course_location.run,
            display_name=xss_content
        )
        CourseEnrollment.enroll(self.student, course.id)

        response = self.client.get(reverse("dashboard"))
        self.assertContains(response, "Thank you for enrolling in")

        # Check if response is escaped
        self.assert_no_xss(response, xss_content)