コード例 #1
0
    def test_format_and_parse(self):
        user = UserFactory()
        body = 'This is a test body.'
        course_run_key = 'course-v1:testX+TestX+Test'

        with mock.patch(self.salesforce_path):
            util = SalesforceUtil(self.salesforce_config.partner)
            formatted_message = util.format_user_comment_body(
                user, body, course_run_key)
            expected_formatted_message = '[User]\n{}\n\n[Course Run]\n{}\n\n[Body]\n{}'.format(
                '{} {} ({})'.format(user.first_name, user.last_name,
                                    user.username), course_run_key, body)
            assert formatted_message == expected_formatted_message
            parsed_message = util._parse_user_comment_body(  # pylint: disable=protected-access
                {'Body': formatted_message})
            parsed_user = parsed_message.get('user')
            assert parsed_user.get('username') == user.username
            # Below 3 will always be None for a matched comment
            assert parsed_user.get('email') is None
            assert parsed_user.get('first_name') is None
            assert parsed_user.get('last_name') is None

            assert parsed_message.get('course_run_key') == course_run_key
            assert parsed_message.get('comment') == body
            user.first_name = ''
            user.last_name = ''
            user.save()

            formatted_message = util.format_user_comment_body(
                user, body, course_run_key)
            expected_formatted_message = '[User]\n{}\n\n[Course Run]\n{}\n\n[Body]\n{}'.format(
                '{}'.format(user.username), course_run_key, body)
            assert formatted_message == expected_formatted_message
コード例 #2
0
class RateLimitingTest(SiteMixin, APITestCase):
    """
    Testing rate limiting of API calls.
    """
    def setUp(self):
        super(RateLimitingTest, self).setUp()

        self.url = reverse('api_docs')
        self.user = UserFactory()
        self.client.login(username=self.user.username, password=USER_PASSWORD)

    def tearDown(self):
        """
        Clear the cache, since DRF uses it for recording requests against a
        URL. Django does not clear the cache between test runs.
        """
        super(RateLimitingTest, self).tearDown()
        cache.clear()

    def _make_requests(self):
        """ Make multiple requests until the throttle's limit is exceeded.

        Returns
            Response: Response of the last request.
        """
        num_requests = OverridableUserRateThrottle().num_requests
        for __ in range(num_requests + 1):
            response = self.client.get(self.url)
        return response

    def test_rate_limiting(self):
        """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """
        response = self._make_requests()

        assert response.status_code == 429

    def test_user_throttle_rate(self):
        """ Verify the UserThrottleRate can be used to override the default rate limit. """
        UserThrottleRate.objects.create(user=self.user, rate='1000/day')
        self.assert_rate_limit_successfully_exceeded()

    def assert_rate_limit_successfully_exceeded(self):
        """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """
        response = self._make_requests()

        assert response.status_code == 200

    def test_superuser_throttling(self):
        """ Verify superusers are not throttled. """
        self.user.is_superuser = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()

    def test_staff_throttling(self):
        """ Verify staff users are not throttled. """
        self.user.is_staff = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()
コード例 #3
0
class RateLimitingTest(APITestCase):
    """
    Testing rate limiting of API calls.
    """

    def setUp(self):
        super(RateLimitingTest, self).setUp()
        self.url = reverse('django.swagger.resources.view')
        self.user = UserFactory()
        self.client.login(username=self.user.username, password=USER_PASSWORD)

    def tearDown(self):
        """
        Clear the cache, since DRF uses it for recording requests against a
        URL. Django does not clear the cache between test runs.
        """
        super(RateLimitingTest, self).tearDown()
        cache.clear()

    def _make_requests(self):
        """ Make multiple requests until the throttle's limit is exceeded.

        Returns
            Response: Response of the last request.
        """
        num_requests = OverridableUserRateThrottle().num_requests
        for __ in range(num_requests + 1):
            response = self.client.get(self.url)
        return response

    def test_rate_limiting(self):
        """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """
        response = self._make_requests()
        self.assertEqual(response.status_code, 429)

    def test_user_throttle_rate(self):
        """ Verify the UserThrottleRate can be used to override the default rate limit. """
        UserThrottleRate.objects.create(user=self.user, rate='1000/day')
        self.assert_rate_limit_successfully_exceeded()

    def assert_rate_limit_successfully_exceeded(self):
        """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """
        response = self._make_requests()
        self.assertEqual(response.status_code, 200)

    def test_superuser_throttling(self):
        """ Verify superusers are not throttled. """
        self.user.is_superuser = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()

    def test_staff_throttling(self):
        """ Verify staff users are not throttled. """
        self.user.is_staff = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()
コード例 #4
0
class UserAutocompleteTests(SiteMixin, TestCase):
    """ Tests for user autocomplete lookups."""
    def setUp(self):
        super(UserAutocompleteTests, self).setUp()
        self.user = UserFactory(username='******', is_staff=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.users_list = UserFactory.create_batch(5)

    def test_user_autocomplete(self):
        """ Verify user autocomplete returns the data. """
        response = self.client.get(
            reverse('admin_core:user-autocomplete') +
            '?q={user}'.format(user='******'))
        self._assert_response(response, 5)

        # update first user's username
        self.users_list[0].username = '******'
        self.users_list[0].save()
        response = self.client.get(
            reverse('admin_core:user-autocomplete') +
            '?q={user}'.format(user='******'))
        self._assert_response(response, 1)

    def test_course_autocomplete_un_authorize_user(self):
        """ Verify user autocomplete returns empty list for un-authorized users. """
        self.client.logout()
        self.user.is_staff = False
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        response = self.client.get(reverse('admin_core:user-autocomplete'))
        self._assert_response(response, 0)

    def _assert_response(self, response, expected_length):
        """ Assert autocomplete response. """
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(len(data['results']), expected_length)
コード例 #5
0
class CourseRunViewSetTests(SerializationMixin, ElasticsearchTestMixin,
                            OAuth2Mixin, APITestCase):
    def setUp(self):
        super(CourseRunViewSetTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.force_authenticate(self.user)
        self.course_run = CourseRunFactory(course__partner=self.partner)
        self.course_run.course.authoring_organizations.add(
            OrganizationFactory(key='course-id'))
        self.course_run_2 = CourseRunFactory(course__key='Test+Course',
                                             course__partner=self.partner)
        self.refresh_index()
        self.request = APIRequestFactory().get('/')
        self.request.user = self.user

    def mock_patch_to_studio(self, key, access_token=True, status=200):
        if access_token:
            self.mock_access_token()
        studio_url = '{root}/api/v1/course_runs/{key}/'.format(
            root=self.partner.studio_url.strip('/'), key=key)
        responses.add(responses.PATCH, studio_url, status=status)
        responses.add(responses.POST,
                      '{url}images/'.format(url=studio_url),
                      status=status)

    def mock_post_to_studio(self, key, access_token=True):
        if access_token:
            self.mock_access_token()
        studio_url = '{root}/api/v1/course_runs/'.format(
            root=self.partner.studio_url.strip('/'))
        responses.add(responses.POST, studio_url, status=200)
        responses.add(responses.POST,
                      '{url}{key}/images/'.format(url=studio_url, key=key),
                      status=200)

    def test_get(self):
        """ Verify the endpoint returns the details for a single course. """
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(11):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertEqual(response.data,
                         self.serialize_course_run(self.course_run))

    def test_get_exclude_deleted_programs(self):
        """ Verify the endpoint returns no associated deleted programs """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Deleted)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(12):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data.get('programs') == []

    def test_get_include_deleted_programs(self):
        """
        Verify the endpoint returns associated deleted programs
        with the 'include_deleted_programs' flag set to True
        """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Deleted)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        url += '?include_deleted_programs=1'

        with self.assertNumQueries(14):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data == \
            self.serialize_course_run(self.course_run, extra_context={'include_deleted_programs': True})

    def test_get_exclude_unpublished_programs(self):
        """ Verify the endpoint returns no associated unpublished programs """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Unpublished)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(12):
            response = self.client.get(url)
            assert response.status_code == 200
            assert response.data.get('programs') == []

    def test_get_include_unpublished_programs(self):
        """
        Verify the endpoint returns associated unpublished programs
        with the 'include_unpublished_programs' flag set to True
        """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Unpublished)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        url += '?include_unpublished_programs=1'

        with self.assertNumQueries(14):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data == \
            self.serialize_course_run(self.course_run, extra_context={'include_unpublished_programs': True})

    @responses.activate
    def test_create_minimum(self):
        """ Verify the endpoint supports creating a course_run with the least info. """
        course = self.course_run.course
        new_key = 'course-v1:{}+1T2000'.format(course.key.replace('/', '+'))
        self.mock_post_to_studio(new_key)
        url = reverse('api:v1:course_run-list')

        # Send nothing - expect complaints
        response = self.client.post(url, {}, format='json')
        self.assertEqual(response.status_code, 400)
        self.assertDictEqual(
            response.data, {
                'course': ['This field is required.'],
                'start': ['This field is required.'],
                'end': ['This field is required.'],
            })

        # Send minimum requested
        response = self.client.post(url, {
            'course': course.key,
            'start': '2000-01-01T00:00:00Z',
            'end': '2001-01-01T00:00:00Z',
        },
                                    format='json')
        self.assertEqual(response.status_code, 201)
        new_course_run = CourseRun.objects.get(key=new_key)
        self.assertDictEqual(response.data,
                             self.serialize_course_run(new_course_run))
        self.assertEqual(new_course_run.pacing_type,
                         'instructor_paced')  # default we provide
        self.assertEqual(
            str(new_course_run.end),
            '2001-01-01 00:00:00+00:00')  # spot check that input made it

    @responses.activate
    def test_create_with_key(self):
        """ Verify the endpoint supports creating a course_run when specifying a key (if allowed). """
        course = self.course_run.course
        date_key = 'course-v1:{}+1T2000'.format(course.key.replace('/', '+'))
        desired_key = 'course-v1:{}+HowdyDoing'.format(
            course.key.replace('/', '+'))
        url = reverse('api:v1:course_run-list')

        data = {
            'course': course.key,
            'start': '2000-01-01T00:00:00Z',
            'end': '2001-01-01T00:00:00Z',
            'key': desired_key,
        }

        # If org doesn't specifically allow it, incoming key is ignored
        self.mock_post_to_studio(date_key)
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, 201)
        new_course_run = CourseRun.objects.get(key=date_key)
        self.assertDictEqual(response.data,
                             self.serialize_course_run(new_course_run))

        # Turn on this feature for this org, notice that we can now specify the course key we want
        org_ext = OrganizationExtensionFactory(
            organization=course.authoring_organizations.first())
        org_ext.auto_create_in_studio = False  # badly named, but this controls whether we let org name their keys
        org_ext.save()
        self.mock_post_to_studio(desired_key, access_token=False)
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, 201)
        new_course_run = CourseRun.objects.get(key=desired_key)
        self.assertDictEqual(response.data,
                             self.serialize_course_run(new_course_run))

    def test_create_if_in_org(self):
        """ Verify the endpoint supports creating a course_run with organization permissions. """
        url = reverse('api:v1:course_run-list')
        course = self.course_run.course
        data = {'course': course.key}

        self.user.is_staff = False
        self.user.save()

        # Not in org, not allowed to POST
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, 403)

        # Add to org
        org_ext = OrganizationExtensionFactory(
            organization=course.authoring_organizations.first())
        self.user.groups.add(org_ext.group)

        # now allowed to POST
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code,
                         400)  # missing start, but at least we got that far

    @responses.activate
    def test_partial_update(self):
        """ Verify the endpoint supports partially updating a course_run's fields, provided user has permission. """
        self.mock_patch_to_studio(self.course_run.key)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        expected_min_effort = 867
        expected_max_effort = 5309
        data = {
            'max_effort': expected_max_effort,
            'min_effort': expected_min_effort,
        }

        # Update this course_run with the new info
        response = self.client.patch(url, data, format='json')
        assert response.status_code == 200

        # refresh and make sure we have the new effort levels
        self.course_run.refresh_from_db()

        assert self.course_run.max_effort == expected_max_effort
        assert self.course_run.min_effort == expected_min_effort

    def test_partial_update_no_studio_url(self):
        """ Verify we skip pushing when no studio url is set. """
        self.partner.studio_url = None
        self.partner.save()

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with mock.patch(
                'course_discovery.apps.api.v1.views.course_runs.log.info'
        ) as mock_logger:
            response = self.client.patch(url, {}, format='json')

        self.assertEqual(response.status_code, 200)
        mock_logger.assert_called_with(
            'Not pushing course run info for %s to Studio as partner %s has no studio_url set.',
            self.course_run.key,
            self.partner.short_code,
        )

    def test_partial_update_bad_permission(self):
        """ Verify partially updating will fail if user doesn't have permission. """
        user = UserFactory(is_staff=False, is_superuser=False)
        self.client.force_authenticate(user)
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        response = self.client.patch(url, {}, format='json')
        assert response.status_code == 403

    @ddt.data(
        (
            {
                'start': '2010-01-01T00:00:00Z',
                'end': '2000-01-01T00:00:00Z'
            },
            'Start date cannot be after the End date',
        ),
        (
            {
                'key': 'course-v1:Blarg+Hello+Run'
            },
            'Key cannot be changed',
        ),
        (
            {
                'course': 'Test+Course'
            },
            'Course cannot be changed',
        ),
        (
            {
                'min_effort': 10000
            },
            'Minimum effort cannot be greater than Maximum effort',
        ),
        (
            {
                'min_effort': 10000,
                'max_effort': 10000
            },
            'Minimum effort and Maximum effort cannot be the same',
        ),
        (
            {
                'max_effort': None
            },
            'Maximum effort cannot be empty',
        ),
    )
    @ddt.unpack
    def test_partial_update_common_errors(self, data, error):
        """ Verify partially updating will fail depending on various validation checks. """
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(url, data, format='json')
        self.assertContains(response, error, status_code=400)

    def test_partial_update_staff(self):
        """ Verify partially updating allows staff updates. """
        self.mock_patch_to_studio(self.course_run.key)

        p1 = PersonFactory()
        p2 = PersonFactory()
        PersonFactory()

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(url, {'staff': [p2.uuid, p1.uuid]},
                                     format='json')
        self.assertEqual(response.status_code, 200)

        self.course_run.refresh_from_db()
        self.assertListEqual(list(self.course_run.staff.all()), [p2, p1])

    @responses.activate
    def test_partial_update_video(self):
        """ Verify partially updating allows video updates. """
        self.mock_patch_to_studio(self.course_run.key)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(
            url, {'video': {
                'src': 'https://example.com/blarg'
            }},
            format='json')
        self.assertEqual(response.status_code, 200)

        self.course_run.refresh_from_db()
        self.assertEqual(self.course_run.video.src,
                         'https://example.com/blarg')

    @responses.activate
    def test_update_if_editor(self):
        """ Verify the endpoint supports updating a course_run with editor permissions. """
        self.mock_patch_to_studio(self.course_run.key)
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        self.user.is_staff = False
        self.user.save()

        # Not an editor, not allowed to patch
        response = self.client.patch(url, {}, format='json')
        self.assertEqual(response.status_code, 403)

        # Add as editor
        org_ext = OrganizationExtensionFactory(
            organization=self.course_run.course.authoring_organizations.first(
            ))
        self.user.groups.add(org_ext.group)
        CourseEditorFactory(user=self.user, course=self.course_run.course)

        # now allowed to patch
        response = self.client.patch(url, {}, format='json')
        self.assertEqual(response.status_code, 200)

    @responses.activate
    def test_studio_update_failure(self):
        """ Verify we bubble up error correctly if studio is giving us static. """
        self.mock_patch_to_studio(self.course_run.key, status=400)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(url, {'title': 'New Title'},
                                     format='json')
        self.assertContains(response,
                            'Failed to set course run data: Client Error 400',
                            status_code=400)

        self.course_run.refresh_from_db()
        self.assertEqual(self.course_run.title_override,
                         None)  # prove we didn't touch the course run object

    @responses.activate
    def test_full_update(self):
        """ Verify full updating is allowed. """
        self.mock_patch_to_studio(self.course_run.key)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.put(
            url,
            {
                'course':
                self.course_run.course.key,  # required, so we need for a put
                'start':
                self.course_run.start,  # required, so we need for a put
                'end': self.course_run.end,  # required, so we need for a put
                'title': 'New Title',
            },
            format='json')
        self.assertEqual(response.status_code, 200)

        self.course_run.refresh_from_db()
        self.assertEqual(self.course_run.title_override, 'New Title')

    @ddt.data(
        CourseRunStatus.LegalReview,
        CourseRunStatus.InternalReview,
    )
    def test_patch_put_restrict_when_reviewing(self, status):
        self.course_run.status = status
        self.course_run.save()
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.put(
            url,
            {
                'course':
                self.course_run.course.key,  # required, so we need for a put
                'start':
                self.course_run.start,  # required, so we need for a put
                'end': self.course_run.end,  # required, so we need for a put
            },
            format='json')
        assert response.status_code == 403

        response = self.client.patch(url, {}, format='json')
        assert response.status_code == 403

    @responses.activate
    def test_patch_put_reset_status(self):
        self.mock_patch_to_studio(self.course_run.key)
        self.course_run.status = CourseRunStatus.Reviewed
        self.course_run.save()
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.put(
            url,
            {
                'course':
                self.course_run.course.key,  # required, so we need for a put
                'start':
                self.course_run.start,  # required, so we need for a put
                'end': self.course_run.end,  # required, so we need for a put
                'status': 'reviewed',
            },
            format='json')
        assert response.status_code == 200
        self.course_run.refresh_from_db()
        assert self.course_run.status == CourseRunStatus.Unpublished

    def test_list(self):
        """ Verify the endpoint returns a list of all course runs. """
        url = reverse('api:v1:course_run-list')

        with self.assertNumQueries(13):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(CourseRun.objects.all().order_by(
                Lower('key')),
                                      many=True))

    def test_list_sorted_by_course_start_date(self):
        """ Verify the endpoint returns a list of all course runs sorted by start date. """
        url = '{root}?ordering=start'.format(
            root=reverse('api:v1:course_run-list'))

        with self.assertNumQueries(13):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(
                CourseRun.objects.all().order_by('start'), many=True))

    def test_list_query(self):
        """ Verify the endpoint returns a filtered list of courses """
        course_runs = CourseRunFactory.create_batch(
            3, title='Some random title', course__partner=self.partner)
        CourseRunFactory(title='non-matching name')
        query = 'title:Some random title'
        url = '{root}?q={query}'.format(root=reverse('api:v1:course_run-list'),
                                        query=query)

        with self.assertNumQueries(39):
            response = self.client.get(url)

        actual_sorted = sorted(response.data['results'],
                               key=lambda course_run: course_run['key'])
        expected_sorted = sorted(self.serialize_course_run(course_runs,
                                                           many=True),
                                 key=lambda course_run: course_run['key'])
        self.assertListEqual(actual_sorted, expected_sorted)

    def assert_list_results(self, url, expected, extra_context=None):
        expected = sorted(expected,
                          key=lambda course_run: course_run.key.lower())
        response = self.client.get(url)
        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(expected,
                                      many=True,
                                      extra_context=extra_context))

    def test_filter_by_keys(self):
        """ Verify the endpoint returns a list of course runs filtered by the specified keys. """
        CourseRun.objects.all().delete()
        expected = CourseRunFactory.create_batch(3,
                                                 course__partner=self.partner)
        keys = ','.join([course.key for course in expected])
        url = '{root}?keys={keys}'.format(
            root=reverse('api:v1:course_run-list'), keys=keys)
        self.assert_list_results(url, expected)

    def test_filter_by_marketable(self):
        """ Verify the endpoint filters course runs to those that are marketable. """
        CourseRun.objects.all().delete()
        expected = CourseRunFactory.create_batch(3,
                                                 course__partner=self.partner)
        for course_run in expected:
            SeatFactory(course_run=course_run)

        CourseRunFactory.create_batch(3,
                                      slug=None,
                                      course__partner=self.partner)
        CourseRunFactory.create_batch(3, slug='', course__partner=self.partner)

        url = reverse('api:v1:course_run-list') + '?marketable=1'
        self.assert_list_results(url, expected)

    def test_filter_by_hidden(self):
        """ Verify the endpoint filters course runs that are hidden. """
        CourseRun.objects.all().delete()
        course_runs = CourseRunFactory.create_batch(
            3, course__partner=self.partner)
        hidden_course_runs = CourseRunFactory.create_batch(
            3, hidden=True, course__partner=self.partner)
        url = reverse('api:v1:course_run-list')
        self.assert_list_results(url, course_runs + hidden_course_runs)
        url = reverse('api:v1:course_run-list') + '?hidden=False'
        self.assert_list_results(url, course_runs)

    def test_filter_by_active(self):
        """ Verify the endpoint filters course runs to those that are active. """
        CourseRun.objects.all().delete()

        # Create course with end date in future and enrollment_end in past.
        end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
        enrollment_end = datetime.datetime.now(
            pytz.UTC) - datetime.timedelta(days=1)
        CourseRunFactory(end=end,
                         enrollment_end=enrollment_end,
                         course__partner=self.partner)

        # Create course with end date in past and no enrollment_end.
        end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2)
        CourseRunFactory(end=end,
                         enrollment_end=None,
                         course__partner=self.partner)

        # Create course with end date in future and enrollment_end in future.
        end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
        enrollment_end = datetime.datetime.now(
            pytz.UTC) + datetime.timedelta(days=1)
        active_enrollment_end = CourseRunFactory(end=end,
                                                 enrollment_end=enrollment_end,
                                                 course__partner=self.partner)

        # Create course with end date in future and no enrollment_end.
        active_no_enrollment_end = CourseRunFactory(
            end=end, enrollment_end=None, course__partner=self.partner)

        expected = [active_enrollment_end, active_no_enrollment_end]
        url = reverse('api:v1:course_run-list') + '?active=1'
        self.assert_list_results(url, expected)

    def test_filter_by_license(self):
        CourseRun.objects.all().delete()
        course_runs_cc = CourseRunFactory.create_batch(
            3, course__partner=self.partner, license='cc-by-sa')
        CourseRunFactory.create_batch(3,
                                      course__partner=self.partner,
                                      license='')

        url = reverse('api:v1:course_run-list') + '?license=cc-by-sa'
        self.assert_list_results(url, course_runs_cc)

    def test_list_exclude_utm(self):
        """ Verify the endpoint returns marketing URLs without UTM parameters. """
        url = reverse('api:v1:course_run-list') + '?exclude_utm=1'
        self.assert_list_results(url,
                                 CourseRun.objects.all(),
                                 extra_context={'exclude_utm': 1})

    def test_contains_single_course_run(self):
        """ Verify that a single course_run is contained in a query """
        qs = urllib.parse.urlencode({
            'query': 'id:course*',
            'course_run_ids': self.course_run.key,
        })
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)
        response = self.client.get(url)
        assert response.status_code == 200
        self.assertEqual(response.data,
                         {'course_runs': {
                             self.course_run.key: True
                         }})

    def test_contains_multiple_course_runs(self):
        qs = urllib.parse.urlencode({
            'query':
            'id:course*',
            'course_run_ids':
            '{},{},{}'.format(self.course_run.key, self.course_run_2.key,
                              'abc')
        })
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)

        response = self.client.get(url)
        assert response.status_code == 200
        self.assertDictEqual(
            response.data, {
                'course_runs': {
                    self.course_run.key: True,
                    self.course_run_2.key: True,
                    'abc': False
                }
            })

    @ddt.data({'params': {
        'course_run_ids': 'a/b/c'
    }}, {'params': {
        'query': 'id:course*'
    }}, {'params': {}})
    @ddt.unpack
    def test_contains_missing_parameter(self, params):
        qs = urllib.parse.urlencode(params)
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)

        response = self.client.get(url)
        assert response.status_code == 400

    def test_options(self):
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.options(url)
        self.assertEqual(response.status_code, 200)

        data = response.data['actions']['PUT']
        self.assertEqual(data['level_type']['choices'],
                         [{
                             'display_name': self.course_run.level_type.name,
                             'value': self.course_run.level_type.name
                         }, {
                             'display_name': self.course_run_2.level_type.name,
                             'value': self.course_run_2.level_type.name
                         }])
        self.assertEqual(data['content_language']['choices'],
                         [{
                             'display_name': x.name,
                             'value': x.code
                         } for x in LanguageTag.objects.all()])
        self.assertTrue(LanguageTag.objects.count() > 0)
コード例 #6
0
class AutocompleteTests(TestCase):
    """ Tests for autocomplete lookups."""
    def setUp(self):
        super(AutocompleteTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.courses = factories.CourseFactory.create_batch(3, title='Some random course title')
        for course in self.courses:
            factories.CourseRunFactory(course=course)
        self.organizations = factories.OrganizationFactory.create_batch(3)
        first_instructor = factories.PersonFactory(given_name="First Instructor")
        second_instructor = factories.PersonFactory(given_name="Second Instructor")
        self.instructors = [first_instructor, second_instructor]

    @ddt.data('dum', 'ing')
    def test_course_autocomplete(self, search_key):
        """ Verify course autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:course-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)
        # update the first course title
        self.courses[0].key = 'edx/dummy/key'
        self.courses[0].title = 'this is some thing new'
        self.courses[0].save()
        response = self.client.get(
            reverse('admin_metadata:course-autocomplete') + '?q={title}'.format(title=search_key)
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.courses[0]))

    def test_course_autocomplete_un_authorize_user(self):
        """ Verify course autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:course-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('ing', 'dum')
    def test_course_run_autocomplete(self, search_key):
        """ Verify course run autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:course-run-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)
        # update the first course title
        course = self.courses[0]
        course.title = 'this is some thing new'
        course.save()
        course_run = self.courses[0].course_runs.first()
        course_run.key = 'edx/dummy/testrun'
        course_run.save()

        response = self.client.get(
            reverse('admin_metadata:course-run-autocomplete') + '?q={q}'.format(q=search_key)
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(course_run))

    def test_course_run_autocomplete_un_authorize_user(self):
        """ Verify course run autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:course-run-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('irc', 'ing')
    def test_organization_autocomplete(self, search_key):
        """ Verify Organization autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:organisation-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)

        self.organizations[0].key = 'Mirco'
        self.organizations[0].name = 'testing name'
        self.organizations[0].save()

        response = self.client.get(
            reverse('admin_metadata:organisation-autocomplete') + '?q={key}'.format(
                key=search_key
            )
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.organizations[0]))
        self.assertEqual(len(data['results']), 1)

    def test_organization_autocomplete_un_authorize_user(self):
        """ Verify Organization autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:organisation-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('dummyurl', 'testing')
    def test_video_autocomplete(self, search_key):
        """ Verify video autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:video-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 6)

        self.courses[0].video.src = 'http://www.youtube.com/dummyurl'
        self.courses[0].video.description = 'testing description'
        self.courses[0].video.save()

        response = self.client.get(
            reverse('admin_metadata:video-autocomplete') + '?q={key}'.format(
                key=search_key
            )
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.courses[0].video))
        self.assertEqual(len(data['results']), 1)

    def test_video_autocomplete_un_authorize_user(self):
        """ Verify video autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:video-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    def _make_user_non_staff(self):
        self.client.logout()
        self.user = UserFactory(is_staff=False)
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)

    def test_instructor_autocomplete(self):
        """ Verify instructor autocomplete returns the data. """
        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins')
        )
        self._assert_response(response, 2)

        # update first instructor's name
        self.instructors[0].given_name = 'dummy_name'
        self.instructors[0].save()

        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='dummy')
        )
        self._assert_response(response, 1)

    def test_instructor_autocomplete_un_authorize_user(self):
        """ Verify instructor autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:person-autocomplete'))
        self._assert_response(response, 0)

    def test_instructor_position_in_label(self):
        """ Verify that instructor label contains position of instructor if it exists."""
        position_title = 'professor'
        PositionFactory.create(person=self.instructors[0], title=position_title, organization=self.organizations[0])

        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins')
        )

        self.assertContains(response, '<p>{position} at {organization}</p>'.format(
            position=position_title,
            organization=self.organizations[0].name))

    def test_instructor_image_in_label(self):
        """ Verify that instructor label contains profile image url."""
        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins')
        )
        self.assertContains(response, self.instructors[0].get_profile_image_url)
        self.assertContains(response, self.instructors[1].get_profile_image_url)

    def _assert_response(self, response, expected_length):
        """ Assert autocomplete response. """
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(len(data['results']), expected_length)

    def test_instructor_autocomplete_with_uuid(self):
        """ Verify instructor autocomplete returns the data with valid uuid. """
        uuid = self.instructors[0].uuid
        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q=uuid)
        )
        self._assert_response(response, 1)

    def test_instructor_autocomplete_with_invalid_uuid(self):
        """ Verify instructor autocomplete returns empty list without giving error. """
        uuid = 'invalid-uuid'
        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q=uuid)
        )
        self._assert_response(response, 0)
コード例 #7
0
class AutoCompletePersonTests(SiteMixin, TestCase):
    """
    Tests for person autocomplete lookups
    """
    def setUp(self):
        super(AutoCompletePersonTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.courses = factories.CourseFactory.create_batch(
            3, title='Some random course title')

        for course in self.courses:
            factories.CourseRunFactory(course=course)

        self.organizations = OrganizationFactory.create_batch(3)
        self.organization_extensions = []

        for organization in self.organizations:
            self.organization_extensions.append(
                factories.OrganizationExtensionFactory(
                    organization=organization))

        self.user.groups.add(self.organization_extensions[0].group)
        first_instructor = PersonFactory(given_name="First",
                                         family_name="Instructor")
        second_instructor = PersonFactory(given_name="Second",
                                          family_name="Instructor")
        self.instructors = [first_instructor, second_instructor]

        for instructor in self.instructors:
            PositionFactory(organization=self.organizations[0],
                            title="professor",
                            person=instructor)

    def query(self, q):
        return self.client.get(
            reverse('admin_metadata:person-autocomplete') +
            '?q={q}'.format(q=q))

    def test_instructor_autocomplete(self):
        """ Verify instructor autocomplete returns the data. """
        response = self.query('ins')
        self._assert_response(response, 2)

        # update first instructor's name
        self.instructors[0].given_name = 'dummy_name'
        self.instructors[0].save()

        response = self.query('dummy')
        self._assert_response(response, 1)

    def test_instructor_autocomplete_un_authorize_user(self):
        """ Verify instructor autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(
            reverse('admin_metadata:person-autocomplete'))
        self._assert_response(response, 0)

    def test_instructor_autocomplete_spaces(self):
        """ Verify instructor autocomplete allows spaces. """
        response = self.query('sec ins')
        self._assert_response(response, 1)

    def test_instructor_autocomplete_no_results(self):
        """ Verify instructor autocomplete correctly finds no matches if string doesn't match. """
        response = self.query('second nope')
        self._assert_response(response, 0)

    def test_instructor_autocomplete_last_name_first_name(self):
        """ Verify instructor autocomplete allows last name first. """
        response = self.query('instructor first')
        self._assert_response(response, 1)

    def test_instructor_position_in_label(self):
        """ Verify that instructor label contains position of instructor if it exists."""
        position_title = 'professor'

        response = self.query('ins')

        self.assertContains(
            response, '<p>{position} at {organization}</p>'.format(
                position=position_title,
                organization=self.organizations[0].name))

    def test_instructor_image_in_label(self):
        """ Verify that instructor label contains profile image url."""
        response = self.query('ins')
        self.assertContains(response,
                            self.instructors[0].get_profile_image_url)
        self.assertContains(response,
                            self.instructors[1].get_profile_image_url)

    def _assert_response(self, response, expected_length):
        """ Assert autocomplete response. """
        assert response.status_code == 200
        data = json.loads(response.content.decode('utf-8'))
        assert len(data['results']) == expected_length

    def test_instructor_autocomplete_with_uuid(self):
        """ Verify instructor autocomplete returns the data with valid uuid. """
        uuid = self.instructors[0].uuid
        response = self.query(uuid)
        self._assert_response(response, 1)

    def test_instructor_autocomplete_with_invalid_uuid(self):
        """ Verify instructor autocomplete returns empty list without giving error. """
        uuid = 'invalid-uuid'
        response = self.query(uuid)
        self._assert_response(response, 0)

    def test_instructor_autocomplete_without_staff_user(self):
        """ Verify instructor autocomplete returns the data if user is not staff. """
        non_staff_user = UserFactory()
        non_staff_user.groups.add(self.organization_extensions[0].group)
        self.client.logout()
        self.client.login(username=non_staff_user.username,
                          password=USER_PASSWORD)

        response = self.query('ins')
        self._assert_response(response, 2)

    def test_instructor_autocomplete_without_login(self):
        """ Verify instructor autocomplete returns the zero record if user is not logged in. """
        self.client.logout()
        person_autocomplete_url = reverse(
            'admin_metadata:person-autocomplete') + '?q={q}'.format(
                q=self.instructors[0].uuid)

        response = self.client.get(person_autocomplete_url)

        self.assertRedirects(response,
                             expected_url='{url}?next={next}'.format(
                                 url=reverse('login'),
                                 next=quote(person_autocomplete_url)),
                             status_code=302,
                             target_status_code=302)

    def test_instructor_autocomplete_from_django_admin(self):
        """ Verify instructor autocomplete return default data from django admin. """
        admin_user = UserFactory(is_staff=True, is_superuser=True)
        self.client.logout()
        self.client.login(username=admin_user.username, password=USER_PASSWORD)

        response = self.client.get(
            reverse('admin_metadata:person-autocomplete') +
            '?q={q}'.format(q='ins'),
            HTTP_REFERER=reverse('admin:publisher_courserun_add'))
        assert response.status_code == 200
        data = json.loads(response.content.decode('utf-8'))
        expected_results = [{
            'id': instructor.id,
            'text': str(instructor)
        } for instructor in self.instructors]
        assert data.get('results') == expected_results

    def _make_user_non_staff(self):
        self.client.logout()
        self.user = UserFactory(is_staff=False)
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
コード例 #8
0
class CourseViewSetTests(SerializationMixin, APITestCase):
    def setUp(self):
        super(CourseViewSetTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.request.user = self.user
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.course = CourseFactory(partner=self.partner,
                                    title='Fake Test',
                                    key='edX+Fake101')
        self.org = OrganizationFactory(key='edX', partner=self.partner)
        self.course.authoring_organizations.add(self.org)  # pylint: disable=no-member

    def test_get(self):
        """ Verify the endpoint returns the details for a single course. """
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})

        with self.assertNumQueries(27):
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.data, self.serialize_course(self.course))

    def test_get_uuid(self):
        """ Verify the endpoint returns the details for a single course with UUID. """
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})

        with self.assertNumQueries(27):
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.data, self.serialize_course(self.course))

    def test_get_exclude_deleted_programs(self):
        """ Verify the endpoint returns no deleted associated programs """
        ProgramFactory(courses=[self.course], status=ProgramStatus.Deleted)
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
        with self.assertNumQueries(18):
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertEqual(response.data.get('programs'), [])

    def test_get_include_deleted_programs(self):
        """
        Verify the endpoint returns associated deleted programs
        with the 'include_deleted_programs' flag set to True
        """
        ProgramFactory(courses=[self.course], status=ProgramStatus.Deleted)
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
        url += '?include_deleted_programs=1'
        with self.assertNumQueries(34):
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertEqual(
                response.data,
                self.serialize_course(
                    self.course,
                    extra_context={'include_deleted_programs': True}))

    def test_get_include_hidden_course_runs(self):
        """
        Verify the endpoint returns associated hidden course runs
        with the 'include_hidden_course_runs' flag set to True
        """
        CourseRunFactory(status=CourseRunStatus.Published,
                         end=datetime.datetime.now(pytz.UTC) +
                         datetime.timedelta(days=10),
                         enrollment_start=None,
                         enrollment_end=None,
                         hidden=True,
                         course=self.course)
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
        url += '?include_hidden_course_runs=1'

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data, self.serialize_course(self.course))

    @ddt.data(1, 0)
    def test_marketable_course_runs_only(self, marketable_course_runs_only):
        """
        Verify that a client requesting marketable_course_runs_only only receives
        course runs that are published, have seats, and can still be enrolled in.
        """
        # Published course run with a seat, no enrollment start or end, and an end date in the future.
        enrollable_course_run = CourseRunFactory(
            status=CourseRunStatus.Published,
            end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
            enrollment_start=None,
            enrollment_end=None,
            course=self.course)
        SeatFactory(course_run=enrollable_course_run)

        # Unpublished course run with a seat.
        unpublished_course_run = CourseRunFactory(
            status=CourseRunStatus.Unpublished, course=self.course)
        SeatFactory(course_run=unpublished_course_run)

        # Published course run with no seats.
        no_seats_course_run = CourseRunFactory(
            status=CourseRunStatus.Published, course=self.course)

        # Published course run with a seat and an end date in the past.
        closed_course_run = CourseRunFactory(
            status=CourseRunStatus.Published,
            end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10),
            course=self.course)
        SeatFactory(course_run=closed_course_run)

        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
        url = '{}?marketable_course_runs_only={}'.format(
            url, marketable_course_runs_only)
        response = self.client.get(url)

        assert response.status_code == 200

        if marketable_course_runs_only:
            # Emulate prefetching behavior.
            for course_run in (unpublished_course_run, no_seats_course_run,
                               closed_course_run):
                course_run.delete()

        assert response.data == self.serialize_course(self.course)

    @ddt.data(1, 0)
    def test_marketable_enrollable_course_runs_with_archived(
            self, marketable_enrollable_course_runs_with_archived):
        """ Verify the endpoint filters course runs to those that are marketable and
        enrollable, including archived course runs (with an end date in the past). """

        past = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2)
        future = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)

        course_run = CourseRunFactory(enrollment_start=None,
                                      enrollment_end=future,
                                      course=self.course)
        SeatFactory(course_run=course_run)

        filtered_course_runs = [
            CourseRunFactory(enrollment_start=None,
                             enrollment_end=None,
                             course=self.course),
            CourseRunFactory(enrollment_start=past,
                             enrollment_end=future,
                             course=self.course),
            CourseRunFactory(enrollment_start=future, course=self.course),
            CourseRunFactory(enrollment_end=past, course=self.course),
        ]

        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
        url = '{}?marketable_enrollable_course_runs_with_archived={}'.format(
            url, marketable_enrollable_course_runs_with_archived)
        response = self.client.get(url)

        assert response.status_code == 200

        if marketable_enrollable_course_runs_with_archived:
            # Emulate prefetching behavior.
            for course_run in filtered_course_runs:
                course_run.delete()

        assert response.data == self.serialize_course(self.course)

    @ddt.data(1, 0)
    def test_get_include_published_course_run(self,
                                              published_course_runs_only):
        """
        Verify the endpoint returns hides unpublished programs if
        the 'published_course_runs_only' flag is set to True
        """
        CourseRunFactory(status=CourseRunStatus.Published, course=self.course)
        unpublished_course_run = CourseRunFactory(
            status=CourseRunStatus.Unpublished, course=self.course)

        url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
        url = '{}?published_course_runs_only={}'.format(
            url, published_course_runs_only)

        response = self.client.get(url)

        assert response.status_code == 200

        if published_course_runs_only:
            # Emulate prefetching behavior.
            unpublished_course_run.delete()

        assert response.data == self.serialize_course(self.course)

    def test_list(self):
        """ Verify the endpoint returns a list of all courses. """
        url = reverse('api:v1:course-list')

        with self.assertNumQueries(35):
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)
            self.assertListEqual(
                response.data['results'],
                self.serialize_course(Course.objects.all().order_by(
                    Lower('key')),
                                      many=True))

    def test_list_query(self):
        """ Verify the endpoint returns a filtered list of courses """
        title = 'Some random title'
        courses = CourseFactory.create_batch(3, title=title)
        courses = sorted(courses, key=lambda course: course.key.lower())
        query = 'title:' + title
        url = '{root}?q={query}'.format(root=reverse('api:v1:course-list'),
                                        query=query)

        with self.assertNumQueries(51):
            response = self.client.get(url)
            self.assertListEqual(response.data['results'],
                                 self.serialize_course(courses, many=True))

    def test_list_key_filter(self):
        """ Verify the endpoint returns a list of courses filtered by the specified keys. """
        courses = CourseFactory.create_batch(3, partner=self.partner)
        courses = sorted(courses, key=lambda course: course.key.lower())
        keys = ','.join([course.key for course in courses])
        url = '{root}?keys={keys}'.format(root=reverse('api:v1:course-list'),
                                          keys=keys)

        with self.assertNumQueries(51):
            response = self.client.get(url)
            self.assertListEqual(response.data['results'],
                                 self.serialize_course(courses, many=True))

    def test_list_uuid_filter(self):
        """ Verify the endpoint returns a list of courses filtered by the specified uuid. """
        courses = CourseFactory.create_batch(3, partner=self.partner)
        courses = sorted(courses, key=lambda course: course.key.lower())
        uuids = ','.join([str(course.uuid) for course in courses])
        url = '{root}?uuids={uuids}'.format(root=reverse('api:v1:course-list'),
                                            uuids=uuids)

        with self.assertNumQueries(51):
            response = self.client.get(url)
            self.assertListEqual(response.data['results'],
                                 self.serialize_course(courses, many=True))

    def test_list_exclude_utm(self):
        """ Verify the endpoint returns marketing URLs without UTM parameters. """
        url = reverse('api:v1:course-list') + '?exclude_utm=1'

        response = self.client.get(url)
        context = {'exclude_utm': 1}
        self.assertEqual(
            response.data['results'],
            self.serialize_course([self.course],
                                  many=True,
                                  extra_context=context))

    @ddt.data(
        ('get', False, False, True),
        ('options', False, False, True),
        ('post', False, False, False),
        ('post', False, True, True),
        ('post', True, False, True),
    )
    @ddt.unpack
    def test_editor_access_list_endpoint(self, method, is_staff, in_org,
                                         allowed):
        """ Verify we check editor access correctly when hitting the courses endpoint. """
        self.user.is_staff = is_staff
        self.user.save()

        if in_org:
            org_ext = OrganizationExtensionFactory(organization=self.org)
            self.user.groups.add(org_ext.group)

        response = getattr(self.client, method)(reverse('api:v1:course-list'),
                                                {
                                                    'org': self.org.key
                                                },
                                                format='json')

        if not allowed:
            self.assertEqual(response.status_code, 403)
        else:
            self.assertNotEqual(response.status_code, 403)

    @ddt.data(
        ('get', False, False, False, True),
        ('options', False, False, False, True),
        ('put', False, False, False, False),  # no access
        ('put', True, False, False, True),  # is staff
        ('patch', False, True, False, False),  # is in org
        ('patch', False, False, True, False),  # is editor but not in org
        ('put', False, True, True, True),  # editor and in org
    )
    @ddt.unpack
    def test_editor_access_detail_endpoint(self, method, is_staff, in_org,
                                           is_editor, allowed):
        """ Verify we check editor access correctly when hitting the course object endpoint. """
        self.user.is_staff = is_staff
        self.user.save()

        # Add another editor, because we have some logic that allows access anyway if a course has no valid editors.
        # That code path is checked in test_course_without_editors below.
        org_ext = OrganizationExtensionFactory(organization=self.org)
        user2 = UserFactory()
        user2.groups.add(org_ext.group)
        CourseEditorFactory(user=user2, course=self.course)

        if in_org:
            # Editors must be in the org to get editor access
            self.user.groups.add(org_ext.group)

        if is_editor:
            CourseEditorFactory(user=self.user, course=self.course)

        response = getattr(self.client,
                           method)(reverse('api:v1:course-detail',
                                           kwargs={'key': self.course.uuid}))

        if not allowed:
            self.assertEqual(response.status_code, 404)
        else:
            # We'll probably fail because we didn't include the right data - but at least we'll have gotten in
            self.assertNotEqual(response.status_code, 404)

    def test_editable_list_gives_drafts(self):
        draft = CourseFactory(partner=self.partner,
                              uuid=self.course.uuid,
                              key=self.course.key,
                              draft=True)
        draft_course_run = CourseRunFactory(
            status=CourseRunStatus.Published,
            end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10),
            course=draft,
            draft=True,
        )
        self.course.draft_version = draft
        self.course.save()
        extra = CourseFactory(partner=self.partner, key=self.course.key +
                              'Z')  # set key so it sorts later

        response = self.client.get(
            reverse('api:v1:course-list') + '?editable=1')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data['results'],
                         self.serialize_course([draft, extra], many=True))
        self.assertEqual(len(response.data['results'][0]['course_runs']), 1)
        self.assertEqual(response.data['results'][0]['course_runs'][0]['uuid'],
                         str(draft_course_run.uuid))

    def test_editable_get_gives_drafts(self):
        draft = CourseFactory(partner=self.partner,
                              uuid=self.course.uuid,
                              key=self.course.key,
                              draft=True)
        self.course.draft_version = draft
        self.course.save()
        extra = CourseFactory(partner=self.partner)

        response = self.client.get(
            reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) +
            '?editable=1')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data, self.serialize_course(draft,
                                                              many=False))

        response = self.client.get(
            reverse('api:v1:course-detail', kwargs={'key': extra.uuid}) +
            '?editable=1')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data, self.serialize_course(extra,
                                                              many=False))

    def test_course_without_editors(self):
        """ Verify we can modify a course with no editors if we're in its authoring org. """
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        self.user.is_staff = False
        self.user.save()
        self.course.draft = True
        self.course.save()

        # Try without being in the organization nor an editor
        self.assertEqual(self.client.patch(url).status_code, 404)

        # Add to authoring org, and we should be let in
        org_ext = OrganizationExtensionFactory(organization=self.org)
        self.user.groups.add(org_ext.group)
        self.assertNotEqual(self.client.patch(url).status_code, 404)

        # Now add a random other user as an editor to the course, so that we will no longer be granted access.
        editor = UserFactory()
        CourseEditorFactory(user=editor, course=self.course)
        editor.groups.add(org_ext.group)
        self.assertEqual(self.client.patch(url).status_code, 404)

        # But if the editor is no longer valid (even though they exist), we're back to having access.
        editor.groups.remove(org_ext.group)
        self.assertNotEqual(self.client.patch(url).status_code, 404)

        # And finally, for a sanity check, confirm we have access when we become an editor also
        CourseEditorFactory(user=self.user, course=self.course)
        self.assertNotEqual(self.client.patch(url).status_code, 404)

    def test_delete_not_allowed(self):
        """ Verify we don't allow deleting a course from the API. """
        response = self.client.delete(
            reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}))
        self.assertEqual(response.status_code, 405)

    def test_create_without_authentication(self):
        """ Verify authentication is required when creating a course. """
        self.client.logout()
        Course.objects.all().delete()

        url = reverse('api:v1:course-list')
        response = self.client.post(url)
        assert response.status_code == 403
        assert Course.objects.count() == 0

    def create_course(self, data=None, update=True):
        url = reverse('api:v1:course-list')
        if update:
            course_data = {
                'title': 'Course title',
                'number': 'test101',
                'org': self.org.key,
                'mode': 'audit',
            }
            course_data.update(data or {})
        else:
            course_data = data or {}
        return self.client.post(url, course_data, format='json')

    @oauth_login
    @responses.activate
    def test_create_with_authentication_verified_mode(self):
        course_data = {
            'mode': 'verified',
            'price': 100,
        }
        ecom_url = self.partner.ecommerce_api_url + 'products/'
        ecom_entitlement_data = {
            'product_class': 'Course Entitlement',
            'title': 'Course title',
            'price': course_data['price'],
            'certificate_type': course_data['mode'],
            'uuid': '00000000-0000-0000-0000-000000000000',
            'stockrecords': [{
                'partner_sku': 'ABC123'
            }],
        }
        responses.add(responses.POST,
                      ecom_url,
                      body=json.dumps(ecom_entitlement_data),
                      content_type='application/json',
                      status=201,
                      match_querystring=True)
        response = self.create_course(course_data)

        course = Course.everything.last()
        self.assertDictEqual(response.data, self.serialize_course(course))
        self.assertEqual(response.status_code, 201)
        expected_course_key = '{org}+{number}'.format(org=self.org.key,
                                                      number='test101')
        self.assertEqual(course.key, expected_course_key)
        self.assertEqual(course.title, 'Course title')
        self.assertListEqual(list(course.authoring_organizations.all()),
                             [self.org])
        self.assertEqual(1, CourseEntitlement.everything.count())  # pylint: disable=no-member

    @oauth_login
    def test_create_with_authentication_audit_mode(self):
        """
        When creating with audit mode, no entitlement should be created.
        """
        response = self.create_course()

        course = Course.everything.last()
        self.assertDictEqual(response.data, self.serialize_course(course))
        self.assertEqual(response.status_code, 201)
        expected_course_key = '{org}+{number}'.format(org=self.org.key,
                                                      number='test101')
        self.assertEqual(course.key, expected_course_key)
        self.assertEqual(course.title, 'Course title')
        self.assertListEqual(list(course.authoring_organizations.all()),
                             [self.org])
        self.assertEqual(0, CourseEntitlement.objects.count())

    @oauth_login
    def test_create_makes_draft(self):
        """ When creating a course, it should start as a draft. """
        ecom_url = self.partner.ecommerce_api_url + 'products/'
        ecom_entitlement_data = {
            'product_class': 'Course Entitlement',
            'title': 'Course title',
            'price': '0.0',
            'certificate_type': 'verified',
            'uuid': '00000000-0000-0000-0000-000000000000',
            'stockrecords': [{
                'partner_sku': 'ABC123'
            }],
        }
        responses.add(responses.POST,
                      ecom_url,
                      body=json.dumps(ecom_entitlement_data),
                      content_type='application/json',
                      status=201,
                      match_querystring=True)

        response = self.create_course({'mode': 'verified'})
        self.assertEqual(response.status_code, 201)

        course = Course.everything.last()
        self.assertTrue(course.draft)
        self.assertTrue(course.entitlements.first().draft)

    @oauth_login
    def test_create_fails_if_official_version_exists(self):
        """ When creating a course, it should not create one if an official version already exists. """
        response = self.create_course({'number': 'Fake101'})
        self.assertEqual(response.status_code, 400)
        expected_error_message = 'Failed to set course data: A course with key {key} already exists.'
        self.assertEqual(response.data,
                         expected_error_message.format(key=self.course.key))

    def test_create_fails_with_missing_field(self):
        response = self.create_course(
            {
                'title': 'Course title',
                'org': self.org.key,
                'mode': 'audit',
            },
            update=False)
        self.assertEqual(response.status_code, 400)
        expected_error_message = 'Incorrect data sent. Missing value for: [number].'
        self.assertEqual(response.data, expected_error_message)

    def test_create_fails_with_nonexistent_org(self):
        response = self.create_course({'org': 'fake org'})
        self.assertEqual(response.status_code, 400)
        expected_error_message = 'Incorrect data sent. Organization does not exist.'
        self.assertEqual(response.data, expected_error_message)

    def test_create_fails_with_nonexistent_mode(self):
        response = self.create_course({'mode': 'fake mode'})
        self.assertEqual(response.status_code, 400)
        expected_error_message = 'Incorrect data sent. Entitlement Track does not exist.'
        self.assertEqual(response.data, expected_error_message)

    @ddt.data(
        ({
            'title': 'Course title',
            'number': 'test101',
            'org': 'fake org',
            'mode': 'fake mode'
        },
         'Incorrect data sent. Organization does not exist. Entitlement Track does not exist.'
         ),
        ({
            'title': 'Course title',
            'org': 'edX',
            'mode': 'fake mode'
        },
         'Incorrect data sent. Missing value for: [number]. Entitlement Track does not exist.'
         ),
        ({
            'title': 'Course title',
            'org': 'fake org',
            'mode': 'audit'
        },
         'Incorrect data sent. Missing value for: [number]. Organization does not exist.'
         ),
        ({
            'number': 'test101',
            'org': 'fake org',
            'mode': 'fake mode'
        },
         'Incorrect data sent. Missing value for: [title]. Organization does not exist. '
         'Entitlement Track does not exist.'),
    )
    @ddt.unpack
    def test_create_fails_with_multiple_errors(self, course_data,
                                               expected_error_message):
        response = self.create_course(course_data, update=False)
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data, expected_error_message)

    def test_create_with_api_exception(self):
        with mock.patch(
                # We are using get_course_key because it is called prior to tryig to contact the
                # e-commerce service and still gives the effect of an api exception.
                'course_discovery.apps.api.v1.views.courses.CourseViewSet.get_course_key',
                side_effect=IntegrityError):
            with LogCapture(course_logger.name) as log_capture:
                response = self.create_course()
                self.assertEqual(response.status_code, 400)
                log_capture.check((
                    course_logger.name,
                    'ERROR',
                    'An error occurred while setting Course data.',
                ))

    @oauth_login
    @responses.activate
    def test_create_with_ecom_api_exception(self):
        ecom_url = self.partner.ecommerce_api_url + 'products/'
        expected_error_message = 'Missing or bad value for: [title].'
        responses.add(
            responses.POST,
            ecom_url,
            body=expected_error_message,
            status=400,
        )
        with LogCapture(course_logger.name) as log_capture:
            response = self.create_course({'mode': 'verified', 'price': 100})
            self.assertEqual(response.status_code, 400)
            log_capture.check((
                course_logger.name, 'ERROR',
                'The following error occurred while setting the Course Entitlement data in E-commerce: '
                '{ecom_error}'.format(ecom_error=expected_error_message)))

    def test_update_without_authentication(self):
        """ Verify authentication is required when updating a course. """
        self.client.logout()
        Course.objects.all().delete()

        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        response = self.client.patch(url)
        assert response.status_code == 403
        assert Course.objects.count() == 0

    @ddt.data('put', 'patch')
    @oauth_login
    @responses.activate
    def test_update_success(self, method):
        entitlement = CourseEntitlementFactory(course=self.course)
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        course_data = {
            'title':
            'Course title',
            'partner':
            self.partner.id,
            'key':
            self.course.key,
            'entitlements': [
                {
                    'mode': entitlement.mode.slug,
                    'price': 1000,
                    'sku': entitlement.sku,
                    'expires': entitlement.expires,
                },
            ],
            # The API is expecting the image to be base64 encoded. We are simulating that here.
            'image':
            'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY'
            '42YAAAAASUVORK5CYII=',
            'video': {
                'src': 'https://link.to.video.for.testing/watch?t_s=5'
            },
        }
        ecom_url = '{0}stockrecords/{1}/'.format(
            self.partner.ecommerce_api_url, entitlement.sku)
        responses.add(
            responses.PUT,
            ecom_url,
            status=200,
        )
        response = getattr(self.client, method)(url,
                                                course_data,
                                                format='json')
        self.assertEqual(response.status_code, 200)

        course = Course.everything.get(uuid=self.course.uuid, draft=True)
        self.assertEqual(course.title, 'Course title')
        self.assertEqual(course.entitlements.first().price, 1000)
        self.assertDictEqual(response.data, self.serialize_course(course))

    @oauth_login
    @responses.activate
    def test_update_operates_on_drafts(self):
        CourseEntitlementFactory(course=self.course)
        self.assertFalse(
            Course.everything.filter(uuid=self.course.uuid,
                                     draft=True).exists())  # sanity check

        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        response = self.client.patch(url, {'title': 'Title'}, format='json')
        self.assertEqual(response.status_code, 200)

        course = Course.everything.get(uuid=self.course.uuid, draft=True)
        self.assertTrue(course.entitlements.first().draft)
        self.assertEqual(course.title, 'Title')

        self.course.refresh_from_db()
        self.assertFalse(self.course.draft)
        self.assertFalse(self.course.entitlements.first().draft)
        self.assertEqual(self.course.title, 'Fake Test')

    @ddt.data(
        (
            {
                'entitlements': [{}]
            },
            'Entitlements must have a mode specified.',
        ),
        ({
            'entitlements': [{
                'mode': 'NOPE'
            }]
        }, 'Entitlement mode NOPE not found.'),
        ({
            'entitlements': [{
                'mode': 'mode2'
            }]
        },
         'Existing entitlement not found for mode mode2 in course Org/Course/Number.'
         ),
        ({
            'entitlements': [{
                'mode': 'mode1'
            }]
        }, 'Entitlement does not have a valid SKU assigned.'),
    )
    @ddt.unpack
    def test_update_fails_with_multiple_errors(self, course_data,
                                               expected_error_message):
        course = CourseFactory(partner=self.partner, key='Org/Course/Number')
        url = reverse('api:v1:course-detail', kwargs={'key': course.uuid})
        mode1 = SeatTypeFactory(name='Mode1')
        SeatTypeFactory(name='Mode2')
        CourseEntitlementFactory(course=course, mode=mode1, sku=None)
        response = self.client.patch(url, course_data, format='json')
        self.assertEqual(response.status_code, 400)
        self.assertEqual(response.data, expected_error_message)

    def test_update_with_api_exception(self):
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        course_data = {
            'title': 'Course title',
            'entitlements': [
                {
                    'price': 1000,
                },
            ],
        }
        with mock.patch(
                'course_discovery.apps.api.v1.views.courses.CourseViewSet.update_entitlement',
                side_effect=IntegrityError):
            with LogCapture(course_logger.name) as log_capture:
                response = self.client.patch(url, course_data, format='json')
                self.assertEqual(response.status_code, 400)
                log_capture.check((
                    course_logger.name,
                    'ERROR',
                    'An error occurred while setting Course data.',
                ))

    @oauth_login
    @responses.activate
    def test_update_with_ecom_api_exception(self):
        entitlement = CourseEntitlementFactory(course=self.course)
        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        course_data = {
            'title': 'Course title',
            'entitlements': [
                {
                    'mode': entitlement.mode.slug,
                    'price': 1000,
                },
            ],
        }
        ecom_url = '{0}stockrecords/{1}/'.format(
            self.partner.ecommerce_api_url, entitlement.sku)
        expected_error_message = 'Nope'
        responses.add(
            responses.PUT,
            ecom_url,
            body=expected_error_message,
            status=400,
        )
        with LogCapture(course_logger.name) as log_capture:
            response = self.client.patch(url, course_data, format='json')
            self.assertEqual(response.status_code, 400)
            log_capture.check((
                course_logger.name, 'ERROR',
                'The following error occurred while setting the Course Entitlement data in E-commerce: '
                '{ecom_error}'.format(ecom_error=expected_error_message)))

    @oauth_login
    @responses.activate
    def test_options(self):
        SubjectFactory(name='Subject1')
        CourseEntitlementFactory(course=self.course,
                                 mode=SeatType.objects.get(slug='verified'))

        url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})
        response = self.client.options(url)
        self.assertEqual(response.status_code, 200)

        data = response.data['actions']['PUT']
        self.assertEqual(data['level_type']['choices'],
                         [{
                             'display_name': self.course.level_type.name,
                             'value': self.course.level_type.name
                         }])
        self.assertEqual(
            data['entitlements']['child']['children']['mode']['choices'],
            [{
                'display_name': 'Audit',
                'value': 'audit'
            }, {
                'display_name': 'Credit',
                'value': 'credit'
            }, {
                'display_name': 'Professional',
                'value': 'professional'
            }, {
                'display_name': 'Verified',
                'value': 'verified'
            }])
        self.assertEqual(data['subjects']['child']['choices'],
                         [{
                             'display_name': 'Subject1',
                             'value': 'subject1'
                         }])
        self.assertFalse(
            'choices' in
            data['partner'])  # we don't whitelist partner to show its choices
コード例 #9
0
class RateLimitingExceededTest(SiteMixin, APITestCase):
    """
    Testing rate limiting of API calls.
    """

    def setUp(self):
        super().setUp()

        self.url = reverse('api_docs')
        self.user = UserFactory()
        self.client.login(username=self.user.username, password=USER_PASSWORD)

    def tearDown(self):
        super().tearDown()
        throttling_cache().clear()

    def _make_requests(self, count=None):
        """ Make multiple requests until the throttle's limit is exceeded.

        Returns
            Response: Response of the last request.
        """
        count = count or 6
        user_rates = {'user': '******'}
        with patch('rest_framework.views.APIView.throttle_classes', (OverridableUserRateThrottle,)):
            with patch.object(OverridableUserRateThrottle, 'THROTTLE_RATES', user_rates):
                for __ in range(count - 1):
                    response = self.client.get(self.url)
                    assert response.status_code == 200
                response = self.client.get(self.url)
        return response

    def assert_rate_limit_successfully_exceeded(self):
        """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """
        response = self._make_requests()
        assert response.status_code == 200

    def assert_rate_limited(self, count=None):
        """ Asserts that the throttle's rate limit was exceeded and we were denied. """
        response = self._make_requests(count)
        assert response.status_code == 429

    def test_rate_limiting(self):
        """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """
        self.assert_rate_limited()

    def test_user_throttle_rate(self):
        """ Verify the UserThrottleRate can be used to override the default rate limit. """
        UserThrottleRate.objects.create(user=self.user, rate='10/hour')
        self.assert_rate_limited(11)

    def test_superuser_throttling(self):
        """ Verify superusers are not throttled. """
        self.user.is_superuser = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()

    def test_staff_throttling(self):
        """ Verify staff users are not throttled. """
        self.user.is_staff = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()

    def test_publisher_user_throttling(self):
        """ Verify publisher users are not throttled. """
        self.user.groups.add(GroupFactory())
        self.assert_rate_limit_successfully_exceeded()

    def test_staff_with_user_throttle_rate(self):
        """ Verify the UserThrottleRate kicks in even for staff. """
        self.user.is_staff = True
        self.user.save()
        UserThrottleRate.objects.create(user=self.user, rate='10/hour')
        self.assert_rate_limited(11)
コード例 #10
0
class CommentViewSetTests(OAuth2Mixin, APITestCase):

    @factory.django.mute_signals(m2m_changed)
    def setUp(self):
        super().setUp()
        self.salesforce_config = SalesforceConfigurationFactory(partner=self.partner)
        self.user = UserFactory(is_staff=True)
        self.request.user = self.user
        self.request.site.partner = self.partner
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.course = CourseFactoryNoSignals(partner=self.partner, title='Fake Test', key='edX+Fake101', draft=True)
        self.org = OrganizationFactoryNoSignals(key='edX', partner=self.partner)
        self.course.authoring_organizations.add(self.org)

    def tearDown(self):
        super().tearDown()
        # Zero out the instances that are created during testing
        SalesforceUtil.instances = {}

    def test_list_no_salesforce_case_id_set(self):
        user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations'

        with mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce'):
            with mock.patch(user_orgs_path, return_value=[self.org]):
                url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid)
                response = self.client.get(url)
                self.assertEqual(response.status_code, 200)
                self.assertEqual(response.data, [])

    def test_list_salesforce_case_id_set(self):
        self.course.salesforce_id = 'TestSalesforceId'
        with factory.django.mute_signals(post_save):
            self.course.save()

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'
        get_comments_path = 'course_discovery.apps.api.v1.views.comments.SalesforceUtil.get_comments_for_course'
        user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations'
        return_value = [
            {
                'user': {
                    'first_name': 'TestFirst',
                    'last_name': 'TestLast',
                    'email': '*****@*****.**',
                    'username': '******',
                },
                'course_run_key': None,
                'created': '2000-01-01T00:00:00.000+0000',
                'comment': 'This is a test comment',
            }
        ]
        with mock.patch(salesforce_path):
            with mock.patch(user_orgs_path, return_value=[self.org]):
                with mock.patch(get_comments_path, return_value=return_value) as mock_get_comments:
                    url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid)
                    response = self.client.get(url)
                    mock_get_comments.assert_called_with(self.course)
                    self.assertEqual(response.status_code, 200)
                    self.assertEqual(response.data, return_value)

    def test_list_400s_without_course_uuid(self):
        with mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce'):
            url = reverse('api:v1:comment-list')
            response = self.client.get(url)
            self.assertEqual(response.status_code, 400)

    def test_list_404s_without_finding_course(self):
        fake_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'  # Needs to resemble a uuid to pass validation
        with mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce'):
            url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), fake_uuid)
            response = self.client.get(url)
            self.assertEqual(response.status_code, 404)

    def test_list_403s_without_permissions(self):
        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'
        user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations'
        self.user.is_staff = False
        self.user.save()

        with mock.patch(salesforce_path):
            with mock.patch(user_orgs_path, return_value=[]):
                url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid)
                response = self.client.get(url)
                self.assertEqual(response.status_code, 403)

    def test_list_200s_as_staff(self):
        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'
        user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations'

        with mock.patch(salesforce_path):
            with mock.patch(user_orgs_path, return_value=[]):
                url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid)
                response = self.client.get(url)
                self.assertEqual(response.status_code, 200)

    def test_create(self):
        body = {
            'course_uuid': self.course.uuid,
            'comment': 'Test comment',
            'course_run_key': 'test-key',
        }

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'
        create_comment_path = ('course_discovery.apps.api.v1.views.comments.'
                               'SalesforceUtil.create_comment_for_course_case')

        with mock.patch(salesforce_path):
            with mock.patch(create_comment_path, return_value={
                'user': {
                    'username': self.user.username,
                    'email': self.user.email,
                    'first_name': self.user.first_name,
                    'last_name': self.user.last_name,
                },
                'comment': 'Comment body',
                'created': datetime.datetime.now(datetime.timezone.utc).isoformat(),
            }) as mock_create_comment:
                url = reverse('api:v1:comment-list')
                response = self.client.post(url, body, format='json')
                mock_create_comment.assert_called_with(
                    self.course,
                    self.request.user,
                    body.get('comment'),
                    course_run_key=body.get('course_run_key'),
                )
                self.assertEqual(response.status_code, 201)

    def test_create_400s_without_data(self):
        body = {}

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'

        with mock.patch(salesforce_path):
            url = reverse('api:v1:comment-list')
            response = self.client.post(url, body, format='json')
            self.assertEqual(response.status_code, 400)

    def test_create_403s_without_permissions(self):
        body = {
            'course_uuid': self.course.uuid,
            'comment': 'Test comment',
            'course_run_key': 'test-key',
        }

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'
        is_editable_path = 'course_discovery.apps.api.v1.views.comments.CourseEditor.is_course_editable'

        with mock.patch(salesforce_path):
            with mock.patch(is_editable_path, return_value=False):
                url = reverse('api:v1:comment-list')
                response = self.client.post(url, body, format='json')
                self.assertEqual(response.status_code, 403)

    def test_create_404s_without_finding_course(self):
        body = {
            'course_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',  # Needs to resemble a uuid to pass validation
            'comment': 'Test comment',
            'course_run_key': 'test-key',
        }

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'

        with mock.patch(salesforce_path):
            url = reverse('api:v1:comment-list')
            response = self.client.post(url, body, format='json')
            self.assertEqual(response.status_code, 404)

    def test_create_404s_without_a_config(self):
        body = {
            'course_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',  # Needs to resemble a uuid to pass validation
            'comment': 'Test comment',
            'course_run_key': 'test-key',
        }

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'

        with mock.patch(salesforce_path):
            url = reverse('api:v1:comment-list')
            response = self.client.post(url, body, format='json')
            self.assertEqual(response.status_code, 404)

    def test_create_500s_without_a_successful_case_create(self):
        body = {
            'course_uuid': self.course.uuid,
            'comment': 'Test comment',
            'course_run_key': 'test-key',
        }

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'
        create_comment_path = ('course_discovery.apps.api.v1.views.comments.'
                               'SalesforceUtil.create_comment_for_course_case')

        with mock.patch(salesforce_path):
            with mock.patch(create_comment_path, side_effect=SalesforceMissingCaseException('Error')):
                url = reverse('api:v1:comment-list')
                response = self.client.post(url, body, format='json')
                self.assertEqual(response.status_code, 500)

    def test_list_404s_without_a_config(self):
        self.salesforce_config.delete()
        body = {
            'course_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',  # Needs to resemble a uuid to pass validation
            'comment': 'Test comment',
            'course_run_key': 'test-key',
        }

        salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce'

        with mock.patch(salesforce_path):
            url = reverse('api:v1:comment-list')
            response = self.client.post(url, body, format='json')
            self.assertEqual(response.status_code, 404)
コード例 #11
0
ファイル: test_admin.py プロジェクト: edx/course-discovery
class AdminTests(TestCase):
    """ Tests Admin page."""

    def setUp(self):
        super(AdminTests, self).setUp()
        self.user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.course_runs = factories.CourseRunFactory.create_batch(3)
        self.courses = [course_run.course for course_run in self.course_runs]

        self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0])
        self.program = factories.ProgramFactory(
            courses=self.courses, excluded_course_runs=[self.excluded_course_run]
        )

    def _post_data(self, status=ProgramStatus.Unpublished, marketing_slug='/foo'):
        return {
            'title': 'some test title',
            'courses': [self.courses[0].id],
            'type': self.program.type.id,
            'status': status,
            'marketing_slug': marketing_slug,
            'partner': self.program.partner.id
        }

    def assert_form_valid(self, data, files):
        form = ProgramAdminForm(data=data, files=files)
        self.assertTrue(form.is_valid())
        program = form.save()
        response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,)))
        self.assertEqual(response.status_code, 200)

    def assert_form_invalid(self, data, files):
        form = ProgramAdminForm(data=data, files=files)
        self.assertFalse(form.is_valid())
        self.assertEqual(
            form.errors['__all__'],
            ['Programs can only be activated if they have a marketing slug and a banner image.']
        )
        with self.assertRaises(ValueError):
            form.save()

    def test_program_detail_form(self):
        """ Verify in admin panel program detail form load successfully. """
        response = self.client.get(reverse('admin:course_metadata_program_change', args=(self.program.id,)))
        self.assertEqual(response.status_code, 200)

    def test_custom_course_selection_page(self):
        """ Verify that course selection page loads successfully. """
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, reverse('admin:course_metadata_program_change', args=(self.program.id,)))
        self.assertContains(response, reverse('admin:course_metadata_program_changelist'))

    def test_custom_course_selection_page_with_invalid_id(self):
        """ Verify that course selection page will return 404 for invalid program id. """
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(10,)))
        self.assertEqual(response.status_code, 404)

    def test_custom_course_selection_page_with_non_staff(self):
        """ Verify that course selection page will return 404 for non authorized user. """
        self.client.logout()
        self.user.is_superuser = False
        self.user.is_staff = False
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        self.assertEqual(response.status_code, 404)

    def test_page_loads_only_course_related_runs(self):
        """ Verify that course selection page loads only all course runs. Also marked checkboxes with
        excluded courses runs only.
        """
        # add some new courses and course runs
        factories.CourseRunFactory.create_batch(2)
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        html = '<input checked="checked" id="id_excluded_course_runs_0" '
        html += 'name="excluded_course_runs" type="checkbox" value="{id}" />'.format(
            id=self.excluded_course_run.id
        )
        self.assertContains(response, html)
        for run in self.course_runs:
            self.assertContains(response, run.key)

    def test_page_with_post_new_course_run(self):
        """ Verify that course selection page with posting the data. """

        self.assertEqual(1, self.program.excluded_course_runs.all().count())
        self.assertEqual(3, sum(1 for _ in self.program.course_runs))

        params = {
            'excluded_course_runs': [self.excluded_course_run.id, self.course_runs[0].id],
        }
        post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,))
        response = self.client.post(post_url, params)
        self.assertRedirects(
            response,
            expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)),
            status_code=302,
            target_status_code=200
        )
        self.assertEqual(2, self.program.excluded_course_runs.all().count())
        self.assertEqual(2, sum(1 for _ in self.program.course_runs))

    def test_page_with_post_without_course_run(self):
        """ Verify that course selection page without posting any selected excluded check run. """

        self.assertEqual(1, self.program.excluded_course_runs.all().count())
        params = {
            'excluded_course_runs': [],
        }
        post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,))
        response = self.client.post(post_url, params)
        self.assertRedirects(
            response,
            expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)),
            status_code=302,
            target_status_code=200
        )
        self.assertEqual(0, self.program.excluded_course_runs.all().count())
        self.assertEqual(4, sum(1 for _ in self.program.course_runs))
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        self.assertNotContains(response, '<input checked="checked")')

    @ddt.data(
        *itertools.product(
            (
                (False, False, False),
                (True, False, False),
                (False, True, False),
                (True, True, True)
            ),
            ProgramStatus.labels
        )
    )
    @ddt.unpack
    def test_program_activation_restrictions(self, booleans, label):
        """Verify that program activation requires both a marketing slug and a banner image."""
        has_marketing_slug, has_banner_image, can_be_activated = booleans
        status = getattr(ProgramStatus, label)

        marketing_slug = '/foo' if has_marketing_slug else ''
        banner_image = make_image_file('test_banner.jpg') if has_banner_image else ''

        data = self._post_data(status=status, marketing_slug=marketing_slug)
        files = {'banner_image': banner_image}

        if status == ProgramStatus.Active:
            if can_be_activated:
                # Transitioning to an active status should require a marketing slug and banner image.
                self.assert_form_valid(data, files)
            else:
                self.assert_form_invalid(data, files)
        else:
            # All other status transitions should be valid regardless of marketing slug and banner image.
            self.assert_form_valid(data, files)

    def test_new_program_without_courses(self):
        """ Verify that new program can be added without `courses`."""
        data = self._post_data()
        data['courses'] = []
        form = ProgramAdminForm(data)
        self.assertTrue(form.is_valid())
        program = form.save()
        self.assertEqual(0, program.courses.all().count())
        response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,)))
        self.assertEqual(response.status_code, 200)
コード例 #12
0
class RateLimitingExceededTest(SiteMixin, APITestCase):
    """
    Testing rate limiting of API calls.
    """

    def setUp(self):
        super(RateLimitingExceededTest, self).setUp()

        self.url = reverse('api_docs')
        self.user = UserFactory()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.default_rate_patcher = mock.patch.dict(
            settings.REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'],
            {'user': '******'}
        )
        self.default_rate_patcher.start()

    def tearDown(self):
        super(RateLimitingExceededTest, self).tearDown()
        throttling_cache().clear()
        self.user.is_superuser = False
        self.user.is_staff = False
        self.user.save()
        self.default_rate_patcher.stop()

    def _make_requests(self):
        """ Make multiple requests until the throttle's limit is exceeded.

        Returns
            Response: Response of the last request.
        """
        default_num_requests = OverridableUserRateThrottle().num_requests
        for __ in range(default_num_requests + 1):
            response = self.client.get(self.url)
        return response

    def test_rate_limiting(self):
        """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """
        response = self._make_requests()

        assert response.status_code == 429

    def test_user_throttle_rate(self):
        """ Verify the UserThrottleRate can be used to override the default rate limit. """
        UserThrottleRate.objects.create(user=self.user, rate='20/hour')
        self.assert_rate_limit_successfully_exceeded()

    def assert_rate_limit_successfully_exceeded(self):
        """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """
        response = self._make_requests()

        assert response.status_code == 200

    def test_superuser_throttling(self):
        """ Verify superusers are not throttled. """
        self.user.is_superuser = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()

    def test_staff_throttling(self):
        """ Verify staff users are not throttled. """
        self.user.is_staff = True
        self.user.save()
        self.assert_rate_limit_successfully_exceeded()

    def test_publisher_user_throttling(self):
        """ Verify publisher users are not throttled. """
        self.user.groups.add(GroupFactory())
        self.assert_rate_limit_successfully_exceeded()
コード例 #13
0
class AutocompleteTests(TestCase):
    """ Tests for autocomplete lookups."""
    def setUp(self):
        super(AutocompleteTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.courses = CourseFactory.create_batch(3, title='Some random course title')
        for course in self.courses:
            CourseRunFactory(course=course)
        self.organizations = OrganizationFactory.create_batch(3)
        first_instructor = PersonFactory(given_name="First Instructor")
        second_instructor = PersonFactory(given_name="Second Instructor")
        self.instructors = [first_instructor, second_instructor]

    @ddt.data('dum', 'ing')
    def test_course_autocomplete(self, search_key):
        """ Verify course autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:course-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)
        # update the first course title
        self.courses[0].key = 'edx/dummy/key'
        self.courses[0].title = 'this is some thing new'
        self.courses[0].save()
        response = self.client.get(
            reverse('admin_metadata:course-autocomplete') + '?q={title}'.format(title=search_key)
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.courses[0]))

    def test_course_autocomplete_un_authorize_user(self):
        """ Verify course autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:course-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('ing', 'dum')
    def test_course_run_autocomplete(self, search_key):
        """ Verify course run autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:course-run-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)
        # update the first course title
        course = self.courses[0]
        course.title = 'this is some thing new'
        course.save()
        course_run = self.courses[0].course_runs.first()
        course_run.key = 'edx/dummy/testrun'
        course_run.save()

        response = self.client.get(
            reverse('admin_metadata:course-run-autocomplete') + '?q={q}'.format(q=search_key)
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(course_run))

    def test_course_run_autocomplete_un_authorize_user(self):
        """ Verify course run autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:course-run-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('irc', 'ing')
    def test_organization_autocomplete(self, search_key):
        """ Verify Organization autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:organisation-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)

        self.organizations[0].key = 'Mirco'
        self.organizations[0].name = 'testing name'
        self.organizations[0].save()

        response = self.client.get(
            reverse('admin_metadata:organisation-autocomplete') + '?q={key}'.format(
                key=search_key
            )
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.organizations[0]))
        self.assertEqual(len(data['results']), 1)

    def test_organization_autocomplete_un_authorize_user(self):
        """ Verify Organization autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:organisation-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    def _make_user_non_staff(self):
        self.client.logout()
        self.user = UserFactory(is_staff=False)
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
コード例 #14
0
class AdminTests(SiteMixin, TestCase):
    """ Tests Admin page."""

    def setUp(self):
        super(AdminTests, self).setUp()
        self.user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.course_runs = factories.CourseRunFactory.create_batch(3)
        self.courses = [course_run.course for course_run in self.course_runs]

        self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0])
        self.program = factories.ProgramFactory(
            courses=self.courses, excluded_course_runs=[self.excluded_course_run]
        )

    def _post_data(self, status=ProgramStatus.Unpublished, marketing_slug='/foo'):
        return {
            'title': 'some test title',
            'courses': [self.courses[0].id],
            'type': self.program.type.id,
            'status': status,
            'marketing_slug': marketing_slug,
            'partner': self.program.partner.id
        }

    def assert_form_valid(self, data, files):
        form = ProgramAdminForm(data=data, files=files)
        self.assertTrue(form.is_valid())
        program = form.save()
        response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,)))
        self.assertEqual(response.status_code, 200)

    def assert_form_invalid(self, data, files):
        form = ProgramAdminForm(data=data, files=files)
        self.assertFalse(form.is_valid())
        self.assertEqual(
            form.errors['__all__'],
            ['Programs can only be activated if they have a banner image.']
        )
        with self.assertRaises(ValueError):
            form.save()

    def test_program_detail_form(self):
        """ Verify in admin panel program detail form load successfully. """
        response = self.client.get(reverse('admin:course_metadata_program_change', args=(self.program.id,)))
        self.assertEqual(response.status_code, 200)

    def test_custom_course_selection_page(self):
        """ Verify that course selection page loads successfully. """
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, reverse('admin:course_metadata_program_change', args=(self.program.id,)))
        self.assertContains(response, reverse('admin:course_metadata_program_changelist'))

    def test_custom_course_selection_page_with_invalid_id(self):
        """ Verify that course selection page will return 404 for invalid program id. """
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(10,)))
        self.assertEqual(response.status_code, 404)

    def test_custom_course_selection_page_with_non_staff(self):
        """ Verify that course selection page will return 404 for non authorized user. """
        self.client.logout()
        self.user.is_superuser = False
        self.user.is_staff = False
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        self.assertEqual(response.status_code, 404)

    def test_page_loads_only_course_related_runs(self):
        """ Verify that course selection page loads only all course runs. Also marked checkboxes with
        excluded courses runs only.
        """
        # add some new courses and course runs
        factories.CourseRunFactory.create_batch(2)
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        response_content = BeautifulSoup(response.content)
        attribute = response_content.find(
            "input", {"value": self.excluded_course_run.id, "type": "checkbox", "name": "excluded_course_runs"}
        )
        assert attribute is not None

        for run in self.course_runs:
            self.assertContains(response, run.key)

    def test_page_with_post_new_course_run(self):
        """ Verify that course selection page with posting the data. """

        self.assertEqual(1, self.program.excluded_course_runs.all().count())
        self.assertEqual(3, sum(1 for _ in self.program.course_runs))

        params = {
            'excluded_course_runs': [self.excluded_course_run.id, self.course_runs[0].id],
        }
        post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,))
        response = self.client.post(post_url, params)
        self.assertRedirects(
            response,
            expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)),
            status_code=302,
            target_status_code=200
        )
        self.assertEqual(2, self.program.excluded_course_runs.all().count())
        self.assertEqual(2, sum(1 for _ in self.program.course_runs))

    def test_page_with_post_without_course_run(self):
        """ Verify that course selection page without posting any selected excluded check run. """

        self.assertEqual(1, self.program.excluded_course_runs.all().count())
        params = {
            'excluded_course_runs': [],
        }
        post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,))
        response = self.client.post(post_url, params)
        self.assertRedirects(
            response,
            expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)),
            status_code=302,
            target_status_code=200
        )
        self.assertEqual(0, self.program.excluded_course_runs.all().count())
        self.assertEqual(4, sum(1 for _ in self.program.course_runs))
        response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,)))
        self.assertNotContains(response, '<input checked="checked")')

    @ddt.data(
        *itertools.product(
            (
                (False, False),
                (True, True)
            ),
            sorted(ProgramStatus.labels)  # We need a consistent ordering to distribute tests with pytest-xdist
        )
    )
    @ddt.unpack
    def test_program_activation_restrictions(self, booleans, label):
        """Verify that program activation requires both a marketing slug and a banner image."""
        has_banner_image, can_be_activated = booleans
        status = getattr(ProgramStatus, label)

        banner_image = make_image_file('test_banner.jpg') if has_banner_image else ''

        data = self._post_data(status=status, marketing_slug='/foo')
        files = {'banner_image': banner_image}

        if status == ProgramStatus.Active:
            if can_be_activated:
                # Transitioning to an active status should require a marketing slug and banner image.
                self.assert_form_valid(data, files)
            else:
                self.assert_form_invalid(data, files)
        else:
            # All other status transitions should be valid regardless of marketing slug and banner image.
            self.assert_form_valid(data, files)

    def test_new_program_without_courses(self):
        """ Verify that new program can be added without `courses`."""
        data = self._post_data()
        data['courses'] = []
        form = ProgramAdminForm(data)
        self.assertTrue(form.is_valid())
        program = form.save()
        self.assertEqual(0, program.courses.all().count())
        response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,)))
        self.assertEqual(response.status_code, 200)
コード例 #15
0
class CourseEditorsViewSetTests(SerializationMixin, APITestCase):
    list_path = reverse('api:v1:course_editor-list')

    def setUp(self):
        super(CourseEditorsViewSetTests, self).setUp()
        self.staff_user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=self.staff_user.username, password=USER_PASSWORD)
        self.user = UserFactory()
        partner = Partner.objects.first()
        self.course = CourseFactory(draft=True, partner=partner)
        self.org_ext = OrganizationExtensionFactory()
        self.course.authoring_organizations.add(self.org_ext.organization)  # pylint: disable=no-member

    def test_list(self):
        """Verify GET endpoint returns list of editors"""
        CourseEditorFactory()
        response = self.client.get(self.list_path)

        assert len(response.data['results']) == 1

        # Test for non staff user
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        response = self.client.get(self.list_path)

        self.assertFalse(response.data['results'])

    def test_course_query_param(self):
        """Verify GET endpoint with course query param returns editors relative to that course"""
        CourseEditorFactory(course=self.course)
        CourseEditorFactory()

        response = self.client.get(self.list_path)

        assert len(response.data['results']) == 2

        response = self.client.get(self.list_path, {'course': self.course.uuid})

        assert len(response.data['results']) == 1
        assert response.data['results'][0]['course'] == self.course.uuid

    @ddt.data(
        (True, True),  # Staff User on Draft Course
        (True, False),  # Staff User on Official Course
        (False, True),  # Non-staff User on Draft Course
        (False, False),  # Non-staff User on Official Course
    )
    @ddt.unpack
    def test_create_for_self_and_draft_course(self, is_staff, is_draft):
        """Verify can make self an editor. Test cases: as staff and non-staff, on official and draft course"""

        self.user.is_staff = is_staff
        self.user.save()
        partner = Partner.objects.first()
        course = CourseFactory(draft=is_draft, partner=partner)
        self.user.groups.add(self.org_ext.group)
        course.authoring_organizations.add(self.org_ext.organization)

        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.client.post(self.list_path, {'course': course.uuid}, format='json')
        course_editor = CourseEditor.objects.first()

        assert course_editor.course == course
        assert course_editor.user == self.user

    def test_create_for_self_as_non_staff_with_invalid_course(self):
        """Verify non staff user cannot make them self an editor of a course they dont belong to"""

        self.client.login(username=self.user.username, password=USER_PASSWORD)

        response = self.client.post(self.list_path, {'course': self.course.uuid}, format='json')

        assert response.status_code == 403

    def test_create_for_other_user_as_staff(self):
        """Verify staff user can make another user an editor"""

        self.user.groups.add(self.org_ext.group)
        self.client.post(self.list_path, {'course': self.course.uuid, 'user_id': self.user.id}, format='json')
        course_editor = CourseEditor.objects.first()

        assert course_editor.course == self.course
        assert course_editor.user == self.user

    def test_create_for_other_user_as_non_staff(self):
        """Verify non staff can make another user an editor"""

        user2 = UserFactory()

        self.user.groups.add(self.org_ext.group)
        user2.groups.add(self.org_ext.group)

        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.client.post(self.list_path, {'course': self.course.uuid, 'user_id': user2.id}, format='json')
        course_editor = CourseEditor.objects.first()

        assert course_editor.course == self.course
        assert course_editor.user == user2

    def test_create_for_invalid_other_user(self):
        """Verify a user can't be made an editor of a course if both are not under the same organization"""

        response = self.client.post(
            self.list_path, {'course': self.course.uuid, 'user_id': self.user.id}, format='json'
        )

        assert response.status_code == 403
コード例 #16
0
class AutoCompletePersonTests(mixins.APITestCase):
    """
    Tests for person autocomplete lookups
    """
    def setUp(self):
        super(AutoCompletePersonTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)

        first_instructor = PersonFactory(given_name="First",
                                         family_name="Instructor")
        second_instructor = PersonFactory(given_name="Second",
                                          family_name="Instructor")
        self.instructors = [first_instructor, second_instructor]

        self.organizations = OrganizationFactory.create_batch(3)
        self.organization_extensions = []

        for instructor in self.instructors:
            PositionFactory(organization=self.organizations[0],
                            title="professor",
                            person=instructor)

        for organization in self.organizations:
            org_ex = publisher_factories.OrganizationExtensionFactory(
                organization=organization)
            self.organization_extensions.append(org_ex)

        disco_course = CourseFactory(
            authoring_organizations=[self.organizations[0]])
        disco_course2 = CourseFactory(
            authoring_organizations=[self.organizations[1]])
        CourseRunFactory(course=disco_course, staff=[first_instructor])
        CourseRunFactory(course=disco_course2, staff=[second_instructor])

        self.user.groups.add(self.organization_extensions[0].group)

    def query(self, q):
        query_params = '?q={q}'.format(q=q)
        path = reverse('api:v1:person-search-typeahead')
        return self.client.get(path + query_params)

    def test_instructor_autocomplete(self):
        """ Verify instructor autocomplete returns the data. """
        response = self.query('ins')
        self._assert_response(response, 2)

        # update first instructor's name
        self.instructors[0].given_name = 'dummy_name'
        self.instructors[0].save()

        response = self.query('dummy')
        self._assert_response(response, 1)

    def test_instructor_autocomplete_non_staff_user(self):
        """ Verify instructor autocomplete works for non-staff users. """
        self._make_user_non_staff()
        response = self.query('dummy')
        self._assert_response(response, 0)

    def test_instructor_autocomplete_no_query_param(self):
        """ Verify instructor autocomplete returns bad response for request with no query. """
        self._make_user_non_staff()
        response = self.client.get(reverse('api:v1:person-search-typeahead'))
        self._assert_error_response(
            response,
            ["The 'q' querystring parameter is required for searching."], 400)

    def test_instructor_autocomplete_spaces(self):
        """ Verify instructor autocomplete allows spaces. """
        response = self.query('sec ins')
        self._assert_response(response, 1)

    def test_instructor_autocomplete_no_results(self):
        """ Verify instructor autocomplete correctly finds no matches if string doesn't match. """
        response = self.query('second nope')
        self._assert_response(response, 0)

    def test_instructor_autocomplete_last_name_first_name(self):
        """ Verify instructor autocomplete allows last name first. """
        response = self.query('instructor first')
        self._assert_response(response, 1)

    def test_instructor_position_in_label(self):
        """ Verify that instructor label contains position of instructor if it exists."""
        position_title = 'professor'

        response = self.query('ins')

        self.assertContains(response, position_title)

    def test_instructor_image_in_label(self):
        """ Verify that instructor label contains profile image url."""
        response = self.query('ins')
        self.assertContains(response,
                            self.instructors[0].get_profile_image_url)
        self.assertContains(response,
                            self.instructors[1].get_profile_image_url)

    def _assert_response(self, response, expected_length):
        """ Assert autocomplete response. """
        assert response.status_code == 200
        data = json.loads(response.content.decode('utf-8'))
        assert len(data) == expected_length

    def _assert_error_response(self,
                               response,
                               expected_response,
                               expected_response_code=200):
        """ Assert autocomplete response. """
        assert response.status_code == expected_response_code
        data = json.loads(response.content.decode('utf-8'))
        assert data == expected_response

    def test_instructor_autocomplete_with_uuid(self):
        """ Verify instructor autocomplete returns the data with valid uuid. """
        uuid = self.instructors[0].uuid
        response = self.query(uuid)
        self._assert_response(response, 1)

    def test_instructor_autocomplete_with_invalid_uuid(self):
        """ Verify instructor autocomplete returns empty list without giving error. """
        uuid = 'invalid-uuid'
        response = self.query(uuid)
        self._assert_response(response, 0)

    def test_instructor_autocomplete_without_staff_user(self):
        """ Verify instructor autocomplete returns the data if user is not staff. """
        non_staff_user = UserFactory()
        non_staff_user.groups.add(self.organization_extensions[0].group)
        self.client.logout()
        self.client.login(username=non_staff_user.username,
                          password=USER_PASSWORD)

        response = self.query('ins')
        self._assert_response(response, 2)

    def test_instructor_autocomplete_without_login(self):
        """ Verify instructor autocomplete returns a forbidden code if user is not logged in. """
        self.client.logout()
        person_autocomplete_url = reverse(
            'api:v1:person-search-typeahead') + '?q={q}'.format(
                q=self.instructors[0].uuid)

        response = self.client.get(person_autocomplete_url)
        self._assert_error_response(
            response,
            {'detail': 'Authentication credentials were not provided.'}, 401)

    def test_autocomplete_limit_by_org(self):
        org = self.organizations[0]
        person_autocomplete_url = reverse(
            'api:v1:person-search-typeahead') + '?q=ins'
        single_autocomplete_url = person_autocomplete_url + '&org={key}'.format(
            key=org.key)
        response = self.client.get(single_autocomplete_url)
        self._assert_response(response, 1)

        org2 = self.organizations[1]
        multiple_autocomplete_url = single_autocomplete_url + '&org={key}'.format(
            key=org2.key)
        response = self.client.get(multiple_autocomplete_url)
        self._assert_response(response, 2)

    def _make_user_non_staff(self):
        self.client.logout()
        self.user = UserFactory(is_staff=False)
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)
コード例 #17
0
ファイル: test_lookups.py プロジェクト: edx/course-discovery
class AutocompleteTests(TestCase):
    """ Tests for autocomplete lookups."""
    def setUp(self):
        super(AutocompleteTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.login(username=self.user.username, password=USER_PASSWORD)
        self.courses = factories.CourseFactory.create_batch(3, title='Some random course title')
        for course in self.courses:
            factories.CourseRunFactory(course=course)
        self.organizations = factories.OrganizationFactory.create_batch(3)

    @ddt.data('dum', 'ing')
    def test_course_autocomplete(self, search_key):
        """ Verify course autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:course-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)
        # update the first course title
        self.courses[0].key = 'edx/dummy/key'
        self.courses[0].title = 'this is some thing new'
        self.courses[0].save()
        response = self.client.get(
            reverse('admin_metadata:course-autocomplete') + '?q={title}'.format(title=search_key)
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.courses[0]))

    def test_course_autocomplete_un_authorize_user(self):
        """ Verify course autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:course-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('ing', 'dum')
    def test_course_run_autocomplete(self, search_key):
        """ Verify course run autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:course-run-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)
        # update the first course title
        course = self.courses[0]
        course.title = 'this is some thing new'
        course.save()
        course_run = self.courses[0].course_runs.first()
        course_run.key = 'edx/dummy/testrun'
        course_run.save()

        response = self.client.get(
            reverse('admin_metadata:course-run-autocomplete') + '?q={q}'.format(q=search_key)
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(course_run))

    def test_course_run_autocomplete_un_authorize_user(self):
        """ Verify course run autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:course-run-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('irc', 'ing')
    def test_organization_autocomplete(self, search_key):
        """ Verify Organization autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:organisation-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 3)

        self.organizations[0].key = 'Mirco'
        self.organizations[0].name = 'testing name'
        self.organizations[0].save()

        response = self.client.get(
            reverse('admin_metadata:organisation-autocomplete') + '?q={key}'.format(
                key=search_key
            )
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.organizations[0]))
        self.assertEqual(len(data['results']), 1)

    def test_organization_autocomplete_un_authorize_user(self):
        """ Verify Organization autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:organisation-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    @ddt.data('dummyurl', 'testing')
    def test_video_autocomplete(self, search_key):
        """ Verify video autocomplete returns the data. """
        response = self.client.get(reverse('admin_metadata:video-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(data['results']), 6)

        self.courses[0].video.src = 'http://www.youtube.com/dummyurl'
        self.courses[0].video.description = 'testing description'
        self.courses[0].video.save()

        response = self.client.get(
            reverse('admin_metadata:video-autocomplete') + '?q={key}'.format(
                key=search_key
            )
        )
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'][0]['text'], str(self.courses[0].video))
        self.assertEqual(len(data['results']), 1)

    def test_video_autocomplete_un_authorize_user(self):
        """ Verify video autocomplete returns empty list for un-authorized users. """
        self._make_user_non_staff()
        response = self.client.get(reverse('admin_metadata:video-autocomplete'))
        data = json.loads(response.content.decode('utf-8'))
        self.assertEqual(data['results'], [])

    def _make_user_non_staff(self):
        self.client.logout()
        self.user = UserFactory(is_staff=False)
        self.user.save()
        self.client.login(username=self.user.username, password=USER_PASSWORD)