From 84202da895169f28b676213b475d0747ff5b0ffa Mon Sep 17 00:00:00 2001 From: KyryloKireiev Date: Wed, 17 Apr 2024 18:22:23 +0300 Subject: [PATCH] test: [AXM-229] Improve test coverage --- .../mobile_api/course_info/serializers.py | 26 ++-- .../mobile_api/tests/test_serializers.py | 145 +++++++++++++++--- lms/djangoapps/mobile_api/tests/test_views.py | 74 +++++++-- 3 files changed, 194 insertions(+), 51 deletions(-) diff --git a/lms/djangoapps/mobile_api/course_info/serializers.py b/lms/djangoapps/mobile_api/course_info/serializers.py index 2dfa5dde67a1..b2bb0ce24701 100644 --- a/lms/djangoapps/mobile_api/course_info/serializers.py +++ b/lms/djangoapps/mobile_api/course_info/serializers.py @@ -52,12 +52,12 @@ class Meta: @staticmethod def get_media(obj): """ - Return course images in the correct forrmat. + Return course images in the correct format. """ return {'image': obj.image_urls} def get_course_sharing_utm_parameters(self, obj): - return get_encoded_course_sharing_utm_params() + return get_encoded_course_sharing_utm_params() def get_course_about_url(self, course_overview): return get_link_for_about_page(course_overview) @@ -87,15 +87,14 @@ class Meta: lookup_field = 'username' - class CourseAccessSerializer(serializers.Serializer): """ Get info whether a user should be able to view course material. """ - has_unmet_prerequisites = serializers.SerializerMethodField(method_name="get_has_unmet_prerequisites") - is_too_early = serializers.SerializerMethodField(method_name="get_is_too_early") - is_staff = serializers.SerializerMethodField(method_name="get_is_staff") + has_unmet_prerequisites = serializers.SerializerMethodField(method_name='get_has_unmet_prerequisites') + is_too_early = serializers.SerializerMethodField(method_name='get_is_too_early') + is_staff = serializers.SerializerMethodField(method_name='get_is_staff') audit_access_expires = serializers.SerializerMethodField() courseware_access = serializers.SerializerMethodField() @@ -103,19 +102,20 @@ def get_has_unmet_prerequisites(self, data: dict) -> bool: """ Check whether or not a course has unmet prerequisites. """ - return any(get_pre_requisite_courses_not_completed(data.get("user"), [data.get("course_id")])) + return any(get_pre_requisite_courses_not_completed(data.get('user'), [data.get('course_id')])) def get_is_too_early(self, data: dict) -> bool: """ Determine if the course is open to a learner (course has started or user has early beta access). """ - return not check_course_open_for_learner(data.get("user"), data.get("course")) + return not check_course_open_for_learner(data.get('user'), data.get('course')) def get_is_staff(self, data: dict) -> bool: """ Determine whether a user has staff access to this course. """ - return any(administrative_accesses_to_course_for_user(data.get("user"), data.get("course_id"))) + return any(administrative_accesses_to_course_for_user(data.get('user'), data.get('course_id'))) + def get_audit_access_expires(self, data: dict) -> Union[str, None]: """ Returns expiration date for a course audit expiration, if any or null @@ -126,10 +126,4 @@ def get_courseware_access(self, data: dict) -> dict: """ Determine if the learner has access to the course, otherwise show error message. """ - return has_access( - data.get('user'), - 'load_mobile', - data.get('course') - ).to_json() - - + return has_access(data.get('user'), 'load_mobile', data.get('course')).to_json() diff --git a/lms/djangoapps/mobile_api/tests/test_serializers.py b/lms/djangoapps/mobile_api/tests/test_serializers.py index 3ac1a5db517e..6c50f68d6811 100644 --- a/lms/djangoapps/mobile_api/tests/test_serializers.py +++ b/lms/djangoapps/mobile_api/tests/test_serializers.py @@ -3,21 +3,23 @@ """ import ddt -from mock import patch from django.test import TestCase +from mock import MagicMock, Mock, patch +from typing import Dict, List, Tuple, Union -from common.djangoapps.student.tests.factories import ( - UserFactory, +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.mobile_api.course_info.serializers import ( + CourseAccessSerializer, + CourseInfoOverviewSerializer, ) -from openedx.core.djangoapps.content.course_overviews.tests.factories import ( - CourseOverviewFactory, -) -from lms.djangoapps.mobile_api.course_info.serializers import CourseAccessSerializer +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory @ddt.ddt class TestCourseAccessSerializer(TestCase): - """Tests for the CourseAccessSerializer""" + """ + Tests for the CourseAccessSerializer. + """ def setUp(self): super().setUp() @@ -25,20 +27,27 @@ def setUp(self): self.course = CourseOverviewFactory() @ddt.data( - ([{"course_id": {}}], True), + ([{'course_id': {}}], True), ([], False), ) @ddt.unpack @patch('lms.djangoapps.mobile_api.course_info.serializers.get_pre_requisite_courses_not_completed') - def test_has_unmet_prerequisites(self, mock_return_value, has_unmet_prerequisites, mock_get_prerequisites): + def test_has_unmet_prerequisites( + self, + mock_return_value: List[Dict], + has_unmet_prerequisites: bool, + mock_get_prerequisites: MagicMock, + ) -> None: mock_get_prerequisites.return_value = mock_return_value + output_data = CourseAccessSerializer({ - "user": self.user, - "course": self.course, - "course_id": self.course.id, + 'user': self.user, + 'course': self.course, + 'course_id': self.course.id, }).data - self.assertEqual(output_data['hasUnmetPrerequisites'], has_unmet_prerequisites) + self.assertEqual(output_data['has_unmet_prerequisites'], has_unmet_prerequisites) + mock_get_prerequisites.assert_called_once_with(self.user, [self.course.id]) @ddt.data( (True, False), @@ -46,15 +55,22 @@ def test_has_unmet_prerequisites(self, mock_return_value, has_unmet_prerequisite ) @ddt.unpack @patch('lms.djangoapps.mobile_api.course_info.serializers.check_course_open_for_learner') - def test_is_too_early(self, mock_return_value, is_too_early, mock_check_course_open): + def test_is_too_early( + self, + mock_return_value: bool, + is_too_early: bool, + mock_check_course_open: MagicMock, + ) -> None: mock_check_course_open.return_value = mock_return_value + output_data = CourseAccessSerializer({ - "user": self.user, - "course": self.course, - "course_id": self.course.id + 'user': self.user, + 'course': self.course, + 'course_id': self.course.id }).data - self.assertEqual(output_data['isTooEarly'], is_too_early) + self.assertEqual(output_data['is_too_early'], is_too_early) + mock_check_course_open.assert_called_once_with(self.user, self.course) @ddt.data( ((False, False, False), False), @@ -63,12 +79,93 @@ def test_is_too_early(self, mock_return_value, is_too_early, mock_check_course_o ) @ddt.unpack @patch('lms.djangoapps.mobile_api.course_info.serializers.administrative_accesses_to_course_for_user') - def test_is_staff(self, mock_return_value, is_staff, mock_administrative_access): + def test_is_staff( + self, + mock_return_value: Tuple[bool], + is_staff: bool, + mock_administrative_access: MagicMock, + ) -> None: mock_administrative_access.return_value = mock_return_value + + output_data = CourseAccessSerializer({ + 'user': self.user, + 'course': self.course, + 'course_id': self.course.id + }).data + + self.assertEqual(output_data['is_staff'], is_staff) + mock_administrative_access.assert_called_once_with(self.user, self.course.id) + + @ddt.data(None, 'mocked_user_course_expiration_date') + @patch('lms.djangoapps.mobile_api.course_info.serializers.get_user_course_expiration_date') + def test_get_audit_access_expires( + self, + mock_return_value: Union[str, None], + mock_get_user_course_expiration_date: MagicMock, + ) -> None: + mock_get_user_course_expiration_date.return_value = mock_return_value + + output_data = CourseAccessSerializer({ + 'user': self.user, + 'course': self.course, + 'course_id': self.course.id + }).data + + self.assertEqual(output_data['audit_access_expires'], mock_return_value) + mock_get_user_course_expiration_date.assert_called_once_with(self.user, self.course) + + @patch('lms.djangoapps.mobile_api.course_info.serializers.has_access') + def test_get_courseware_access(self, mock_has_access: MagicMock) -> None: + mocked_access = { + 'has_access': True, + 'error_code': None, + 'developer_message': None, + 'user_message': None, + 'additional_context_user_message': None, + 'user_fragment': None + } + mock_has_access.return_value = Mock(to_json=Mock(return_value=mocked_access)) + output_data = CourseAccessSerializer({ - "user": self.user, - "course": self.course, - "course_id": self.course.id + 'user': self.user, + 'course': self.course, + 'course_id': self.course.id }).data - self.assertEqual(output_data['isStaff'], is_staff) + self.assertDictEqual(output_data['courseware_access'], mocked_access) + mock_has_access.assert_called_once_with(self.user, 'load_mobile', self.course) + mock_has_access.return_value.to_json.assert_called_once_with() + + +class TestCourseInfoOverviewSerializer(TestCase): + """ + Tests for the CourseInfoOverviewSerializer. + """ + + def setUp(self): + super().setUp() + self.user = UserFactory() + self.course_overview = CourseOverviewFactory() + + def test_get_media(self): + output_data = CourseInfoOverviewSerializer(self.course_overview, context={'user': self.user}).data + + self.assertIn('media', output_data) + self.assertIn('image', output_data['media']) + self.assertIn('raw', output_data['media']['image']) + self.assertIn('small', output_data['media']['image']) + self.assertIn('large', output_data['media']['image']) + + @patch('lms.djangoapps.mobile_api.course_info.serializers.get_link_for_about_page', return_value='mock_about_link') + def test_get_course_sharing_utm_parameters(self, mock_get_link_for_about_page: MagicMock) -> None: + output_data = CourseInfoOverviewSerializer(self.course_overview, context={'user': self.user}).data + + self.assertEqual(output_data['course_about'], mock_get_link_for_about_page.return_value) + mock_get_link_for_about_page.assert_called_once_with(self.course_overview) + + def test_get_course_modes(self): + expected_course_modes = [{'slug': 'audit', 'sku': None, 'android_sku': None, 'ios_sku': None, 'min_price': 0}] + + output_data = CourseInfoOverviewSerializer(self.course_overview, context={'user': self.user}).data + + self.assertListEqual(output_data['course_modes'], expected_course_modes) diff --git a/lms/djangoapps/mobile_api/tests/test_views.py b/lms/djangoapps/mobile_api/tests/test_views.py index 41ba6e54f8cd..67d2c79f9017 100644 --- a/lms/djangoapps/mobile_api/tests/test_views.py +++ b/lms/djangoapps/mobile_api/tests/test_views.py @@ -12,12 +12,15 @@ from edx_toggles.toggles.testutils import override_waffle_flag from milestones.tests.utils import MilestonesTestCaseMixin from mock import patch +from rest_framework import status from common.djangoapps.student.tests.factories import UserFactory # pylint: disable=unused-import +from common.djangoapps.util.course import get_link_for_about_page from lms.djangoapps.mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin from lms.djangoapps.mobile_api.utils import API_V1, API_V05 from lms.djangoapps.mobile_api.course_info.views import BlocksInfoInCourseView from lms.djangoapps.course_api.blocks.tests.test_views import TestBlocksInCourseView +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.features.course_experience import ENABLE_COURSE_GOALS from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order @@ -263,7 +266,7 @@ def test_flag_disabled(self, mock_logger): @ddt.ddt -class TestBlocksInfoInCourseView(TestBlocksInCourseView): # lint-amnesty, pylint: disable=test-inherits-tests +class TestBlocksInfoInCourseView(TestBlocksInCourseView, MilestonesTestCaseMixin): # lint-amnesty, pylint: disable=test-inherits-tests """ Test class for BlocksInfoInCourseView """ @@ -277,14 +280,14 @@ def setUp(self): self.student_user = UserFactory.create(username="student_user") @ddt.data( - ("anonymous", None, None), - ("staff", "student_user", "student_user"), - ("student", "student_user", "student_user"), - ("student", None, "student_user"), - ("student", "other_student", None), + ('anonymous', None, None), + ('staff', 'student_user', 'student_user'), + ('student', 'student_user', 'student_user'), + ('student', None, 'student_user'), + ('student', 'other_student', None), ) @ddt.unpack - @patch("lms.djangoapps.mobile_api.course_info.views.User.objects.get") + @patch('lms.djangoapps.mobile_api.course_info.views.User.objects.get') def test_get_requested_user(self, user_role, username, expected_username, mock_get): """ Test get_requested_user utility from the BlocksInfoInCourseView. @@ -294,16 +297,16 @@ def test_get_requested_user(self, user_role, username, expected_username, mock_g username: username query parameter from the request. expected_username: username of the returned user. """ - if user_role == "anonymous": + if user_role == 'anonymous': request_user = AnonymousUser() - elif user_role == "staff": + elif user_role == 'staff': request_user = self.admin_user - elif user_role == "student": + elif user_role == 'student': request_user = self.student_user self.request.user = request_user - if expected_username == "student_user": + if expected_username == 'student_user': mock_user = self.student_user mock_get.return_value = mock_user @@ -370,3 +373,52 @@ def test_additional_info_response(self, mock_certificate_downloadable_status): assert response.data['certificate'] == {'url': certificate_url} assert response.data['is_self_paced'] is False mock_certificate_downloadable_status.assert_called_once() + + def test_course_access_details(self): + response = self.verify_response(url=self.url) + + expected_course_access_details = { + 'has_unmet_prerequisites': False, + 'is_too_early': False, + 'is_staff': False, + 'audit_access_expires': None, + 'courseware_access': { + 'has_access': True, + 'error_code': None, + 'developer_message': None, + 'user_message': None, + 'additional_context_user_message': None, + 'user_fragment': None + } + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(response.data['course_access_details'], expected_course_access_details) + + def test_course_sharing_utm_parameters(self): + response = self.verify_response(url=self.url) + + expected_course_sharing_utm_parameters = { + 'facebook': 'utm_medium=social&utm_campaign=social-sharing-db&utm_source=facebook', + 'twitter': 'utm_medium=social&utm_campaign=social-sharing-db&utm_source=twitter' + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(response.data['course_sharing_utm_parameters'], expected_course_sharing_utm_parameters) + + def test_course_about_url(self): + response = self.verify_response(url=self.url) + + course_overview = CourseOverview.objects.get(id=self.course.course_id) + expected_course_about_link = get_link_for_about_page(course_overview) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['course_about'], expected_course_about_link) + + def test_course_modes(self): + response = self.verify_response(url=self.url) + + expected_course_modes = [{'slug': 'audit', 'sku': None, 'android_sku': None, 'ios_sku': None, 'min_price': 0}] + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual(response.data['course_modes'], expected_course_modes)