Files
edx-platform/common/djangoapps/entitlements/api/v1/tests/test_views.py
Albert St. Aubin ddab8123c1 Updates to the Course Entitlement API to block Learners from enrolling
in runs that are not available.

[LEARNER-3800]
2018-01-30 09:52:06 -05:00

648 lines
25 KiB
Python

import json
import logging
import unittest
import uuid
from datetime import timedelta
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.timezone import now
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from mock import patch
from opaque_keys.edx.locator import CourseKey
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.models import CourseEnrollment
from student.tests.factories import (TEST_PASSWORD, CourseEnrollmentFactory, UserFactory)
log = logging.getLogger(__name__)
# Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection
if settings.ROOT_URLCONF == 'lms.urls':
from entitlements.tests.factories import CourseEntitlementFactory
from entitlements.models import CourseEntitlement
from entitlements.api.v1.serializers import CourseEntitlementSerializer
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementViewSetTest(ModuleStoreTestCase):
ENTITLEMENTS_DETAILS_PATH = 'entitlements_api:v1:entitlements-detail'
def setUp(self):
super(EntitlementViewSetTest, self).setUp()
self.user = UserFactory(is_staff=True)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.course = CourseFactory()
self.course_mode = CourseModeFactory(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=now() + timedelta(days=1)
)
self.entitlements_list_url = reverse('entitlements_api:v1:entitlements-list')
def _get_data_set(self, user, course_uuid):
"""
Get a basic data set for an entitlement
"""
return {
"user": user.username,
"mode": "verified",
"course_uuid": course_uuid,
"order_number": "EDX-1001"
}
def test_auth_required(self):
self.client.logout()
response = self.client.get(self.entitlements_list_url)
assert response.status_code == 401
def test_staff_user_not_required_for_get(self):
not_staff_user = UserFactory()
self.client.login(username=not_staff_user.username, password=TEST_PASSWORD)
response = self.client.get(self.entitlements_list_url)
assert response.status_code == 200
def test_add_entitlement_with_missing_data(self):
entitlement_data_missing_parts = self._get_data_set(self.user, str(uuid.uuid4()))
entitlement_data_missing_parts.pop('mode')
entitlement_data_missing_parts.pop('course_uuid')
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data_missing_parts),
content_type='application/json',
)
assert response.status_code == 400
def test_staff_user_required_for_post(self):
not_staff_user = UserFactory()
self.client.login(username=not_staff_user.username, password=TEST_PASSWORD)
course_uuid = uuid.uuid4()
entitlement_data = self._get_data_set(self.user, str(course_uuid))
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data),
content_type='application/json',
)
assert response.status_code == 403
def test_staff_user_required_for_delete(self):
not_staff_user = UserFactory()
self.client.login(username=not_staff_user.username, password=TEST_PASSWORD)
course_entitlement = CourseEntitlementFactory.create()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(course_entitlement.uuid)])
response = self.client.delete(
url,
content_type='application/json',
)
assert response.status_code == 403
def test_add_entitlement(self):
course_uuid = uuid.uuid4()
entitlement_data = self._get_data_set(self.user, str(course_uuid))
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data),
content_type='application/json',
)
assert response.status_code == 201
results = response.data
course_entitlement = CourseEntitlement.objects.get(
user=self.user,
course_uuid=course_uuid
)
assert results == CourseEntitlementSerializer(course_entitlement).data
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_add_entitlement_and_upgrade_audit_enrollment(self, mock_get_course_runs):
"""
Verify that if an entitlement is added for a user, if the user has one upgradeable enrollment
that enrollment is upgraded to the mode of the entitlement and linked to the entitlement.
"""
course_uuid = uuid.uuid4()
entitlement_data = self._get_data_set(self.user, str(course_uuid))
mock_get_course_runs.return_value = [{'key': str(self.course.id)}]
# Add an audit course enrollment for user.
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT)
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data),
content_type='application/json',
)
assert response.status_code == 201
results = response.data
course_entitlement = CourseEntitlement.objects.get(
user=self.user,
course_uuid=course_uuid
)
# Assert that enrollment mode is now verified
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)[0]
assert enrollment_mode == course_entitlement.mode
assert course_entitlement.enrollment_course_run == enrollment
assert results == CourseEntitlementSerializer(course_entitlement).data
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_add_entitlement_inactive_audit_enrollment(self, mock_get_course_runs):
"""
Verify that if an entitlement is added for a user, if the user has an inactive audit enrollment
that enrollment is NOT upgraded to the mode of the entitlement and linked to the entitlement.
"""
course_uuid = uuid.uuid4()
entitlement_data = self._get_data_set(self.user, str(course_uuid))
mock_get_course_runs.return_value = [{'key': str(self.course.id)}]
# Add an audit course enrollment for user.
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT)
enrollment.update_enrollment(is_active=False)
response = self.client.post(
self.entitlements_list_url,
data=json.dumps(entitlement_data),
content_type='application/json',
)
assert response.status_code == 201
results = response.data
course_entitlement = CourseEntitlement.objects.get(
user=self.user,
course_uuid=course_uuid
)
# Assert that enrollment mode is now verified
enrollment_mode, enrollment_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
assert enrollment_mode == CourseMode.AUDIT
assert enrollment_active is False
assert course_entitlement.enrollment_course_run is None
assert results == CourseEntitlementSerializer(course_entitlement).data
def test_non_staff_get_select_entitlements(self):
not_staff_user = UserFactory()
self.client.login(username=not_staff_user.username, password=TEST_PASSWORD)
CourseEntitlementFactory.create_batch(2)
entitlement = CourseEntitlementFactory.create(user=not_staff_user)
response = self.client.get(
self.entitlements_list_url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data.get('results', []) # pylint: disable=no-member
assert results == CourseEntitlementSerializer([entitlement], many=True).data
def test_staff_get_only_staff_entitlements(self):
CourseEntitlementFactory.create_batch(2)
entitlement = CourseEntitlementFactory.create(user=self.user)
response = self.client.get(
self.entitlements_list_url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data.get('results', [])
assert results == CourseEntitlementSerializer([entitlement], many=True).data
def test_staff_get_expired_entitlements(self):
past_datetime = now() - timedelta(days=365 * 2)
entitlements = CourseEntitlementFactory.create_batch(2, created=past_datetime, user=self.user)
# Set the first entitlement to be at a time that it isn't expired
entitlements[0].created = now()
entitlements[0].save()
response = self.client.get(
self.entitlements_list_url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data.get('results', []) # pylint: disable=no-member
# Make sure that the first result isn't expired, and the second one is also not for staff users
assert results[0].get('expired_at') is None and results[1].get('expired_at') is None
def test_get_user_expired_entitlements(self):
past_datetime = now() - timedelta(days=365 * 2)
not_staff_user = UserFactory()
self.client.login(username=not_staff_user.username, password=TEST_PASSWORD)
entitlement_user2 = CourseEntitlementFactory.create_batch(2, user=not_staff_user, created=past_datetime)
url = reverse('entitlements_api:v1:entitlements-list')
url += '?user={username}'.format(username=not_staff_user.username)
# Set the first entitlement to be at a time that it isn't expired
entitlement_user2[0].created = now()
entitlement_user2[0].save()
response = self.client.get(
url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data.get('results', []) # pylint: disable=no-member
assert results[0].get('expired_at') is None and results[1].get('expired_at')
def test_get_user_entitlements(self):
user2 = UserFactory()
CourseEntitlementFactory.create()
entitlement_user2 = CourseEntitlementFactory.create(user=user2)
url = reverse('entitlements_api:v1:entitlements-list')
url += '?user={username}'.format(username=user2.username)
response = self.client.get(
url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data.get('results', [])
assert results == CourseEntitlementSerializer([entitlement_user2], many=True).data
def test_get_entitlement_by_uuid(self):
entitlement = CourseEntitlementFactory.create()
CourseEntitlementFactory.create_batch(2)
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(entitlement.uuid)])
response = self.client.get(
url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data
assert results == CourseEntitlementSerializer(entitlement).data and results.get('expired_at') is None
def test_get_expired_entitlement_by_uuid(self):
past_datetime = now() - timedelta(days=365 * 2)
entitlement = CourseEntitlementFactory(created=past_datetime)
CourseEntitlementFactory.create_batch(2)
CourseEntitlementFactory()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(entitlement.uuid)])
response = self.client.get(
url,
content_type='application/json',
)
assert response.status_code == 200
results = response.data # pylint: disable=no-member
assert results.get('expired_at')
def test_delete_and_revoke_entitlement(self):
course_entitlement = CourseEntitlementFactory.create()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(course_entitlement.uuid)])
response = self.client.delete(
url,
content_type='application/json',
)
assert response.status_code == 204
course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is not None
def test_revoke_unenroll_entitlement(self):
course_entitlement = CourseEntitlementFactory.create()
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(course_entitlement.uuid)])
enrollment = CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
course_entitlement.refresh_from_db()
course_entitlement.enrollment_course_run = enrollment
course_entitlement.save()
assert course_entitlement.enrollment_course_run is not None
response = self.client.delete(
url,
content_type='application/json',
)
assert response.status_code == 204
course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is not None
assert course_entitlement.enrollment_course_run is None
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
"""
Tests for the EntitlementEnrollmentViewSets
"""
ENTITLEMENTS_ENROLLMENT_NAMESPACE = 'entitlements_api:v1:enrollments'
def setUp(self):
super(EntitlementEnrollmentViewSetTest, self).setUp()
self.user = UserFactory()
UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
self.course2 = CourseFactory.create(org='edX', number='DemoX2', display_name='Demo_Course 2')
self.course_mode = CourseModeFactory(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=now() + timedelta(days=1)
)
self.course_mode = CourseModeFactory(
course_id=self.course2.id,
mode_slug=CourseMode.VERIFIED,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime=now() + timedelta(days=1)
)
self.return_values = [
{'key': str(self.course.id)},
{'key': str(self.course2.id)}
]
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_can_enroll(self, mock_get_course_runs):
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
assert course_entitlement.enrollment_course_run is None
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_can_unenroll(self, mock_get_course_runs):
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
assert course_entitlement.enrollment_course_run is None
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
response = self.client.delete(
url,
content_type='application/json',
)
assert response.status_code == 204
course_entitlement.refresh_from_db()
assert not CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is None
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_can_switch(self, mock_get_course_runs):
mock_get_course_runs.return_value = self.return_values
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
assert course_entitlement.enrollment_course_run is None
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
data = {
'course_run_id': str(self.course2.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
assert response.status_code == 201
course_entitlement.refresh_from_db()
assert CourseEnrollment.is_enrolled(self.user, self.course2.id)
assert course_entitlement.enrollment_course_run is not None
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_already_enrolled(self, mock_get_course_runs):
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
CourseEnrollment.enroll(self.user, self.course.id, mode=course_entitlement.mode)
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_cannot_enroll_in_unknown_course_run_id(self, mock_get_course_runs):
fake_course_str = str(self.course.id) + 'fake'
fake_course_key = CourseKey.from_string(fake_course_str)
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
data = {
'course_run_id': str(fake_course_key)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
expected_message = 'The Course Run ID is not a match for this Course Entitlement.'
assert response.status_code == 400
assert response.data['message'] == expected_message # pylint: disable=no-member
assert not CourseEnrollment.is_enrolled(self.user, fake_course_key)
@patch('entitlements.api.v1.views.refund_entitlement', return_value=True)
@patch('entitlements.api.v1.views.get_course_runs_for_course')
def test_user_can_revoke_and_refund(self, mock_get_course_runs, mock_refund_entitlement):
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
assert course_entitlement.enrollment_course_run is None
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
# Unenroll with Revoke for refund
revoke_url = url + '?is_refund=true'
response = self.client.delete(
revoke_url,
content_type='application/json',
)
assert response.status_code == 204
course_entitlement.refresh_from_db()
assert mock_refund_entitlement.is_called
assert (CourseEntitlementSerializer(mock_refund_entitlement.call_args[1]['course_entitlement']).data ==
CourseEntitlementSerializer(course_entitlement).data)
assert not CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is None
assert course_entitlement.expired_at is not None
@patch('entitlements.api.v1.views.CourseEntitlement.is_entitlement_refundable', return_value=False)
@patch('entitlements.api.v1.views.refund_entitlement', return_value=True)
@patch('entitlements.api.v1.views.get_course_runs_for_course')
def test_user_can_revoke_and_no_refund_available(
self,
mock_get_course_runs,
mock_refund_entitlement,
mock_is_refundable
):
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
assert course_entitlement.enrollment_course_run is None
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
# Unenroll with Revoke for refund
revoke_url = url + '?is_refund=true'
response = self.client.delete(
revoke_url,
content_type='application/json',
)
assert response.status_code == 400
course_entitlement.refresh_from_db()
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
assert course_entitlement.expired_at is None
@patch('entitlements.api.v1.views.CourseEntitlement.is_entitlement_refundable', return_value=True)
@patch('entitlements.api.v1.views.refund_entitlement', return_value=False)
@patch("entitlements.api.v1.views.get_course_runs_for_course")
def test_user_is_not_unenrolled_on_failed_refund(
self,
mock_get_course_runs,
mock_refund_entitlement,
mock_is_refundable
):
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
mock_get_course_runs.return_value = self.return_values
url = reverse(
self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
args=[str(course_entitlement.uuid)]
)
assert course_entitlement.enrollment_course_run is None
# Enroll the User
data = {
'course_run_id': str(self.course.id)
}
response = self.client.post(
url,
data=json.dumps(data),
content_type='application/json',
)
course_entitlement.refresh_from_db()
assert response.status_code == 201
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
# Unenroll with Revoke for refund
revoke_url = url + '?is_refund=true'
response = self.client.delete(
revoke_url,
content_type='application/json',
)
assert response.status_code == 500
course_entitlement.refresh_from_db()
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
assert course_entitlement.expired_at is None