Files
edx-platform/common/djangoapps/course_modes/tests/test_models.py
Moeez Zahid c8af0f607f Enable In-App purchases on edx-mobile app (#31512)
* feat: added mobile skus in course mode

* fix: changed api

---------

Co-authored-by: jawad-khan <jawadkhan444@gmail.com>
Co-authored-by: Robert Raposa <rraposa@edx.org>
2023-02-10 16:16:37 +05:00

535 lines
21 KiB
Python

"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
import itertools
from datetime import timedelta
from unittest.mock import patch
import ddt
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from django.utils.timezone import now
from opaque_keys.edx.locator import CourseLocator
from common.djangoapps.course_modes.helpers import enrollment_mode_display
from common.djangoapps.course_modes.models import (
CourseMode,
Mode,
get_cosmetic_display_price,
invalidate_course_mode_cache
)
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@ddt.ddt
class CourseModeModelTest(TestCase):
"""
Tests for the CourseMode model
"""
NOW = 'now'
DATES = {
NOW: now(),
None: None,
}
def setUp(self):
super().setUp()
self.course_key = CourseLocator('Test', 'TestCourse', 'TestCourseRun')
CourseMode.objects.all().delete()
def tearDown(self):
super().tearDown()
invalidate_course_mode_cache(sender=None)
def create_mode(
self,
mode_slug,
mode_name,
min_price=0,
suggested_prices='',
currency='usd',
expiration_datetime=None,
):
"""
Create a new course mode
"""
return CourseMode.objects.get_or_create(
course_id=self.course_key,
mode_display_name=mode_name,
mode_slug=mode_slug,
min_price=min_price,
suggested_prices=suggested_prices,
currency=currency,
_expiration_datetime=expiration_datetime,
)
def test_save(self):
""" Verify currency is always lowercase. """
cm, __ = self.create_mode('honor', 'honor', 0, '', 'USD')
assert cm.currency == 'usd'
cm.currency = 'GHS'
cm.save()
assert cm.currency == 'ghs'
def test_modes_for_course_empty(self):
"""
If we can't find any modes, we should get back the default mode
"""
# shouldn't be able to find a corresponding course
modes = CourseMode.modes_for_course(self.course_key)
assert [CourseMode.DEFAULT_MODE] == modes
def test_nodes_for_course_single(self):
"""
Find the modes for a course with only one mode
"""
self.create_mode('verified', 'Verified Certificate', 10)
modes = CourseMode.modes_for_course(self.course_key)
mode = Mode('verified', 'Verified Certificate', 10, '', 'usd', None, None, None, None, None, None)
assert [mode] == modes
modes_dict = CourseMode.modes_for_course_dict(self.course_key)
assert modes_dict['verified'] == mode
assert CourseMode.mode_for_course(self.course_key, 'verified') == mode
def test_modes_for_course_multiple(self):
"""
Finding the modes when there's multiple modes
"""
mode1 = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None, None, None)
mode2 = Mode('verified', 'Verified Certificate', 10, '', 'usd', None, None, None, None, None, None)
set_modes = [mode1, mode2]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices)
modes = CourseMode.modes_for_course(self.course_key)
assert modes == set_modes
assert mode1 == CourseMode.mode_for_course(self.course_key, 'honor')
assert mode2 == CourseMode.mode_for_course(self.course_key, 'verified')
assert CourseMode.mode_for_course(self.course_key, 'DNE') is None
def test_min_course_price_for_currency(self):
"""
Get the min course price for a course according to currency
"""
# no modes, should get 0
assert 0 == CourseMode.min_course_price_for_currency(self.course_key, 'usd')
# with mode with other currency, should get 0
mode = Mode('audit', 'Audit', 30, '', 'eur', None, None, None, None, None, None)
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
assert 0 == CourseMode.min_course_price_for_currency(self.course_key, 'usd')
# create some modes
mode1 = Mode('honor', 'Honor Code Certificate', 10, '', 'usd', None, None, None, None, None, None)
mode2 = Mode('verified', 'Verified Certificate', 20, '', 'usd', None, None, None, None, None, None)
mode3 = Mode('honor', 'Honor Code Certificate', 80, '', 'cny', None, None, None, None, None, None)
set_modes = [mode1, mode2, mode3]
for mode in set_modes:
self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency)
assert 10 == CourseMode.min_course_price_for_currency(self.course_key, 'usd')
assert 80 == CourseMode.min_course_price_for_currency(self.course_key, 'cny')
def test_modes_for_course_expired(self):
expired_mode, _status = self.create_mode('verified', 'Verified Certificate', 10)
expired_mode.expiration_datetime = now() + timedelta(days=-1)
expired_mode.save()
modes = CourseMode.modes_for_course(self.course_key)
assert [CourseMode.DEFAULT_MODE] == modes
mode1 = Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None, None, None)
self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices)
modes = CourseMode.modes_for_course(self.course_key)
assert [mode1] == modes
expiration_datetime = now() + timedelta(days=1)
expired_mode.expiration_datetime = expiration_datetime
expired_mode.save()
expired_mode_value = Mode(
'verified',
'Verified Certificate',
10,
'',
'usd',
expiration_datetime,
None,
None,
None,
None,
None
)
modes = CourseMode.modes_for_course(self.course_key)
assert [expired_mode_value, mode1] == modes
modes = CourseMode.modes_for_course(CourseLocator('TestOrg', 'TestCourse', 'TestRun'))
assert [CourseMode.DEFAULT_MODE] == modes
def test_verified_mode_for_course(self):
self.create_mode('verified', 'Verified Certificate', 10)
mode = CourseMode.verified_mode_for_course(self.course_key)
assert mode.slug == 'verified'
# verify that the professional mode is preferred
self.create_mode('professional', 'Professional Education Verified Certificate', 10)
mode = CourseMode.verified_mode_for_course(self.course_key)
assert mode.slug == 'professional'
def test_course_has_payment_options(self):
# Has no payment options.
honor, _ = self.create_mode('honor', 'Honor')
assert not CourseMode.has_payment_options(self.course_key)
# Now we do have a payment option.
verified, _ = self.create_mode('verified', 'Verified', min_price=5)
assert CourseMode.has_payment_options(self.course_key)
# Remove the verified option.
verified.delete()
assert not CourseMode.has_payment_options(self.course_key)
# Finally, give the honor mode payment options
honor.suggested_prices = '5, 10, 15'
honor.save()
assert CourseMode.has_payment_options(self.course_key)
def test_course_has_payment_options_with_no_id_professional(self):
# Has payment options.
self.create_mode('no-id-professional', 'no-id-professional', min_price=5)
assert CourseMode.has_payment_options(self.course_key)
@ddt.data(
([], True),
([("honor", 0), ("audit", 0), ("verified", 100)], True),
([("honor", 100)], False),
([("professional", 100)], False),
([("no-id-professional", 100)], False),
)
@ddt.unpack
def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
# Create the modes and min prices
for mode_slug, min_price in modes_and_prices:
self.create_mode(mode_slug, mode_slug.capitalize(), min_price=min_price)
# Verify that we can or cannot auto enroll
assert CourseMode.can_auto_enroll(self.course_key) == can_auto_enroll
@ddt.data(
([], None),
(["honor", "audit", "verified"], "honor"),
(["honor", "audit"], "honor"),
(["audit", "verified"], "audit"),
(["professional"], None),
(["no-id-professional"], None),
(["credit", "audit", "verified"], "audit"),
(["credit"], None),
)
@ddt.unpack
def test_auto_enroll_mode(self, modes, result):
# Verify that the proper auto enroll mode is returned
assert CourseMode.auto_enroll_mode(self.course_key, modes) == result
def test_all_modes_for_courses(self):
now_dt = now()
future = now_dt + timedelta(days=1)
past = now_dt - timedelta(days=1)
# Unexpired, no expiration date
CourseModeFactory.create(
course_id=self.course_key,
mode_display_name="Honor No Expiration",
mode_slug="honor_no_expiration",
expiration_datetime=None
)
# Unexpired, expiration date in future
CourseModeFactory.create(
course_id=self.course_key,
mode_display_name="Honor Not Expired",
mode_slug="honor_not_expired",
expiration_datetime=future
)
# Expired
CourseModeFactory.create(
course_id=self.course_key,
mode_display_name="Verified Expired",
mode_slug="verified_expired",
expiration_datetime=past
)
# We should get all of these back when querying for *all* course modes,
# including ones that have expired.
other_course_key = CourseLocator(org="not", course="a", run="course")
all_modes = CourseMode.all_modes_for_courses([self.course_key, other_course_key])
assert len(all_modes[self.course_key]) == 3
assert all_modes[self.course_key][0].name == 'Honor No Expiration'
assert all_modes[self.course_key][1].name == 'Honor Not Expired'
assert all_modes[self.course_key][2].name == 'Verified Expired'
# Check that we get a default mode for when no course mode is available
assert len(all_modes[other_course_key]) == 1
assert all_modes[other_course_key][0] == CourseMode.DEFAULT_MODE
@ddt.data('', 'no-id-professional', 'professional', 'verified')
def test_course_has_professional_mode(self, mode):
# check the professional mode.
self.create_mode(mode, 'course mode', 10)
modes_dict = CourseMode.modes_for_course_dict(self.course_key)
if mode in ['professional', 'no-id-professional']:
assert CourseMode.has_professional_mode(modes_dict)
else:
assert not CourseMode.has_professional_mode(modes_dict)
@ddt.data('no-id-professional', 'professional', 'verified')
def test_course_is_professional_mode(self, mode):
# check that tuple has professional mode
course_mode, __ = self.create_mode(mode, 'course mode', 10)
if mode in ['professional', 'no-id-professional']:
assert CourseMode.is_professional_mode(course_mode.to_tuple())
else:
assert not CourseMode.is_professional_mode(course_mode.to_tuple())
def test_course_is_professional_mode_with_invalid_tuple(self):
# check that tuple has professional mode with None
assert not CourseMode.is_professional_mode(None)
@ddt.data(
('no-id-professional', False),
('professional', True),
('verified', True),
('honor', False),
('audit', False)
)
@ddt.unpack
def test_is_verified_slug(self, mode_slug, is_verified):
# check that mode slug is verified or not
if is_verified:
assert CourseMode.is_verified_slug(mode_slug)
else:
assert not CourseMode.is_verified_slug(mode_slug)
@ddt.data(*itertools.product(
(
CourseMode.HONOR,
CourseMode.AUDIT,
CourseMode.VERIFIED,
CourseMode.PROFESSIONAL,
CourseMode.NO_ID_PROFESSIONAL_MODE
),
(NOW, None),
))
@ddt.unpack
def test_invalid_mode_expiration(self, mode_slug, exp_dt_name):
exp_dt = self.DATES[exp_dt_name]
is_error_expected = CourseMode.is_professional_slug(mode_slug) and exp_dt is not None
try:
self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), expiration_datetime=exp_dt, min_price=10)
assert not is_error_expected, 'Expected a ValidationError to be thrown.'
except ValidationError as exc:
assert is_error_expected, 'Did not expect a ValidationError to be thrown.'
assert exc.messages == ['Professional education modes are not allowed to have expiration_datetime set.']
@ddt.data(
"verified",
"honor",
"audit",
"professional",
"no-id-professional",
)
def test_enrollment_mode_display(self, mode):
assert enrollment_mode_display(mode, self.course_key) == \
self._enrollment_display_modes_dicts(mode)
@ddt.data(
(['honor', 'verified', 'credit'], ['honor', 'verified']),
(['professional', 'credit'], ['professional']),
)
@ddt.unpack
def test_hide_credit_modes(self, available_modes, expected_selectable_modes):
# Create the course modes
for mode in available_modes:
CourseModeFactory.create(
course_id=self.course_key,
mode_display_name=mode,
mode_slug=mode,
)
# Check the selectable modes, which should exclude credit
selectable_modes = CourseMode.modes_for_course_dict(self.course_key)
self.assertCountEqual(list(selectable_modes.keys()), expected_selectable_modes)
# When we get all unexpired modes, we should see credit as well
all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False)
self.assertCountEqual(list(all_modes.keys()), available_modes)
def _enrollment_display_modes_dicts(self, mode):
"""
Helper function to generate the enrollment display mode dict.
"""
dict_keys = ['enrollment_title', 'enrollment_value', 'show_image', 'image_alt', 'display_mode']
display_values = {
"verified": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge',
'verified'],
"honor": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'],
"audit": ["", "", False, '', 'audit'],
"professional": ["You're enrolled as a professional education student", "Professional Ed", False, '',
'professional'],
"no-id-professional": ["You're enrolled as a professional education student", "Professional Ed", False, '',
'professional'],
}
return dict(list(zip(dict_keys, display_values.get(mode))))
def test_expiration_datetime_explicitly_set(self):
""" Verify that setting the expiration_date property sets the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate', 10)
now_dt = now()
verified_mode.expiration_datetime = now_dt
assert verified_mode.expiration_datetime_is_explicit
assert verified_mode.expiration_datetime == now_dt
def test_expiration_datetime_not_explicitly_set(self):
""" Verify that setting the _expiration_date property does not set the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate', 10)
now_dt = now()
verified_mode._expiration_datetime = now_dt # pylint: disable=protected-access
assert not verified_mode.expiration_datetime_is_explicit
assert verified_mode.expiration_datetime == now_dt
def test_expiration_datetime_explicitly_set_to_none(self):
""" Verify that setting the _expiration_date property does not set the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate', 10)
assert not verified_mode.expiration_datetime_is_explicit
verified_mode.expiration_datetime = None
assert not verified_mode.expiration_datetime_is_explicit
assert verified_mode.expiration_datetime is None
@ddt.data(
(False, CourseMode.AUDIT, False),
(False, CourseMode.HONOR, True),
(False, CourseMode.VERIFIED, True),
(False, CourseMode.CREDIT_MODE, True),
(False, CourseMode.PROFESSIONAL, True),
(False, CourseMode.NO_ID_PROFESSIONAL_MODE, True),
(True, CourseMode.AUDIT, False),
(True, CourseMode.HONOR, False),
(True, CourseMode.VERIFIED, True),
(True, CourseMode.CREDIT_MODE, True),
(True, CourseMode.PROFESSIONAL, True),
(True, CourseMode.NO_ID_PROFESSIONAL_MODE, True),
)
@ddt.unpack
def test_eligible_for_cert(self, disable_honor_cert, mode_slug, expected_eligibility):
"""Verify that non-audit modes are eligible for a cert."""
with override_settings(FEATURES={'DISABLE_HONOR_CERTIFICATES': disable_honor_cert}):
assert CourseMode.is_eligible_for_certificate(mode_slug) == expected_eligibility
@ddt.data(
(CourseMode.AUDIT, False),
(CourseMode.HONOR, False),
(CourseMode.VERIFIED, True),
(CourseMode.CREDIT_MODE, False),
(CourseMode.PROFESSIONAL, True),
(CourseMode.NO_ID_PROFESSIONAL_MODE, False),
(CourseMode.MASTERS, False),
)
@ddt.unpack
def test_verified_min_price(self, mode_slug, is_error_expected):
"""Verify that verified modes have a price."""
try:
self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), min_price=0)
except ValidationError:
assert is_error_expected, 'Did not expect a ValidationError to be thrown.'
else:
assert not is_error_expected, 'Expected a ValidationError to be thrown.'
@ddt.data(
([], False),
([CourseMode.VERIFIED, CourseMode.AUDIT], False),
([CourseMode.MASTERS], True),
([CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.MASTERS], True)
)
@ddt.unpack
def test_contains_masters_mode(self, available_modes, expected_contains_masters_mode):
for mode in available_modes:
self.create_mode(mode, mode, 10)
modes = CourseMode.modes_for_course_dict(self.course_key)
assert CourseMode.contains_masters_mode(modes) == expected_contains_masters_mode
@ddt.data(
([], False),
([CourseMode.VERIFIED, CourseMode.AUDIT], False),
([CourseMode.MASTERS], True),
([CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.MASTERS], False)
)
@ddt.unpack
def test_is_masters_only(self, available_modes, expected_is_masters_only):
for mode in available_modes:
self.create_mode(mode, mode, 10)
assert CourseMode.is_masters_only(self.course_key) == expected_is_masters_only
class TestCourseOverviewIntegration(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
def test_course_overview_version_update(self):
course = CourseFactory.create()
course_overview = CourseOverview.get_from_id(course.id)
course_overview.version -= 1
course_overview.save()
course_mode = CourseModeFactory.create(course_id=course_overview.id)
assert CourseMode.objects.filter(pk=course_mode.pk).exists()
CourseOverview.get_from_id(course.id)
assert CourseMode.objects.filter(pk=course_mode.pk).exists()
class TestDisplayPrices(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
@override_settings(PAID_COURSE_REGISTRATION_CURRENCY=["USD", "$"])
def test_get_cosmetic_display_price(self):
"""
Check that get_cosmetic_display_price() returns the correct price given its inputs.
"""
course = CourseFactory.create()
registration_price = 99
course.cosmetic_display_price = 10
with patch(
'common.djangoapps.course_modes.models.CourseMode.min_course_price_for_currency',
return_value=registration_price,
):
# Since registration_price is set, it overrides the cosmetic_display_price and should be returned
assert get_cosmetic_display_price(course) == '$99'
registration_price = 0
with patch(
'common.djangoapps.course_modes.models.CourseMode.min_course_price_for_currency',
return_value=registration_price,
):
# Since registration_price is not set, cosmetic_display_price should be returned
assert get_cosmetic_display_price(course) == '$10'
course.cosmetic_display_price = 0
# Since both prices are not set, there is no price, thus "Free"
assert get_cosmetic_display_price(course) == 'Free'