def test_course_visibility(self, mock, fetch_mock): # pylint: disable=unused-argument """ Assert that an instructor can also see courses which are not live but which they own, in both course list and detail view. """ # Create an instructor instructor = User.objects.create_user(username="******", password="******") instructor.groups.add(Group.objects.get(name="Instructor")) self.client.login(username="******", password="******") not_live_course = CourseFactory.create(live=False) ModuleFactory.create(course=not_live_course, price_without_tax=1) not_live_owned_course = CourseFactory.create(live=False) ModuleFactory.create(course=not_live_owned_course, price_without_tax=3) instructor.courses_owned.add(not_live_owned_course) visibility_pairs = [(self.course.uuid, True), (not_live_course.uuid, False), (not_live_owned_course.uuid, True)] for course_uuid, _ in visibility_pairs: fetch_mock.get("{base}v1/coursexs/{course_uuid}/".format( base=FAKE_CCXCON_API, course_uuid=course_uuid, ), json={}) fetch_mock.get("{base}v1/coursexs/{course_uuid}/modules/".format( base=FAKE_CCXCON_API, course_uuid=course_uuid), json=[]) self.assert_course_visibility(visibility_pairs)
def test_see_own_courses_perm(self, mock, fetch_mock): # pylint: disable=unused-argument """ Assert that ownership isn't enough to see not live courses, you need the permission too. """ # Create an instructor not_live_course = CourseFactory.create(live=False) ModuleFactory.create(course=not_live_course, price_without_tax=1) not_live_owned_course = CourseFactory.create(live=False) ModuleFactory.create(course=not_live_owned_course, price_without_tax=3) self.teacher.courses_owned.add(not_live_owned_course) visibility_pairs = [(self.course.uuid, True), (not_live_course.uuid, False), (not_live_owned_course.uuid, False)] for course_uuid, _ in visibility_pairs: fetch_mock.get("{base}v1/coursexs/{course_uuid}/".format( base=FAKE_CCXCON_API, course_uuid=course_uuid, ), json={}) fetch_mock.get("{base}v1/coursexs/{course_uuid}/modules/".format( base=FAKE_CCXCON_API, course_uuid=course_uuid), json=[]) self.assert_course_visibility(visibility_pairs)
def test_not_logged_in(self): """ If the user is not logged in, return a 403. """ course = CourseFactory.create(live=True) ModuleFactory.create(price_without_tax=3, course=course) assert course.is_available_for_purchase resp = self.client.get(reverse('course-permissions', kwargs={'uuid': course.uuid})) assert resp.status_code == 403, resp.content.decode('utf-8')
def test_course_module_mismatch(self): """ Assert that we don't allow duplicate items in cart """ course2 = CourseFactory.create(live=True) ModuleFactory.create(price_without_tax=123, course=course2) with self.assertRaises(ValidationError) as ex: validate_cart([{ "uuids": [self.module.uuid], "seats": 10, "course_uuid": course2.uuid }], self.user) assert ex.exception.detail[0] == "Course does not match up with module"
def test_non_200_propagates_to_response(self, ccxcon_api): """ If ccx POST returns a non-200, the response will have this information. """ error_message = "This is an error" ccxcon_api.return_value.create_ccx.return_value = [ False, 500, error_message ] total_seats = 5 # Add an extra module to assert we only POST to ccx endpoint once per course. module2 = ModuleFactory.create(course=self.course, price_without_tax=123) cart_item = { "uuids": [self.module.uuid, module2.uuid], "seats": total_seats, "course_uuid": self.course.uuid } cart = [cart_item] total = calculate_cart_subtotal(cart) # Note: autospec intentionally not used, we need an unbound method here with patch.object(Charge, 'create') as create_mock: resp = self.client.post(reverse('checkout'), content_type='application/json', data=json.dumps({ "cart": cart, "token": "token", "total": float(total) })) assert resp.status_code == 500, resp.content.decode('utf-8') assert error_message in resp.content.decode('utf-8') assert create_mock.call_count == 1
def test_errors_propagate_to_response(self, ccxcon_api): """ If there are errors on checkout, they make it to the response. """ course2 = CourseFactory.create(live=True) module2 = ModuleFactory.create(course=course2, price_without_tax=345) ccxcon_api.return_value.create_ccx.side_effect = [ AttributeError("Example Error"), AttributeError("Another Error"), ] cart = [{ "uuids": [self.module.uuid], "seats": 5, "course_uuid": self.course.uuid }, { "uuids": [module2.uuid], "seats": 9, "course_uuid": module2.course.uuid }] with patch.object(Charge, 'create'): resp = self.client.post(reverse('checkout'), content_type='application/json', data=json.dumps({ "cart": cart, "token": "token", "total": float(calculate_cart_subtotal(cart)) })) assert resp.status_code == 500, resp.content.decode('utf-8') assert "Example Error" in resp.content.decode('utf-8') assert "Another Error" in resp.content.decode('utf-8')
def test_module_serializer(self): # pylint: disable=no-self-use """Assert behavior of ModuleSerializer""" module = ModuleFactory.create() assert dict(ModuleSerializer().to_representation(module)) == { "uuid": module.uuid, "title": module.title, "price_without_tax": float(module.price_without_tax) }
def test_module_str(self): # pylint: disable=no-self-use """ Assert str(Module) """ module = ModuleFactory.create() expected = "{title} ({uuid})".format(title=module.title, uuid=module.uuid) assert str(module) == expected
def test_module_course_mismatch(self): """Error if the module uuid doesn't match the course""" module2 = ModuleFactory.create() course_dict = { 'uuid': self.course.uuid, 'modules': [{ 'uuid': module2.uuid }] } self.assert_patch_validation( self.course.uuid, course_dict, "Unable to find module {}".format(module2.uuid))
def test_permissions(self): """ Test all possible combinations of permissions. """ User.objects.create_user(username="******", password="******") self.client.login(username="******", password="******") course = CourseFactory.create(live=True) ModuleFactory.create(price_without_tax=3, course=course) assert course.is_available_for_purchase def assert_permissions(edit_content, edit_liveness, edit_price, is_owner, see_not_live): """Mock and assert these set of permissions""" @patch.object(Helpers, 'is_owner', return_value=is_owner) @patch.object(Helpers, 'can_edit_own_content', return_value=edit_content) @patch.object(Helpers, 'can_edit_own_liveness', return_value=edit_liveness) @patch.object(Helpers, 'can_edit_own_price', return_value=edit_price) @patch.object(Helpers, 'can_see_own_not_live', return_value=see_not_live) def run_assert_permissions(*args): # pylint: disable=unused-argument """Something to attach our patch objects to so we don't indent each time""" resp = self.client.get( reverse('course-permissions', kwargs={"uuid": course.uuid}) ) assert resp.status_code == 200, resp.content.decode('utf-8') result = json.loads(resp.content.decode('utf-8')) assert result == { "is_owner": is_owner, EDIT_OWN_CONTENT[0]: edit_content, EDIT_OWN_PRICE[0]: edit_price, EDIT_OWN_LIVENESS[0]: edit_liveness, SEE_OWN_NOT_LIVE[0]: see_not_live, } run_assert_permissions() num_args = get_function_code(assert_permissions).co_argcount # pylint: disable=no-member args = [(True, False)] * num_args for tup in product(*args): assert_permissions(*tup)
def test_cart_total(self): """ Assert that the cart total is calculated correctly (seats * price) """ module2 = ModuleFactory.create(course=self.course, ) seats = 10 assert calculate_cart_subtotal([{ "uuids": [self.module.uuid, module2.uuid], "seats": seats, "course_uuid": self.course.uuid }]) == (self.module.price_without_tax * seats + module2.price_without_tax * seats)
def test_duplicate_courses(self): """ Assert that we don't allow duplicate courses in cart """ module2 = ModuleFactory.create(course=self.course) with self.assertRaises(ValidationError) as ex: validate_cart([{ "uuids": [self.module.uuid], "seats": 10, "course_uuid": self.course.uuid }, { "uuids": [module2.uuid], "seats": 15, "course_uuid": self.course.uuid }], self.user) assert ex.exception.detail[0] == "Duplicate course in cart"
def test_order_course(self): """ Assert that an order is created in the database """ # Create second module to test cart with multiple items title = "other product title" second_price = 345 second_course = CourseFactory.create(live=True) second_module = ModuleFactory.create(course=second_course, title=title, price_without_tax=second_price) first_seats = 5 second_seats = 10 order = create_order([ { "uuids": [self.module.uuid], "seats": first_seats, "course_uuid": self.course.uuid }, { "uuids": [second_module.uuid], "seats": second_seats, "course_uuid": second_module.course.uuid }, ], self.user) first_line_total = self.module.price_without_tax * first_seats second_line_total = second_price * second_seats total = first_line_total + second_line_total assert order.purchaser == self.user assert order.total_paid == total assert order.subtotal == total assert order.orderline_set.count() == 2 first_line = order.orderline_set.get(module=self.module) assert first_line.line_total == first_line_total assert first_line.price_without_tax == self.module.price_without_tax assert first_line.seats == first_seats second_line = order.orderline_set.get(module=second_module) assert second_line.line_total == second_line_total assert second_line.price_without_tax == second_price assert second_line.seats == second_seats
def test_course_serializer(self): # pylint: disable=no-self-use """Assert behavior of CourseSerializer""" course = CourseFactory.create() module = ModuleFactory.create(course=course) rep = CourseSerializer(course).data assert isinstance(rep['instructors'], list) assert dict(rep) == { "uuid": course.uuid, "title": course.title, "author_name": course.author_name, "overview": course.overview, "description": course.description, "image_url": course.image_url, "edx_instance": course.instance.instance_url, "instructors": course.instructors, "course_id": course.course_id, "live": course.live, "modules": [ModuleSerializer().to_representation(module)] }
def test_price_one_module(self): """ Make sure we allow patching only the modules we care about. """ # Get dict when we have one module, then create a second one module2 = ModuleFactory.create(course=self.course) second_old_price = module2.price_without_tax new_price = '4' course_dict = self.patch_dict_for_price(new_price) assert self.module.price_without_tax != new_price resp = self.client.patch(reverse('course-detail', kwargs={"uuid": self.course.uuid}), content_type="application/json", data=json.dumps(course_dict)) assert resp.status_code == 200, resp.content.decode('utf-8') self.module.refresh_from_db() assert self.module.price_without_tax == Decimal(new_price) assert module2.price_without_tax == second_old_price
def test_ccx_creation(self, ccxcon_api): """ Assert that CCX is created, on successful checkout. """ total_seats = 5 # Add an extra module to assert we only POST to ccx endpoint once per course. module2 = ModuleFactory.create(course=self.course, price_without_tax=123) ccxcon_api.return_value.create_ccx.return_value = (True, 200, '') cart_item = { "uuids": [self.module.uuid, module2.uuid], "seats": total_seats, "course_uuid": self.course.uuid } cart = [cart_item] total = calculate_cart_subtotal(cart) # Note: autospec intentionally not used, we need an unbound method here with patch.object(Charge, 'create') as create_mock: resp = self.client.post(reverse('checkout'), content_type='application/json', data=json.dumps({ "cart": cart, "token": "token", "total": float(total) })) assert resp.status_code == 200, resp.content.decode('utf-8') assert create_mock.call_count == 1 assert ccxcon_api.return_value.create_ccx.call_count == 1 ccxcon_api.return_value.create_ccx.assert_called_with( self.course.uuid, self.user.email, total_seats, self.course.title, course_modules=[self.module.uuid, module2.uuid], )
def test_total_is_a_number_string(self): # pylint: disable=unused-argument """ Assert that the total can be a string with a number representation """ total_seats = 5 # Add an extra module to assert we only POST to ccx endpoint once per course. module2 = ModuleFactory.create(course=self.course, price_without_tax=123) cart_item = { "uuids": [self.module.uuid, module2.uuid], "seats": total_seats, "course_uuid": self.course.uuid } cart = [cart_item] total = calculate_cart_subtotal(cart) # Note: autospec intentionally not used, we need an unbound method here with patch.object(Charge, 'create') as create_mock: with patch( 'portal.views.checkout_api.CheckoutView.notify_external_services' ) as notify_mock: resp = self.client.post(reverse('checkout'), content_type='application/json', data=json.dumps({ "cart": cart, "token": "token", "total": str(float(total)) })) assert resp.status_code == 200, resp.content.decode('utf-8') assert create_mock.call_count == 1 assert notify_mock.call_count == 1
def setUp(self): """ Create a course and module which aren't live. """ self.course = CourseFactory.create(live=False) self.module = ModuleFactory.create(course=self.course)