Files
edx-platform/lms/djangoapps/commerce/tests/test_utils.py
Usama Sadiq e1747f3844 build: add support for Python 3.12 (#37160)
* build: add support for Python 3.12
2025-12-15 12:12:29 +05:00

383 lines
15 KiB
Python

"""Tests of commerce utilities."""
import json
from unittest.mock import patch
from urllib.parse import urlencode
import ddt
import httpretty
from django.conf import settings
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from opaque_keys.edx.locator import CourseLocator
from waffle.testutils import override_switch
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService, refund_entitlement, refund_seat
from openedx.core.djangolib.testing.utils import skip_unless_lms
from openedx.core.lib.log_utils import audit_log
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
# Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection
if settings.ROOT_URLCONF == 'lms.urls':
from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory
def update_commerce_config(enabled=False, checkout_page='/test_basket/add/'):
""" Enable / Disable CommerceConfiguration model """
CommerceConfiguration.objects.create(
checkout_on_ecommerce_service=enabled,
basket_checkout_page=checkout_page,
)
class AuditLogTests(TestCase):
"""Tests of the commerce audit logging helper."""
@patch('openedx.core.lib.log_utils.log')
def test_log_message(self, mock_log):
"""Verify that log messages are constructed correctly."""
audit_log('foo', qux='quux', bar='baz')
# Verify that the logged message contains comma-separated
# key-value pairs ordered alphabetically by key.
message = 'foo: bar="baz", qux="quux"'
mock_log.info.assert_called_with(message)
@ddt.ddt
class EcommerceServiceTests(TestCase):
"""Tests for the EcommerceService helper class."""
def setUp(self):
self.request_factory = RequestFactory()
self.user = UserFactory.create()
self.request = self.request_factory.get("foo")
update_commerce_config(enabled=True)
super().setUp()
def test_is_enabled(self):
"""Verify that is_enabled() returns True when ecomm checkout is enabled. """
is_enabled = EcommerceService().is_enabled(self.user)
assert is_enabled
config = CommerceConfiguration.current()
config.checkout_on_ecommerce_service = False
config.save()
is_not_enabled = EcommerceService().is_enabled(self.user)
assert not is_not_enabled
# TODO: replace override_switch with override_waffle_switch when the
# DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH will be defined as actual WaffleSwitch. Now
# we have only switch name defined in the settings
@override_switch(settings.DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH, active=True)
def test_is_enabled_activation_requirement_disabled(self):
"""Verify that is_enabled() returns True when ecomm checkout is enabled. """
self.user.is_active = False
self.user.save()
is_enabled = EcommerceService().is_enabled(self.user)
assert is_enabled
@patch('openedx.core.djangoapps.theming.helpers.is_request_in_themed_site')
def test_is_enabled_for_sites(self, is_site):
"""Verify that is_enabled() returns True if used for a site."""
is_site.return_value = True
is_enabled = EcommerceService().is_enabled(self.user)
assert is_enabled
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
def test_ecommerce_url_root(self):
"""Verify that the proper root URL is returned."""
assert EcommerceService().ecommerce_url_root == 'http://ecommerce_url'
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
def test_get_absolute_ecommerce_url(self):
"""Verify that the proper URL is returned."""
url = EcommerceService().get_absolute_ecommerce_url('/test_basket/')
assert url == 'http://ecommerce_url/test_basket/'
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
def test_get_receipt_page_url(self):
"""Verify that the proper Receipt page URL is returned."""
order_number = 'ORDER1'
url = EcommerceService().get_receipt_page_url(order_number)
expected_url = f'http://ecommerce_url/checkout/receipt/?order_number={order_number}'
assert url == expected_url
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
def test_get_order_dashboard_url(self):
"""Verify that the proper order dashboard url is returned."""
url = EcommerceService().get_order_dashboard_url()
expected_url = 'http://ecommerce_url/dashboard/orders/'
assert url == expected_url
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
@ddt.data(
{
'skus': ['TESTSKU']
},
{
'skus': ['TESTSKU1', 'TESTSKU2', 'TESTSKU3']
},
{
'skus': ['TESTSKU'],
'program_uuid': '12345678-9012-3456-7890-123456789012'
},
{
'skus': ['TESTSKU1', 'TESTSKU2', 'TESTSKU3'],
'program_uuid': '12345678-9012-3456-7890-123456789012'
}
)
def test_get_checkout_page_url(self, skus, program_uuid=None):
""" Verify the checkout page URL is properly constructed and returned. """
url = EcommerceService().get_checkout_page_url(*skus, program_uuid=program_uuid)
config = CommerceConfiguration.current()
expected_url = '{root}{basket_url}?{skus}'.format(
basket_url=config.basket_checkout_page,
root=settings.ECOMMERCE_PUBLIC_URL_ROOT,
skus=urlencode({'sku': skus}, doseq=True),
)
if program_uuid:
expected_url = '{expected_url}&basket={program_uuid}'.format(
expected_url=expected_url,
program_uuid=program_uuid
)
assert url == expected_url
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url')
@ddt.data(
{
'skus': ['TESTSKU'],
'enterprise_catalog_uuid': None
},
{
'skus': ['TESTSKU'],
'enterprise_catalog_uuid': '6eca3efb-f3a0-4c08-806f-c6e6b65d61cb'
},
)
@ddt.unpack
def test_get_checkout_page_url_with_enterprise_catalog_uuid(self, skus, enterprise_catalog_uuid):
""" Verify the checkout page URL is properly constructed and returned. """
url = EcommerceService().get_checkout_page_url(
*skus,
catalog=enterprise_catalog_uuid
)
config = CommerceConfiguration.current()
query = {'sku': skus}
if enterprise_catalog_uuid:
query.update({'catalog': enterprise_catalog_uuid})
expected_url = '{root}{basket_url}?{skus}'.format(
basket_url=config.basket_checkout_page,
root=settings.ECOMMERCE_PUBLIC_URL_ROOT,
skus=urlencode(query, doseq=True),
)
assert url == expected_url
@ddt.ddt
@skip_unless_lms
class RefundUtilMethodTests(ModuleStoreTestCase):
"""Tests for Refund Utilities"""
def setUp(self):
super().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')
@patch('lms.djangoapps.commerce.utils.is_commerce_service_configured', return_value=False)
def test_ecommerce_service_not_configured(self, mock_commerce_configured):
course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
refund_success = refund_entitlement(course_entitlement)
assert mock_commerce_configured.is_called
assert not refund_success
@httpretty.activate
def test_no_ecommerce_connection_and_failure(self):
httpretty.register_uri(
httpretty.POST,
settings.ECOMMERCE_API_URL + 'refunds/',
status=404,
body='{}',
content_type='application/json'
)
course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
refund_success = refund_entitlement(course_entitlement)
assert not refund_success
@httpretty.activate
def test_ecommerce_successful_refund(self):
httpretty.register_uri(
httpretty.POST,
settings.ECOMMERCE_API_URL + 'refunds/',
status=201,
body='[1]',
content_type='application/json'
)
httpretty.register_uri(
httpretty.PUT,
settings.ECOMMERCE_API_URL + 'refunds/1/process/',
status=200,
body=json.dumps({
"id": 9,
"created": "2017-12-21T18:23:49.468298Z",
"modified": "2017-12-21T18:24:02.741426Z",
"total_credit_excl_tax": "100.00",
"currency": "USD",
"status": "Complete",
"order": 15,
"user": 5
}),
content_type='application/json'
)
course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
refund_success = refund_entitlement(course_entitlement)
assert refund_success
@httpretty.activate
@patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True)
def test_ecommerce_refund_failed_process_notification_sent(self, mock_send_notification):
httpretty.register_uri(
httpretty.POST,
settings.ECOMMERCE_API_URL + 'refunds/',
status=201,
body='[1]',
content_type='application/json'
)
httpretty.register_uri(
httpretty.PUT,
settings.ECOMMERCE_API_URL + 'refunds/1/process/',
status=400,
body='{}',
content_type='application/json'
)
course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
refund_success = refund_entitlement(course_entitlement)
assert mock_send_notification.is_called
call_args = list(mock_send_notification.call_args)
assert call_args[0] == (course_entitlement.user, [1])
assert refund_success
@httpretty.activate
@patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True)
def test_ecommerce_refund_not_verified_notification_for_entitlement(self, mock_send_notification):
"""
Note that we are currently notifying Support whenever a refund require approval for entitlements as
Entitlements are only available in paid modes. This test should be updated if this logic changes
in the future.
PROFESSIONAL mode is used here although we never auto approve PROFESSIONAL refunds right now
"""
httpretty.register_uri(
httpretty.POST,
settings.ECOMMERCE_API_URL + 'refunds/',
status=201,
body='[1]',
content_type='application/json'
)
httpretty.register_uri(
httpretty.PUT,
settings.ECOMMERCE_API_URL + 'refunds/1/process/',
status=400,
body='{}',
content_type='application/json'
)
course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.PROFESSIONAL)
refund_success = refund_entitlement(course_entitlement)
assert mock_send_notification.is_called
call_args = list(mock_send_notification.call_args)
assert call_args[0] == (course_entitlement.user, [1])
assert refund_success
@httpretty.activate
@patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True)
def test_ecommerce_refund_send_notification_failed(self, mock_send_notification):
httpretty.register_uri(
httpretty.POST,
settings.ECOMMERCE_API_URL + 'refunds/',
status=201,
body='[1]',
content_type='application/json'
)
httpretty.register_uri(
httpretty.PUT,
settings.ECOMMERCE_API_URL + 'refunds/1/process/',
status=400,
body='{}',
content_type='application/json'
)
mock_send_notification.side_effect = NotImplementedError
course_entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
refund_success = refund_entitlement(course_entitlement)
assert mock_send_notification.is_called
call_args = list(mock_send_notification.call_args)
assert call_args[0] == (course_entitlement.user, [1])
assert not refund_success
@httpretty.activate
@ddt.data(
(["verified", "audit"], "audit"),
(["professional"], "professional"),
)
@ddt.unpack
def test_mode_change_after_refund_seat(self, course_modes, new_mode):
"""
Test if a course seat is refunded student is enrolled into default course mode
unless no default mode available.
"""
course_id = CourseLocator('test_org', 'test_course_number', 'test_run')
CourseMode.objects.all().delete()
for course_mode in course_modes:
CourseModeFactory.create(
course_id=course_id,
mode_slug=course_mode,
mode_display_name=course_mode,
)
httpretty.register_uri(
httpretty.POST,
settings.ECOMMERCE_API_URL + 'refunds/',
status=201,
body='[1]',
content_type='application/json'
)
httpretty.register_uri(
httpretty.PUT,
settings.ECOMMERCE_API_URL + 'refunds/1/process/',
status=200,
body=json.dumps({
"id": 9,
"created": "2017-12-21T18:23:49.468298Z",
"modified": "2017-12-21T18:24:02.741426Z",
"total_credit_excl_tax": "100.00",
"currency": "USD",
"status": "Complete",
"order": 15,
"user": 5
}),
content_type='application/json'
)
enrollment = CourseEnrollment.enroll(self.user, course_id, mode=course_modes[0])
refund_success = refund_seat(enrollment, True)
enrollment = CourseEnrollment.get_or_create_enrollment(self.user, course_id)
assert refund_success
assert enrollment.mode == new_mode