def test_get_viewers(self): """ Verify the method returns a QuerySet of individuals with explicit permission to view a Catalog. """ catalog = self.catalog self.assertFalse(catalog.viewers.exists()) # pylint:disable=no-member user = UserFactory() user.add_obj_perm(Catalog.VIEW_PERMISSION, catalog) self.assertListEqual(list(catalog.viewers), [user])
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()
def setUp(self): super(UpdateCommentTests, self).setUp() self.user = UserFactory.create() self.comment = CommentFactory.create(user=self.user) self.path = reverse('publisher_comments:api:comments', kwargs={'pk': self.comment.id}) self.data = {'comment': 'updated comment'}
def test_update_without_editing_permission(self): """ Verify that non owner user of the comment can not edit. """ dummy_user = UserFactory.create() self.client.login(username=dummy_user.username, password=USER_PASSWORD) response = self.client.patch(self.path, json.dumps(self.data), JSON_CONTENT_TYPE) self.assertEqual(response.status_code, 403)
def test_retrieve_permissions(self): """ Verify only users with the correct permissions can create, read, or modify a Catalog. """ # Use an unprivileged user user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) # A user with no permissions should NOT be able to view a Catalog. self.assertFalse(user.has_perm('catalogs.view_catalog', self.catalog)) response = self.client.get(url) self.assertEqual(response.status_code, 403) # The permitted user should be able to view the Catalog. self.grant_catalog_permission_to_user(user, 'view') response = self.client.get(url) self.assertEqual(response.status_code, 200)
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)
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 test_is_owner_permission(self): """ If object.user matches request.user, return True. """ # users has access to their own objects request = self._make_request(user=self.user, data={'comment': 'update_comment'}) self.assertTrue(self.permissions_class.has_object_permission(request, None, self.comment)) # users CANNOT have access to object of other users user = UserFactory.create() request = self._make_request(user=user, data={'username': '******'}) self.assertFalse(self.permissions_class.has_object_permission(request, None, self.comment))
def test_get_full_name(self): """ Test that the user model concatenates first and last name if the full name is not set. """ full_name = "George Costanza" user = UserFactory(full_name=full_name) self.assertEqual(user.get_full_name(), full_name) first_name = "Jerry" last_name = "Seinfeld" user = UserFactory(full_name=None, first_name=first_name, last_name=last_name) expected = "{first_name} {last_name}".format(first_name=first_name, last_name=last_name) self.assertEqual(user.get_full_name(), expected) user = UserFactory(full_name=full_name, first_name=first_name, last_name=last_name) self.assertEqual(user.get_full_name(), full_name)
def test_set_viewers(self): """ Verify the method updates the set of users with permission to view a Catalog. """ users = UserFactory.create_batch(2) permission = 'catalogs.' + Catalog.VIEW_PERMISSION for user in users: self.assertFalse(user.has_perm(permission, self.catalog)) # Verify a list of users can be added as viewers self.catalog.viewers = users for user in users: self.assertTrue(user.has_perm(permission, self.catalog)) # Verify existing users, not in the list, have their access revoked. permitted = users[0] revoked = users[1] self.catalog.viewers = [permitted] self.assertTrue(permitted.has_perm(permission, self.catalog)) self.assertFalse(revoked.has_perm(permission, self.catalog)) # Verify all users have their access revoked when passing in an empty list self.catalog.viewers = [] for user in users: self.assertFalse(user.has_perm(permission, self.catalog))
def setUp(self): super(IsOwnerTests, self).setUp() self.permissions_class = IsOwner() self.user = UserFactory.create() self.comment = CommentFactory.create(user=self.user, comment='test comment')
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 setUp(self): super(UserTests, self).setUp() self.user = UserFactory()
def setUp(self): super(PublisherUtilsTests, self).setUp() self.user = UserFactory()
def setUp(self): super().setUp() self.service_user = UserFactory(username=self.SERVICE_USERNAME) self.url = reverse("api:v1:replace_usernames")
def setUp(self): super(JournalViewSetTests, self).setUp() self.user = UserFactory(is_staff=True) self.journal = JournalFactory(uuid=uuid.uuid4()) self.client.login(username=self.user.username, password=USER_PASSWORD) self.journal_url = reverse('journal:api:v1:journal-list')
def setUp(self): super().setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_get_comments_for_course_case_id_set(self): user = UserFactory() query_path = self.salesforce_util_path + '._query' course = CourseFactoryNoSignals(partner=self.salesforce_config.partner, salesforce_case_id='TestSalesforceId') return_value = { 'records': [ { 'CreatedBy': { 'Username': user.username }, 'CreatedDate': '2000-01-01', 'Body': ('[User]\n{}\n\n' '[Course Run]\ncourse-v1:testX+TestX+Test\n\n' '[Body]\nThis is a formatted test message.').format( user.username), }, { 'CreatedBy': { 'Username': '******' }, 'CreatedDate': '2000-01-01', 'Body': 'This is an internal user comment without formatting.' }, ] } with mock.patch(self.salesforce_path): with mock.patch(query_path, return_value=return_value) as mock_query: util = SalesforceUtil(self.salesforce_config.partner) comments = util.get_comments_for_course(course) mock_query.assert_called_with( "SELECT CreatedDate,Body,CreatedBy.Username,CreatedBy.Email,CreatedBy.FirstName,CreatedBy.LastName " "FROM FeedItem WHERE ParentId='{}' AND IsDeleted=FALSE ORDER BY CreatedDate ASC" .format(course.salesforce_case_id)) self.assertEqual(comments, [ { 'user': { 'username': user.username, 'email': user.email, 'first_name': user.first_name, 'last_name': user.last_name, }, 'course_run_key': 'course-v1:testX+TestX+Test', 'comment': 'This is a formatted test message.', 'created': '2000-01-01', }, { 'user': { 'username': '******', 'email': None, 'first_name': None, 'last_name': None, }, 'course_run_key': None, 'comment': 'This is an internal user comment without formatting.', 'created': '2000-01-01', }, ])
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) self.courses = publisher_factories.CourseFactory.create_batch( 3, title='Some random course title') 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) self.course_runs = [ publisher_factories.CourseRunFactory(course=course) for course in self.courses ] 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)
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 setUp(self): super(LoginMixin, self).setUp() self.user = UserFactory() self.client.login(username=self.user.username, password=USER_PASSWORD) if getattr(self, 'request'): self.request.user = self.user
def setUp(self): super(LoginMixin, self).setUp() self.user = UserFactory() self.client.login(username=self.user.username, password=USER_PASSWORD)
def setUp(self): super().setUp() self.user = UserFactory()
def setUp(self): super(CourseViewSetTests, self).setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.course = CourseFactory()
def setUp(self): super(OrganizationExtensionAdminTests, self).setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.admin_page_url = reverse( 'admin:publisher_organizationextension_add')
def test_course_form(self): """ Verify that UserModelChoiceField returns `full_name` as choice label. """ user = UserFactory(username='******', full_name='Test Full Name') self._assert_choice_label(user.full_name)
class CommentViewSetTests(OAuth2Mixin, APITestCase): @factory.django.mute_signals(m2m_changed) def setUp(self): super(CommentViewSetTests, self).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) # pylint: disable=no-member 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)
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)
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 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), (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_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)
def test_authenticated(self): """ Verify the view raises `PermissionDenied` if the request is authenticated. """ user = UserFactory() self.request.user = user self.assertRaises(PermissionDenied, api_docs_permission_denied_handler, self.request)
class CourseEditorsViewSetTests(SerializationMixin, APITestCase): list_path = reverse('api:v1:course_editor-list') def setUp(self): super().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) 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) assert not 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
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_2 = CourseRunFactory(course__key='Test+Course', course__partner=self.partner) self.draft_course = CourseFactory(partner=self.partner, draft=True) self.draft_course_run = CourseRunFactory(course=self.draft_course, draft=True) self.draft_course_run.course.authoring_organizations.add( OrganizationFactory(key='course-id')) 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(17): 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(17): 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.draft_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.everything.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 self.assertTrue(new_course_run.draft) @ddt.data(True, False, "bogus") @responses.activate def test_create_draft_ignored(self, draft): """ Verify the endpoint supports creating a course_run, but always as a draft. """ course = self.draft_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 minimum + draft: True/False/bogus response = self.client.post(url, { 'course': course.key, 'start': '2000-01-01T00:00:00Z', 'end': '2001-01-01T00:00:00Z', 'draft': draft, }, format='json') self.assertEqual(response.status_code, 201) new_course_run = CourseRun.everything.get(key=new_key) self.assertDictEqual(response.data, self.serialize_course_run(new_course_run)) self.assertTrue(new_course_run.draft) @responses.activate def test_create_with_key(self): """ Verify the endpoint supports creating a course_run when specifying a key (if allowed). """ course = self.draft_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.everything.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.everything.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.draft_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 def test_create_fails_with_missing_fields(self): course = self.draft_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.'], }) @responses.activate def test_update_operates_on_drafts(self): self.assertFalse( CourseRun.everything.filter(key=self.course_run.key, draft=True).exists()) # sanity check self.mock_patch_to_studio(self.course_run.key) expected_original_max_effort = self.course_run.max_effort url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.patch(url, {'max_effort': 777}, format='json') self.assertEqual(response.status_code, 200) course_run = CourseRun.everything.get(key=self.course_run.key, draft=True) self.assertEqual(course_run.max_effort, 777) self.course_run.refresh_from_db() self.assertFalse(self.course_run.draft) self.assertEqual(self.course_run.max_effort, expected_original_max_effort) @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.draft_course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_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.draft_course_run.refresh_from_db() assert self.draft_course_run.max_effort == expected_max_effort assert self.draft_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.draft_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.draft_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.draft_course_run.key}) response = self.client.patch(url, {}, format='json') assert response.status_code == 404 @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.draft_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.draft_course_run.key) p1 = PersonFactory() p2 = PersonFactory() PersonFactory() url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_course_run.key}) response = self.client.patch(url, {'staff': [p2.uuid, p1.uuid]}, format='json') self.assertEqual(response.status_code, 200) self.draft_course_run.refresh_from_db() self.assertListEqual(list(self.draft_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.draft_course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_course_run.key}) response = self.client.patch( url, {'video': { 'src': 'https://example.com/blarg' }}, format='json') self.assertEqual(response.status_code, 200) self.draft_course_run.refresh_from_db() self.assertEqual(self.draft_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.draft_course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_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, 404) # Add as editor org_ext = OrganizationExtensionFactory( organization=self.draft_course_run.course.authoring_organizations. first()) self.user.groups.add(org_ext.group) CourseEditorFactory(user=self.user, course=self.draft_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.draft_course_run.key, status=400) url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_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.draft_course_run.refresh_from_db() self.assertEqual(self.draft_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.draft_course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_course_run.key}) response = self.client.put( url, { 'course': self.draft_course_run.course. key, # required, so we need for a put 'start': self.draft_course_run.start, # required, so we need for a put 'end': self.draft_course_run.end, # required, so we need for a put 'title': 'New Title', }, format='json') self.assertEqual(response.status_code, 200) self.draft_course_run.refresh_from_db() self.assertEqual(self.draft_course_run.title_override, 'New Title') @ddt.data( CourseRunStatus.LegalReview, CourseRunStatus.InternalReview, ) def test_patch_put_restrict_when_reviewing(self, status): self.draft_course_run.status = status self.draft_course_run.save() url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_course_run.key}) response = self.client.put( url, { 'course': self.draft_course_run.course. key, # required, so we need for a put 'start': self.draft_course_run.start, # required, so we need for a put 'end': self.draft_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.draft_course_run.key) self.draft_course_run.status = CourseRunStatus.Reviewed self.draft_course_run.save() url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_course_run.key}) response = self.client.put( url, { 'course': self.draft_course_run.course. key, # required, so we need for a put 'start': self.draft_course_run.start, # required, so we need for a put 'end': self.draft_course_run.end, # required, so we need for a put 'status': 'reviewed', }, format='json') assert response.status_code == 200 self.draft_course_run.refresh_from_db() draft_course_run = CourseRun.everything.get( key=self.draft_course_run.key, draft=True) assert draft_course_run.status == CourseRunStatus.Unpublished @ddt.data( CourseRunStatus.Unpublished, CourseRunStatus.Reviewed, ) @responses.activate def test_patch_put_draft_false(self, status): """ Verify that setting draft to False moves status to LegalReview. """ self.mock_patch_to_studio(self.draft_course_run.key) self.draft_course_run.status = status self.draft_course_run.save() url = reverse('api:v1:course_run-detail', kwargs={'key': self.draft_course_run.key}) response = self.client.put( url, { 'course': self.draft_course_run.course. key, # required, so we need for a put 'start': self.draft_course_run.start, # required, so we need for a put 'end': self.draft_course_run.end, # required, so we need for a put 'draft': False, }, format='json') assert response.status_code == 200, "Status {}: {}".format( response.status_code, response.content) draft_course_run = CourseRun.everything.get( key=self.draft_course_run.key, draft=True) assert draft_course_run.status == CourseRunStatus.LegalReview 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(14): 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 }, { 'display_name': self.draft_course_run.level_type.name, 'value': self.draft_course_run.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) def test_editable_list_gives_drafts(self): # We delete self.course_run_2 and self.draft_course_run here so we can test that specifically # draft and extra are the only ones showing up. self.course_run_2.delete() self.draft_course_run.delete() draft = CourseRunFactory(course__partner=self.partner, uuid=self.course_run.uuid, key=self.course_run.key, draft=True) self.course_run.draft_version = draft self.course_run.save() extra = CourseRunFactory(course__partner=self.partner) response = self.client.get( reverse('api:v1:course_run-list') + '?editable=1') actual_sorted = sorted(response.data['results'], key=lambda course_run: course_run['key']) expected_sorted = sorted(self.serialize_course_run([draft, extra], many=True), key=lambda course_run: course_run['key']) self.assertEqual(response.status_code, 200) self.assertEqual(actual_sorted, expected_sorted) def test_editable_get_gives_drafts(self): draft = CourseRunFactory(course__partner=self.partner, uuid=self.course_run.uuid, key=self.course_run.key, draft=True) self.course_run.draft_version = draft self.course_run.save() extra = CourseRunFactory(course__partner=self.partner) response = self.client.get( reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) + '?editable=1') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course_run(draft, many=False)) response = self.client.get( reverse('api:v1:course_run-detail', kwargs={'key': extra.key}) + '?editable=1') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course_run(extra, many=False)) def test_list_query_with_editable_raises_exception(self): """ Verify the endpoint raises an exception if both a q param and editable=1 are passed in """ query = 'title:Some random title' url = '{root}?q={query}&editable=1'.format( root=reverse('api:v1:course_run-list'), query=query) with pytest.raises(EditableAndQUnsupported) as exc: self.client.get(url) self.assertEqual( str(exc.value), 'Specifying both editable=1 and a q parameter is not supported.')
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)
def test_team_admin_without_full_name(self): """ Verify that UserModelChoiceField returns `username` if `full_name` is empty. """ user = UserFactory(username='******', full_name='', first_name='', last_name='') self._assert_choice_label(user.username)
def setUp(self): super(SendForReviewEmailTests, self).setUp() self.user = UserFactory() self.course_state = factories.CourseStateFactory()
def setUp(self): super(JournalBundleViewSetTests, self).setUp() self.user = UserFactory(is_staff=True) self.journal_bundle = JournalBundleFactory() self.client.login(username=self.user.username, password=USER_PASSWORD) self.journal_bundle_url = reverse('journal:api:v1:journal_bundle-list')
def setUp(self): super(CourseMarkAsReviewedEmailTests, self).setUp() self.user = UserFactory() self.course_state = factories.CourseStateFactory()
def setUp(self): super(ManagementCommandViewTestMixin, self).setUp() self.superuser = UserFactory(is_superuser=True) self.client.force_authenticate(self.superuser) # pylint: disable=no-member