tests.py 11.3 KB
Newer Older
Dave St.Germain committed
1 2 3
"""
Tests for users API
"""
4
import datetime
5
from django.utils import timezone
6

7
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
Dave St.Germain committed
8
from student.models import CourseEnrollment
9 10
from certificates.models import CertificateStatuses
from certificates.tests.factories import GeneratedCertificateFactory
11 12

from .. import errors
13
from ..testutils import MobileAPITestCase, MobileAuthTestMixin, MobileAuthUserTestMixin, MobileCourseAccessTestMixin
14
from .serializers import CourseEnrollmentSerializer
Dave St.Germain committed
15 16


17
class TestUserDetailApi(MobileAPITestCase, MobileAuthUserTestMixin):
Dave St.Germain committed
18
    """
19
    Tests for /api/mobile/v0.5/users/<user_name>...
Dave St.Germain committed
20
    """
21
    REVERSE_INFO = {'name': 'user-detail', 'params': ['username']}
22

23 24
    def test_success(self):
        self.login()
Dave St.Germain committed
25

26 27 28
        response = self.api_response()
        self.assertEqual(response.data['username'], self.user.username)
        self.assertEqual(response.data['email'], self.user.email)
29 30


31 32 33 34 35 36
class TestUserInfoApi(MobileAPITestCase, MobileAuthTestMixin):
    """
    Tests for /api/mobile/v0.5/my_user_info
    """
    def reverse_url(self, reverse_args=None, **kwargs):
        return '/api/mobile/v0.5/my_user_info'
Dave St.Germain committed
37

38 39 40 41 42
    def test_success(self):
        """Verify the endpoint redirects to the user detail endpoint"""
        self.login()

        response = self.api_response(expected_response_code=302)
Dave St.Germain committed
43 44
        self.assertTrue(self.username in response['location'])

45

46
class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileCourseAccessTestMixin):
47 48 49 50
    """
    Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
    """
    REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
51
    ALLOW_ACCESS_TO_UNRELEASED_COURSE = True
52
    ALLOW_ACCESS_TO_MILESTONE_COURSE = True
53

54 55 56 57
    def verify_success(self, response):
        super(TestUserEnrollmentApi, self).verify_success(response)
        courses = response.data
        self.assertEqual(len(courses), 1)
58

59 60 61 62 63
        found_course = courses[0]['course']
        self.assertTrue('video_outline' in found_course)
        self.assertTrue('course_handouts' in found_course)
        self.assertEqual(found_course['id'], unicode(self.course.id))
        self.assertEqual(courses[0]['mode'], 'honor')
64
        self.assertEqual(courses[0]['course']['subscription_id'], self.course.clean_id(padding_char='_'))
65

66 67 68 69 70
    def verify_failure(self, response):
        self.assertEqual(response.status_code, 200)
        courses = response.data
        self.assertEqual(len(courses), 0)

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
    def test_sort_order(self):
        self.login()

        num_courses = 3
        courses = []
        for course_num in range(num_courses):
            courses.append(CourseFactory.create(mobile_available=True))
            self.enroll(courses[course_num].id)

        # verify courses are returned in the order of enrollment, with most recently enrolled first.
        response = self.api_response()
        for course_num in range(num_courses):
            self.assertEqual(
                response.data[course_num]['course']['id'],  # pylint: disable=no-member
                unicode(courses[num_courses - course_num - 1].id)
            )

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
    def test_no_certificate(self):
        self.login_and_enroll()

        response = self.api_response()
        certificate_data = response.data[0]['certificate']  # pylint: disable=no-member
        self.assertDictEqual(certificate_data, {})

    def test_certificate(self):
        self.login_and_enroll()

        certificate_url = "http://test_certificate_url"
        GeneratedCertificateFactory.create(
            user=self.user,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            mode='verified',
            download_url=certificate_url,
        )

        response = self.api_response()
        certificate_data = response.data[0]['certificate']  # pylint: disable=no-member
        self.assertEquals(certificate_data['url'], certificate_url)

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
    def test_no_facebook_url(self):
        self.login_and_enroll()

        response = self.api_response()
        course_data = response.data[0]['course']  # pylint: disable=no-member
        self.assertIsNone(course_data['social_urls']['facebook'])

    def test_facebook_url(self):
        self.login_and_enroll()

        self.course.facebook_url = "http://facebook.com/test_group_page"
        self.store.update_item(self.course, self.user.id)

        response = self.api_response()
        course_data = response.data[0]['course']  # pylint: disable=no-member
        self.assertEquals(course_data['social_urls']['facebook'], self.course.facebook_url)

128 129 130 131 132 133

class CourseStatusAPITestCase(MobileAPITestCase):
    """
    Base test class for /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
    """
    REVERSE_INFO = {'name': 'user-course-status', 'params': ['username', 'course_id']}
134

135
    def setUp(self):
136 137 138
        """
        Creates a basic course structure for our course
        """
139 140 141 142 143 144 145 146 147
        super(CourseStatusAPITestCase, self).setUp()

        self.section = ItemFactory.create(
            parent=self.course,
            category='chapter',
        )
        self.sub_section = ItemFactory.create(
            parent=self.section,
            category='sequential',
148
        )
149 150 151
        self.unit = ItemFactory.create(
            parent=self.sub_section,
            category='vertical',
152
        )
153 154 155
        self.other_sub_section = ItemFactory.create(
            parent=self.section,
            category='sequential',
156
        )
157 158 159
        self.other_unit = ItemFactory.create(
            parent=self.other_sub_section,
            category='vertical',
160 161 162
        )


163
class TestCourseStatusGET(CourseStatusAPITestCase, MobileAuthUserTestMixin, MobileCourseAccessTestMixin):
164 165 166 167 168
    """
    Tests for GET of /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
    """
    def test_success(self):
        self.login_and_enroll()
169

170
        response = self.api_response()
171
        self.assertEqual(
172 173 174 175 176 177
            response.data["last_visited_module_id"],  # pylint: disable=no-member
            unicode(self.sub_section.location)
        )
        self.assertEqual(
            response.data["last_visited_module_path"],  # pylint: disable=no-member
            [unicode(module.location) for module in [self.sub_section, self.section, self.course]]
178
        )
179 180


181
class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, MobileCourseAccessTestMixin):
182 183 184 185 186 187
    """
    Tests for PATCH of /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
    """
    def url_method(self, url, **kwargs):
        # override implementation to use PATCH method.
        return self.client.patch(url, data=kwargs.get('data', None))  # pylint: disable=no-member
188

189 190
    def test_success(self):
        self.login_and_enroll()
191 192 193 194 195
        response = self.api_response(data={"last_visited_module_id": unicode(self.other_unit.location)})
        self.assertEqual(
            response.data["last_visited_module_id"],  # pylint: disable=no-member
            unicode(self.other_sub_section.location)
        )
196 197 198 199

    def test_invalid_module(self):
        self.login_and_enroll()
        response = self.api_response(data={"last_visited_module_id": "abc"}, expected_response_code=400)
200 201 202 203
        self.assertEqual(
            response.data,  # pylint: disable=no-member
            errors.ERROR_INVALID_MODULE_ID
        )
204 205 206 207 208

    def test_nonexistent_module(self):
        self.login_and_enroll()
        non_existent_key = self.course.id.make_usage_key('video', 'non-existent')
        response = self.api_response(data={"last_visited_module_id": non_existent_key}, expected_response_code=400)
209 210 211 212
        self.assertEqual(
            response.data,  # pylint: disable=no-member
            errors.ERROR_INVALID_MODULE_ID
        )
213

214 215
    def test_no_timezone(self):
        self.login_and_enroll()
216
        past_date = datetime.datetime.now()
217 218
        response = self.api_response(
            data={
219
                "last_visited_module_id": unicode(self.other_unit.location),
220 221
                "modification_date": past_date.isoformat()  # pylint: disable=maybe-no-member
            },
222
            expected_response_code=400
223
        )
224 225 226 227
        self.assertEqual(
            response.data,  # pylint: disable=no-member
            errors.ERROR_INVALID_MODIFICATION_DATE
        )
228

229
    def _date_sync(self, date, initial_unit, update_unit, expected_subsection):
230 231 232 233
        """
        Helper for test cases that use a modification to decide whether
        to update the course status
        """
234 235
        self.login_and_enroll()

236
        # save something so we have an initial date
237
        self.api_response(data={"last_visited_module_id": unicode(initial_unit.location)})
238 239

        # now actually update it
240 241
        response = self.api_response(
            data={
242 243
                "last_visited_module_id": unicode(update_unit.location),
                "modification_date": date.isoformat()
244
            }
245
        )
246 247 248 249
        self.assertEqual(
            response.data["last_visited_module_id"],  # pylint: disable=no-member
            unicode(expected_subsection.location)
        )
250

251 252
    def test_old_date(self):
        self.login_and_enroll()
253
        date = timezone.now() + datetime.timedelta(days=-100)
254
        self._date_sync(date, self.unit, self.other_unit, self.sub_section)
255

256 257
    def test_new_date(self):
        self.login_and_enroll()
258
        date = timezone.now() + datetime.timedelta(days=100)
259
        self._date_sync(date, self.unit, self.other_unit, self.other_sub_section)
260

261 262 263 264
    def test_no_initial_date(self):
        self.login_and_enroll()
        response = self.api_response(
            data={
265
                "last_visited_module_id": unicode(self.other_unit.location),
266 267 268
                "modification_date": timezone.now().isoformat()
            }
        )
269 270 271 272
        self.assertEqual(
            response.data["last_visited_module_id"],  # pylint: disable=no-member
            unicode(self.other_sub_section.location)
        )
273

274 275 276
    def test_invalid_date(self):
        self.login_and_enroll()
        response = self.api_response(data={"modification_date": "abc"}, expected_response_code=400)
277 278 279 280
        self.assertEqual(
            response.data,  # pylint: disable=no-member
            errors.ERROR_INVALID_MODIFICATION_DATE
        )
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

class TestCourseEnrollmentSerializer(MobileAPITestCase):
    """
    Test the course enrollment serializer
    """
    def test_success(self):
        self.login_and_enroll()

        serialized = CourseEnrollmentSerializer(CourseEnrollment.enrollments_for_user(self.user)[0]).data  # pylint: disable=no-member
        self.assertEqual(serialized['course']['video_outline'], None)
        self.assertEqual(serialized['course']['name'], self.course.display_name)
        self.assertEqual(serialized['course']['number'], self.course.id.course)
        self.assertEqual(serialized['course']['org'], self.course.id.org)

    def test_with_display_overrides(self):
        self.login_and_enroll()

        self.course.display_coursenumber = "overridden_number"
        self.course.display_organization = "overridden_org"
301
        self.store.update_item(self.course, self.user.id)
302 303 304 305

        serialized = CourseEnrollmentSerializer(CourseEnrollment.enrollments_for_user(self.user)[0]).data  # pylint: disable=no-member
        self.assertEqual(serialized['course']['number'], self.course.display_coursenumber)
        self.assertEqual(serialized['course']['org'], self.course.display_organization)