1131 lines
45 KiB
Python
1131 lines
45 KiB
Python
|
|
|
|
import json
|
|
import logging
|
|
import unittest
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
|
|
from django.conf import settings
|
|
from django.urls import reverse
|
|
from django.utils.timezone import now
|
|
from mock import patch
|
|
from opaque_keys.edx.locator import CourseKey
|
|
|
|
from course_modes.models import CourseMode
|
|
from course_modes.tests.factories import CourseModeFactory
|
|
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
|
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
|
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
|
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
|
from openedx.core.djangoapps.user_api.models import UserOrgTag
|
|
from student.models import CourseEnrollment
|
|
from student.tests.factories import TEST_PASSWORD, CourseEnrollmentFactory, UserFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
|
|
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, CourseEntitlementPolicy, CourseEntitlementSupportDetail
|
|
from entitlements.api.v1.serializers import CourseEntitlementSerializer
|
|
from entitlements.api.v1.views import set_entitlement_policy
|
|
|
|
|
|
@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": CourseMode.VERIFIED,
|
|
"course_uuid": course_uuid,
|
|
"order_number": "EDX-1001",
|
|
}
|
|
|
|
def _assert_default_policy(self, policy):
|
|
"""
|
|
Assert that a policy is equal to the default Course Entitlement Policy.
|
|
"""
|
|
default_policy = CourseEntitlementPolicy()
|
|
assert policy.expiration_period == default_policy.expiration_period
|
|
assert policy.refund_period == default_policy.refund_period
|
|
assert policy.regain_period == default_policy.regain_period
|
|
assert policy.mode == default_policy.mode
|
|
|
|
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
|
|
|
|
def test_add_duplicate_entitlement(self):
|
|
"""
|
|
Request with identical course_uuid and order_number should not create duplicate
|
|
entitlement
|
|
"""
|
|
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
|
|
response = self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
assert response.status_code == 400
|
|
course_entitlement = CourseEntitlement.objects.filter(
|
|
course_uuid=course_uuid,
|
|
order_number=entitlement_data['order_number']
|
|
)
|
|
assert course_entitlement.count() == 1
|
|
|
|
def test_order_number_null(self):
|
|
"""
|
|
Test that for same course_uuid order_number set to null is treated as unique
|
|
entitlement
|
|
"""
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['order_number'] = None
|
|
|
|
response = self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
assert response.status_code == 201
|
|
response = self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
assert response.status_code == 201
|
|
course_entitlement = CourseEntitlement.objects.filter(
|
|
course_uuid=course_uuid,
|
|
order_number=entitlement_data['order_number']
|
|
)
|
|
assert course_entitlement.count() == 2
|
|
|
|
def test_default_no_policy_entry(self):
|
|
"""
|
|
Verify that, when there are no entries in the course entitlement policy table,
|
|
the default policy is used for a newly created entitlement.
|
|
"""
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
self._assert_default_policy(course_entitlement.policy)
|
|
|
|
def test_default_no_matching_policy_entry(self):
|
|
"""
|
|
Verify that, when no course entitlement policy is found with the same mode or site
|
|
as the created entitlement, the default policy is used for the entitlement.
|
|
"""
|
|
CourseEntitlementPolicy.objects.create(mode=CourseMode.PROFESSIONAL, site=None)
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
self._assert_default_policy(course_entitlement.policy)
|
|
|
|
def test_set_custom_mode_policy_on_create(self):
|
|
"""
|
|
Verify that, when there does not exist a course entitlement policy with the same mode and site as
|
|
a created entitlement, but there does exist a policy with the same mode and a null site,
|
|
that policy is assigned to the entitlement.
|
|
"""
|
|
policy = CourseEntitlementPolicy.objects.create(mode=CourseMode.PROFESSIONAL, site=None)
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['mode'] = CourseMode.PROFESSIONAL
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
assert course_entitlement.policy == policy
|
|
|
|
# To verify policy selecting behavior involving site specificity, we interact directly
|
|
# with the 'set_entitlement_policy' method due to an inablity to predict or manually assign
|
|
# the site associated with the requests made in unittests.
|
|
def test_set_custom_site_policy_on_create(self):
|
|
"""
|
|
Verify that, when there does not exist a course entitlement policy with the same mode and site as
|
|
a created entitlement, but there does exist a policy with the same site and a null mode,
|
|
that policy is assigned to the entitlement.
|
|
"""
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
|
|
policy_site = SiteFactory.create()
|
|
policy = CourseEntitlementPolicy.objects.create(mode=None, site=policy_site)
|
|
|
|
set_entitlement_policy(course_entitlement, policy_site)
|
|
assert course_entitlement.policy == policy
|
|
|
|
def test_set_policy_match_site_over_mode(self):
|
|
"""
|
|
Verify that, when both a mode-agnostic policy matching the site of a created entitlement and a site-agnostic
|
|
policy matching the mode of a created entitlement exist but no policy matching both the site and mode of the
|
|
created entitlement exists, the site-specific (mode-agnostic) policy matching the entitlement is selected over
|
|
the mode-specific (site-agnostic) policy.
|
|
"""
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
|
|
policy_site = SiteFactory.create()
|
|
policy = CourseEntitlementPolicy.objects.create(mode=None, site=policy_site)
|
|
CourseEntitlementPolicy.objects.create(mode=entitlement_data['mode'], site=None)
|
|
|
|
set_entitlement_policy(course_entitlement, policy_site)
|
|
assert course_entitlement.policy == policy
|
|
|
|
def test_set_policy_site_and_mode_specific(self):
|
|
"""
|
|
Verify that, when there exists a policy matching both the mode and site of the a given course entitlement,
|
|
it is selected over appropriate site- and mode-specific (mode- and site-agnostic) policies and the default
|
|
policy for assignment to the entitlement.
|
|
"""
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['mode'] = CourseMode.PROFESSIONAL
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
|
|
policy_site = SiteFactory.create()
|
|
policy = CourseEntitlementPolicy.objects.create(mode=entitlement_data['mode'], site=policy_site)
|
|
CourseEntitlementPolicy.objects.create(mode=entitlement_data['mode'], site=None)
|
|
CourseEntitlementPolicy.objects.create(mode=None, site=policy_site)
|
|
|
|
set_entitlement_policy(course_entitlement, policy_site)
|
|
assert course_entitlement.policy == policy
|
|
|
|
def test_professional_policy_for_no_id_professional(self):
|
|
"""
|
|
Verify that when there exists a policy with a professional mode that it is assigned
|
|
to new entitlements with the mode no-id-professional.
|
|
"""
|
|
policy = CourseEntitlementPolicy.objects.create(mode=CourseMode.PROFESSIONAL)
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['mode'] = CourseMode.NO_ID_PROFESSIONAL_MODE
|
|
|
|
self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
|
|
course_entitlement = CourseEntitlement.objects.get(
|
|
user=self.user,
|
|
course_uuid=course_uuid
|
|
)
|
|
assert course_entitlement.policy == policy
|
|
|
|
@patch("entitlements.api.v1.views.get_owners_for_course")
|
|
def test_email_opt_in_single_org(self, mock_get_owners):
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['email_opt_in'] = True
|
|
|
|
org = u'particularly'
|
|
mock_get_owners.return_value = [{'key': org}]
|
|
|
|
response = self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
result_obj = UserOrgTag.objects.get(user=self.user, org=org, key='email-optin')
|
|
self.assertEqual(result_obj.value, u"True")
|
|
|
|
@patch("entitlements.api.v1.views.get_owners_for_course")
|
|
def test_email_opt_in_multiple_orgs(self, mock_get_owners):
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['email_opt_in'] = True
|
|
|
|
org_1 = u'particularly'
|
|
org_2 = u'underwood'
|
|
mock_get_owners.return_value = [{'key': org_1}, {'key': org_2}]
|
|
|
|
response = self.client.post(
|
|
self.entitlements_list_url,
|
|
data=json.dumps(entitlement_data),
|
|
content_type='application/json',
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
result_obj = UserOrgTag.objects.get(user=self.user, org=org_1, key='email-optin')
|
|
self.assertEqual(result_obj.value, u"True")
|
|
result_obj = UserOrgTag.objects.get(user=self.user, org=org_2, key='email-optin')
|
|
self.assertEqual(result_obj.value, u"True")
|
|
|
|
def test_add_entitlement_with_support_detail(self):
|
|
"""
|
|
Verify that an EntitlementSupportDetail entry is made when the request includes support interaction information.
|
|
"""
|
|
course_uuid = uuid.uuid4()
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
entitlement_data['support_details'] = [
|
|
{
|
|
"action": "CREATE",
|
|
"comments": "Family emergency."
|
|
},
|
|
]
|
|
|
|
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_and_upgrade_audit_enrollment_with_dynamic_deadline(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 regardless of
|
|
dynamic upgrade deadline being set.
|
|
"""
|
|
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
|
course = CourseFactory.create(self_paced=True)
|
|
course_uuid = uuid.uuid4()
|
|
course_mode = CourseModeFactory(
|
|
course_id=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)
|
|
)
|
|
|
|
# Set up Entitlement
|
|
entitlement_data = self._get_data_set(self.user, str(course_uuid))
|
|
mock_get_course_runs.return_value = [{'key': str(course.id)}]
|
|
|
|
# Add an audit course enrollment for user.
|
|
enrollment = CourseEnrollment.enroll(self.user, course.id, mode=CourseMode.AUDIT)
|
|
|
|
# Set an upgrade schedule so that dynamic upgrade deadlines are used
|
|
ScheduleFactory.create(
|
|
enrollment=enrollment,
|
|
upgrade_deadline=course_mode.expiration_datetime + timedelta(days=-3)
|
|
)
|
|
|
|
# The upgrade should complete and ignore the deadline
|
|
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, 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
|
|
|
|
@patch("entitlements.models.get_course_uuid_for_course")
|
|
def test_revoke_unenroll_entitlement(self, mock_course_uuid):
|
|
enrollment = CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
|
|
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
|
|
mock_course_uuid.return_value = course_entitlement.course_uuid
|
|
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(course_entitlement.uuid)])
|
|
|
|
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
|
|
|
|
def test_reinstate_entitlement(self):
|
|
enrollment = CourseEnrollmentFactory(user=self.user, is_active=True)
|
|
expired_entitlement = CourseEntitlementFactory.create(
|
|
user=self.user, enrollment_course_run=enrollment, expired_at=datetime.now()
|
|
)
|
|
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(expired_entitlement.uuid)])
|
|
|
|
update_data = {
|
|
'expired_at': None,
|
|
'enrollment_course_run': None,
|
|
'support_details': [
|
|
{
|
|
'unenrolled_run': str(enrollment.course.id),
|
|
'action': CourseEntitlementSupportDetail.REISSUE,
|
|
'comments': 'Severe illness.'
|
|
}
|
|
]
|
|
}
|
|
|
|
response = self.client.patch(
|
|
url,
|
|
data=json.dumps(update_data),
|
|
content_type='application/json'
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
results = response.data
|
|
reinstated_entitlement = CourseEntitlement.objects.get(
|
|
uuid=expired_entitlement.uuid
|
|
)
|
|
assert results == CourseEntitlementSerializer(reinstated_entitlement).data
|
|
|
|
def test_reinstate_refundable_entitlement(self):
|
|
""" Verify that an entitlement that is refundable stays refundable when support reinstates it. """
|
|
enrollment = CourseEnrollmentFactory(user=self.user, is_active=True, course=CourseOverviewFactory(start=now()))
|
|
fulfilled_entitlement = CourseEntitlementFactory.create(
|
|
user=self.user, enrollment_course_run=enrollment
|
|
)
|
|
assert fulfilled_entitlement.is_entitlement_refundable() is True
|
|
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(fulfilled_entitlement.uuid)])
|
|
|
|
update_data = {
|
|
'expired_at': None,
|
|
'enrollment_course_run': None,
|
|
'support_details': [
|
|
{
|
|
'unenrolled_run': str(enrollment.course.id),
|
|
'action': CourseEntitlementSupportDetail.REISSUE,
|
|
'comments': 'Severe illness.'
|
|
}
|
|
]
|
|
}
|
|
|
|
response = self.client.patch(
|
|
url,
|
|
data=json.dumps(update_data),
|
|
content_type='application/json'
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
reinstated_entitlement = CourseEntitlement.objects.get(
|
|
uuid=fulfilled_entitlement.uuid
|
|
)
|
|
assert reinstated_entitlement.refund_locked is False
|
|
assert reinstated_entitlement.is_entitlement_refundable() is True
|
|
|
|
def test_reinstate_unrefundable_entitlement(self):
|
|
""" Verify that a no longer refundable entitlement does not become refundable when support reinstates it. """
|
|
enrollment = CourseEnrollmentFactory(user=self.user, is_active=True)
|
|
expired_entitlement = CourseEntitlementFactory.create(
|
|
user=self.user, enrollment_course_run=enrollment, expired_at=datetime.now()
|
|
)
|
|
assert expired_entitlement.is_entitlement_refundable() is False
|
|
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(expired_entitlement.uuid)])
|
|
|
|
update_data = {
|
|
'expired_at': None,
|
|
'enrollment_course_run': None,
|
|
'support_details': [
|
|
{
|
|
'unenrolled_run': str(enrollment.course.id),
|
|
'action': CourseEntitlementSupportDetail.REISSUE,
|
|
'comments': 'Severe illness.'
|
|
}
|
|
]
|
|
}
|
|
|
|
response = self.client.patch(
|
|
url,
|
|
data=json.dumps(update_data),
|
|
content_type='application/json'
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
reinstated_entitlement = CourseEntitlement.objects.get(
|
|
uuid=expired_entitlement.uuid
|
|
)
|
|
assert reinstated_entitlement.refund_locked is True
|
|
assert reinstated_entitlement.is_entitlement_refundable() is False
|
|
|
|
|
|
@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.models.get_course_uuid_for_course")
|
|
@patch("entitlements.api.v1.views.get_course_runs_for_course")
|
|
def test_user_can_unenroll(self, mock_get_course_runs, mock_get_course_uuid):
|
|
course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
|
|
mock_get_course_runs.return_value = self.return_values
|
|
mock_get_course_uuid.return_value = course_entitlement.course_uuid
|
|
|
|
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_already_enrolled_in_unpaid_mode(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=CourseMode.AUDIT)
|
|
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)
|
|
(enrolled_mode, is_active) = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
|
assert is_active and (enrolled_mode == course_entitlement.mode)
|
|
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.models.refund_entitlement', return_value=True)
|
|
@patch('entitlements.api.v1.views.get_course_runs_for_course')
|
|
@patch("entitlements.models.get_course_uuid_for_course")
|
|
def test_user_can_revoke_and_refund(self, mock_course_uuid, 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
|
|
mock_course_uuid.return_value = course_entitlement.course_uuid
|
|
|
|
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 mock_refund_entitlement.call_args[1]['course_entitlement'] == course_entitlement
|
|
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.models.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.models.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
|