diff --git a/ecommerce/subscriptions/api/v2/serializers.py b/ecommerce/subscriptions/api/v2/serializers.py index ce46d9bbd11..a4e0b7761bb 100644 --- a/ecommerce/subscriptions/api/v2/serializers.py +++ b/ecommerce/subscriptions/api/v2/serializers.py @@ -92,7 +92,7 @@ def get_is_course_payments_enabled(self, product): class Meta: model = Product fields = [ - 'id', 'title', 'date_created', 'subscription_type', 'subscription_actual_price', 'subscription_price', + 'title', 'subscription_type', 'subscription_actual_price', 'subscription_price', 'subscription_status', 'display_order', 'partner_sku', 'is_course_payments_enabled' ] @@ -325,6 +325,6 @@ def _create_conditional_offer(self, subscription, partner): class Meta: model = Product fields = ( - 'id', 'title', 'date_created', 'date_updated', 'subscription_type', 'subscription_actual_price', 'subscription_price', 'subscription_status', + 'title', 'subscription_type', 'subscription_actual_price', 'subscription_price', 'subscription_status', 'number_of_courses', 'subscription_duration_value', 'subscription_duration_unit' ) diff --git a/ecommerce/subscriptions/api/v2/tests/__init__.py b/ecommerce/subscriptions/api/v2/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ecommerce/subscriptions/api/v2/tests/factories.py b/ecommerce/subscriptions/api/v2/tests/factories.py new file mode 100644 index 00000000000..2dac92897aa --- /dev/null +++ b/ecommerce/subscriptions/api/v2/tests/factories.py @@ -0,0 +1,161 @@ +""" +Subscription related factories. +""" +from datetime import date +from factory import post_generation, RelatedFactory, SubFactory +from factory.django import DjangoModelFactory +from factory.fuzzy import ( + FuzzyChoice, + FuzzyDate, + FuzzyFloat, + FuzzyInteger, + FuzzyText, +) +from oscar.core.loading import get_model +from oscar.test.factories import ( + BenefitFactory, + ConditionFactory, + ConditionalOfferFactory, + ProductAttributeValueFactory, + ProductClassFactory, +) +from random import choice + +from ecommerce.subscriptions.benefits import SubscriptionBenefit +from ecommerce.subscriptions.conditions import SubscriptionCondition +from ecommerce.subscriptions.custom import class_path + +Basket = get_model('basket', 'Basket') +BasketAttribute = get_model('basket', 'BasketAttribute') +BasketAttributeType = get_model('basket', 'BasketAttributeType') +ConditionalOffer = get_model('offer', 'ConditionalOffer') +Product = get_model('catalogue', 'Product') + +SUBSCRIPTION_ATTRIBUTES_VALUES = lambda subscription: { + 'limited-access': { + 'number_of_courses': FuzzyInteger(1, 10).fuzz(), + 'subscription_duration_value': FuzzyInteger(1, 10).fuzz(), + 'subscription_duration_unit': choice(subscription.attr.get_attribute_by_code('subscription_duration_unit').option_group.options.all()), + 'subscription_actual_price': FuzzyFloat(100.0, 500.0).fuzz(), + 'subscription_price': FuzzyFloat(10.0, 99.0).fuzz(), + 'subscription_display_order': FuzzyInteger(1, 5).fuzz(), + 'subscription_status': FuzzyChoice((True, False)).fuzz() + }, + 'full-access-courses': { + 'subscription_duration_value': FuzzyInteger(1, 10).fuzz(), + 'subscription_duration_unit': choice(subscription.attr.get_attribute_by_code('subscription_duration_unit').option_group.options.all()), + 'subscription_actual_price': FuzzyFloat(100.0, 500.0).fuzz(), + 'subscription_price': FuzzyFloat(10.0, 99.0).fuzz(), + 'subscription_display_order': FuzzyInteger(1, 5).fuzz(), + 'subscription_status': FuzzyChoice((True, False)).fuzz() + }, + 'full-access-time-period': { + 'number_of_courses': FuzzyInteger(1, 10).fuzz(), + 'subscription_actual_price': FuzzyFloat(100.0, 500.0).fuzz(), + 'subscription_price': FuzzyFloat(10.0, 99.0).fuzz(), + 'subscription_display_order': FuzzyInteger(1, 5).fuzz(), + 'subscription_status': FuzzyChoice((True, False)).fuzz() + }, + 'lifetime-access': { + 'subscription_actual_price': FuzzyFloat(100.0, 500.0).fuzz(), + 'subscription_price': FuzzyFloat(10.0, 99.0).fuzz(), + 'subscription_display_order': FuzzyInteger(1, 5).fuzz(), + 'subscription_status': FuzzyChoice((True, False)).fuzz() + } +} + + +class SubscriptionFactory(DjangoModelFactory): + """ + Factory class for Subscription product. + """ + class Meta(object): + model = Product + + title = FuzzyText() + product_class = SubFactory(ProductClassFactory) + stockrecords = RelatedFactory('oscar.test.factories.StockRecordFactory', 'product') + + @post_generation + def product_attributes(subscription, create, extracted, **kwargs): + """ + Post generation product to set required subscription product attributes. + """ + if not create: + return + + subscription_type = kwargs.get('subscription_type') + if subscription_type and not extracted: + subscription_type_attribute = subscription.attr.get_attribute_by_code('subscription_type') + subscription_type_option = subscription_type_attribute.option_group.options.get(option=subscription_type) + setattr(subscription.attr, 'subscription_type', subscription_type_option) + for attribute_key, attribute_value in SUBSCRIPTION_ATTRIBUTES_VALUES(subscription)[subscription_type].items(): + setattr(subscription.attr, attribute_key, attribute_value) + + elif extracted: + subscription_type = extracted.get('subscription_type') + if subscription_type: + subscription_type_attribute = subscription.attr.get_attribute_by_code('subscription_type') + subscription_type_option = subscription_type_attribute.option_group.options.get(option=subscription_type) + setattr(subscription.attr, 'subscription_type', subscription_type_option) + for attribute_key in SUBSCRIPTION_ATTRIBUTES_VALUES(subscription)[subscription_type].keys(): + if attribute_key == 'subscription_duration_unit': + duration_unit_attribute = subscription.attr.get_attribute_by_code('subscription_duration_unit') + duration_unit_option = duration_unit_attribute.option_group.options.get(option=extracted.get(attribute_key)) + setattr(subscription.attr, attribute_key, duration_unit_option) + else: + setattr(subscription.attr, attribute_key, extracted.get(attribute_key)) + + else: + subscription_type = choice(subscription.attr.get_attribute_by_code('subscription_type').option_group.options.all()) + setattr(subscription.attr, 'subscription_type', subscription_type) + for attribute_key, attribute_value in SUBSCRIPTION_ATTRIBUTES_VALUES(subscription)[subscription_type.option].items(): + setattr(subscription.attr, attribute_key, attribute_value) + + setattr(subscription.attr, 'subscription_status', kwargs.get('subscription_status', subscription.attr.subscription_status)) + subscription.attr.save() + + +class SubscriptionConditionFactory(ConditionFactory): + """ + Factory class for "SubscriptionCondition". + """ + class Meta(object): + model = SubscriptionCondition + + range = None + type = '' + value = None + proxy_class = class_path(SubscriptionCondition) + + +class SubscriptionBenefitFactory(BenefitFactory): + """ + Factory class for "SubscriptionBenefit". + """ + range = None + type = '' + value = 100 + proxy_class = class_path(SubscriptionBenefit) + + +class SubscriptionOfferFactory(ConditionalOfferFactory): + """ + Factory class for Subscription Conditional offer. + """ + benefit = SubFactory(SubscriptionBenefitFactory) + condition = SubFactory(SubscriptionConditionFactory) + offer_type = ConditionalOffer.SITE + status = ConditionalOffer.OPEN + + +class BasketAttributeFactory(DjangoModelFactory): + """ + Factory class for "BasketAttribute". + """ + class Meta(object): + model = BasketAttribute + + basket = SubFactory(Basket) + attribute_type = SubFactory(BasketAttributeType) + value_text = FuzzyText() diff --git a/ecommerce/subscriptions/api/v2/tests/mixins.py b/ecommerce/subscriptions/api/v2/tests/mixins.py new file mode 100644 index 00000000000..15645418103 --- /dev/null +++ b/ecommerce/subscriptions/api/v2/tests/mixins.py @@ -0,0 +1,161 @@ +""" +Subscription product mixins. +""" +from oscar.test.factories import ( + AttributeOptionFactory, + AttributeOptionGroupFactory, + CategoryFactory, + ProductAttributeFactory, + ProductAttributeValueFactory, + ProductCategoryFactory, + ProductClassFactory, + ProductFactory, +) +from oscar.core.loading import get_model + +from ecommerce.core.constants import SUBSCRIPTION_CATEGORY_NAME, SUBSCRIPTION_PRODUCT_CLASS_NAME +from ecommerce.subscriptions.api.v2.tests.constants import SUBSCRIPTION_DURATION_UNIT_OPTIONS, SUBSCRIPTION_TYPES +from ecommerce.subscriptions.api.v2.tests.factories import SubscriptionFactory + +Category = get_model('catalogue', 'Category') +ProductAttribute = get_model('catalogue', 'ProductAttribute') + + +class SubscriptionProductMixin(object): + """ + Mixin to provide a method to create a subscription for tests. + + This mixin provides a method that creates a subscription product and sets up + all required attributes and relevant class instances. + """ + SUBSRIPTION_ATTRIBUTES = [ + { + 'name': 'Number of Courses', + 'code': 'number_of_courses', + 'type': ProductAttribute.INTEGER, + }, + { + 'name': 'Subscription Actual Price', + 'code': 'subscription_actual_price', + 'type': ProductAttribute.FLOAT, + }, + { + 'name': 'Subscription Display Order', + 'code': 'subscription_display_order', + 'type': ProductAttribute.INTEGER, + }, + { + 'name': 'Subscription Duration unit', + 'code': 'subscription_duration_unit', + 'type': ProductAttribute.OPTION, + }, + { + 'name': 'Subscription Duration value', + 'code': 'subscription_duration_value', + 'type': ProductAttribute.INTEGER, + }, + { + 'name': 'Subscription Price', + 'code': 'subscription_price', + 'type': ProductAttribute.FLOAT, + }, + { + 'name': 'Subscription Status', + 'code': 'subscription_status', + 'type': ProductAttribute.BOOLEAN, + }, + { + 'name': 'Subscription Type', + 'code': 'subscription_type', + 'type': ProductAttribute.OPTION, + }, + ] + SUBSCRIPTION_DURATION_UNIT_GROUP_NAME = 'Subscription Duration Units' + SUBSCRIPTION_TYPE_GROUP_NAME = 'Subscription Access Types' + + def _create_subscription_type_attribute_option_group(self): + """ + Private method to create subscription type attribute group. + """ + subscription_type_option_group = AttributeOptionGroupFactory(name=self.SUBSCRIPTION_TYPE_GROUP_NAME) + for option in SUBSCRIPTION_TYPES: + AttributeOptionFactory(option=option, group=subscription_type_option_group) + + return subscription_type_option_group + + def _create_subscription_duration_unit_attribute_option_group(self): + """ + Private method to create subscription duration unit attribute group. + """ + duration_unit_option_group = AttributeOptionGroupFactory(name=self.SUBSCRIPTION_DURATION_UNIT_GROUP_NAME) + for option in SUBSCRIPTION_DURATION_UNIT_OPTIONS: + AttributeOptionFactory(option=option, group=duration_unit_option_group) + + return duration_unit_option_group + + def _create_subscription_product_class(self): + """ + Private method to set up subscription product class. + + Creates the subscription class, sets all of subscription attributes and attribute option groups. + """ + subscription_product_class = ProductClassFactory( + name=SUBSCRIPTION_PRODUCT_CLASS_NAME, + requires_shipping=False, + track_stock=False + ) + for attribute in self.SUBSRIPTION_ATTRIBUTES: + product_attribute = ProductAttributeFactory( + name=attribute['name'], + code=attribute['code'], + type=attribute['type'], + product_class=subscription_product_class, + ) + + if product_attribute.code == 'subscription_type': + product_attribute.option_group = self._create_subscription_type_attribute_option_group() + + if product_attribute.code == 'subscription_duration_unit': + product_attribute.option_group = self._create_subscription_duration_unit_attribute_option_group() + + product_attribute.save() + + return subscription_product_class + + def create_subscription(self, attributes=None, subscription_type=None, subscription_status=True, **kwargs): + """ + Public method to create a dummy subscription. + + This method creates a subscription with the given parameters (type, status etc.) as well as the + subscription category and subscription product category factory. + + Arguments: + attributes (dict): Dict of subscription attributes to create a custom subscription + subscription_type (str): Subscription type string to create a specific type of subscription + subscription_status (bool): Subscription type to set for the subscription + **kwargs: Additional kwargs e.g. stockrecord partner to set for the subscription + + Returns: + Product: A dummy subscription product with all relevant product attributes set + """ + Category.objects.all().delete() + if subscription_type not in SUBSCRIPTION_TYPES: + subscription_type = None + + subscription_product_class = self._create_subscription_product_class() + subscription_category = CategoryFactory( + name=SUBSCRIPTION_CATEGORY_NAME, + ) + subscription = SubscriptionFactory( + product_class=subscription_product_class, + product_attributes=attributes, + product_attributes__subscription_type=subscription_type, + product_attributes__subscription_status=subscription_status, + **kwargs + ) + ProductCategoryFactory( + product=subscription, + category=subscription_category + ) + + return subscription diff --git a/ecommerce/subscriptions/api/v2/tests/test_serializers.py b/ecommerce/subscriptions/api/v2/tests/test_serializers.py new file mode 100644 index 00000000000..d93de6e4e55 --- /dev/null +++ b/ecommerce/subscriptions/api/v2/tests/test_serializers.py @@ -0,0 +1,101 @@ +""" +Unit tests for subscription API serializers. +""" +import ddt + +from ecommerce.subscriptions.api.v2.serializers import ( + SubscriptionListSerializer, + SubscriptionSerializer, +) +from ecommerce.subscriptions.api.v2.tests.constants import ( + FULL_ACCESS_COURSES, + FULL_ACCESS_TIME_PERIOD, + LIFETIME_ACCESS, + LIMITED_ACCESS, + SUBSCRIPTION_TYPES, +) +from ecommerce.subscriptions.api.v2.tests.mixins import SubscriptionProductMixin +from ecommerce.tests.testcases import TestCase + + +class SubscriptionListSerializerTests(SubscriptionProductMixin, TestCase): + """ + Unit tests for "SubscriptionListSerializer". + """ + + def test_list_serializer(self): + """ + Verify that subscriptions list serializer correctly serializes subscriptions. + """ + context = { + 'request': self.request, + 'partner': self.partner + } + subscription = self.create_subscription() + expected_data = { + 'title': subscription.title, + 'subscription_type': subscription.attr.subscription_type.option, + 'subscription_actual_price': subscription.attr.subscription_actual_price, + 'subscription_price': subscription.attr.subscription_price, + 'subscription_status': subscription.attr.subscription_status, + 'display_order': subscription.attr.subscription_display_order, + 'partner_sku': subscription.stockrecords.first().partner_sku, + 'is_course_payments_enabled': self.request.site.siteconfiguration.enable_course_payments + } + subscription_serializer = SubscriptionListSerializer(subscription, context=context) + self.assertEqual(subscription_serializer.data, expected_data) + + +@ddt.ddt +class SubscriptionSerializerTests(SubscriptionProductMixin, TestCase): + """ + Unit tests for "SubscriptionSerializer". + """ + @ddt.data( + LIMITED_ACCESS, + FULL_ACCESS_COURSES, + FULL_ACCESS_TIME_PERIOD, + LIFETIME_ACCESS + ) + def test_subscription_serializer(self, subscription_type): + """ + Verify that subscription serializer correctly serializes subscription detail. + """ + context = { + 'request': self.request, + 'partner': self.partner + } + subscription = self.create_subscription(subscription_type=subscription_type) + conditional_attribute_values = { + LIMITED_ACCESS: lambda subscription: { + 'number_of_courses': subscription.attr.number_of_courses, + 'subscription_duration_unit': subscription.attr.subscription_duration_unit.option, + 'subscription_duration_value': subscription.attr.subscription_duration_value + }, + FULL_ACCESS_COURSES: lambda subscription: { + 'number_of_courses': None, + 'subscription_duration_unit': subscription.attr.subscription_duration_unit.option, + 'subscription_duration_value': subscription.attr.subscription_duration_value + }, + FULL_ACCESS_TIME_PERIOD: lambda subscription: { + 'number_of_courses': subscription.attr.number_of_courses, + 'subscription_duration_unit': None, + 'subscription_duration_value': None + }, + LIFETIME_ACCESS: lambda subscription: { + 'number_of_courses': None, + 'subscription_duration_unit': None, + 'subscription_duration_value': None + }, + } + expected_data = { + 'title': subscription.title, + 'subscription_type': subscription_type, + 'subscription_actual_price': subscription.attr.subscription_actual_price, + 'subscription_price': subscription.attr.subscription_price, + 'subscription_status': subscription.attr.subscription_status, + } + expected_data.update(conditional_attribute_values[subscription_type](subscription)) + + subscription_serializer = SubscriptionSerializer(subscription, context=context) + self.assertEqual(subscription_serializer.data, expected_data) diff --git a/ecommerce/subscriptions/api/v2/tests/test_views.py b/ecommerce/subscriptions/api/v2/tests/test_views.py new file mode 100644 index 00000000000..a46df2cc929 --- /dev/null +++ b/ecommerce/subscriptions/api/v2/tests/test_views.py @@ -0,0 +1,127 @@ +""" +Unit tests for subscription API views. +""" +import json +import pytest + +from django.urls import reverse + +from ecommerce.subscriptions.api.v2.tests.constants import LIMITED_ACCESS +from ecommerce.subscriptions.api.v2.tests.mixins import SubscriptionProductMixin +from ecommerce.tests.testcases import TestCase + + +class SubscriptionViewSetTests(SubscriptionProductMixin, TestCase): + """ + Unit tests for "SubscriptionViewSet". + """ + + def test_list(self): + """ + Verify that subscriptions list API returns results correctly. + """ + self.create_subscription(stockrecords__partner=self.site.partner) + expected_keys = [ + 'title', 'subscription_type', 'subscription_actual_price', 'subscription_price', 'subscription_status', + 'display_order', 'partner_sku', 'is_course_payments_enabled' + ] + request_url = reverse('api:v2:subscriptions-list') + response = self.client.get(request_url) + self.assertEqual(response.status_code, 200) + self.assertGreater(response.data.get('count'), 0) + self.assertEqual(response.data.get('results')[0].keys(), expected_keys) + + def test_list_active(self): + """ + Verify that subscriptions list API returns results correctly with active filter. + """ + request_url = reverse('api:v2:subscriptions-list') + '?filter_active=true' + self.create_subscription(stockrecords__partner=self.site.partner) + response = self.client.get(request_url) + self.assertEqual(response.status_code, 200) + self.assertGreater(response.data.get('count'), 0) + self.create_subscription(stockrecords__partner=self.site.partner, subscription_status=False) + response = self.client.get(request_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data.get('count'), 1) + + def test_retrieve(self): + """ + Verify that subscriptions API returns results correctly. + """ + subscription = self.create_subscription(stockrecords__partner=self.site.partner) + request_url = reverse('api:v2:subscriptions-detail', args=[subscription.id]) + response = self.client.get(request_url) + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.data) + + def test_create(self): + """ + Verify that subscriptions API correctly creates a new subscription. + """ + subscription_data = { + 'title': 'Test subscription', + 'subscription_type': LIMITED_ACCESS, + 'subscription_actual_price': 100.00, + 'subscription_price': 50.00, + 'subscription_active_status': 'true', + 'number_of_courses': 4, + 'subscription_duration_value': 4, + 'subscription_duration_unit': 'months', + 'subscription_display_order': 1 + } + request_url = reverse('api:v2:subscriptions-list') + response = self.client.get(request_url) + self.assertEqual(response.data.get('count'), 0) + response = self.client.post(request_url, data=subscription_data) + self.assertEqual(response.status_code, 201) + response = self.client.get(request_url) + self.assertGreater(response.data.get('count'), 0) + + def test_update(self): + """ + Verify that subscriptions API correctly updates a new subscription. + """ + subscription = self.create_subscription(stockrecords__partner=self.site.partner) + subscription_data = { + 'title': 'Test subscription', + 'subscription_type': LIMITED_ACCESS, + 'subscription_actual_price': 100.00, + 'subscription_price': 50.00, + 'subscription_active_status': 'inactive', + 'number_of_courses': 4, + 'subscription_duration_value': 4, + 'subscription_duration_unit': 'months', + 'subscription_display_order': 1 + } + request_url = reverse('api:v2:subscriptions-detail', args=[subscription.id]) + response = self.client.put(request_url, data=json.dumps(subscription_data), content_type='application/json') + self.assertEqual(response.status_code, 200) + response = self.client.get(request_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data.get('title'), subscription_data.get('title')) + self.assertFalse(response.data.get('subscription_status')) + + def test_toggle_course_payments(self): + """ + Verify that subscriptions API correctly toggles course payments flag. + """ + request_url = reverse('api:v2:subscriptions-toggle-course-payments-list') + expected_data = { + 'course_payments': not self.site.siteconfiguration.enable_course_payments + } + response = self.client.post(request_url) + self.assertEqual(response.data, expected_data) + self.assertEqual(response.status_code, 200) + + def test_course_payments_status(self): + """ + Verify that subscriptions API correctly returns course payments flag status. + """ + request_url = reverse('api:v2:subscriptions-course-payments-status-list') + expected_data = { + 'course_payments': self.site.siteconfiguration.enable_course_payments + } + response = self.client.get(request_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, expected_data) diff --git a/ecommerce/subscriptions/conditions.py b/ecommerce/subscriptions/conditions.py index 5c932dc45bf..e58a4d980f0 100644 --- a/ecommerce/subscriptions/conditions.py +++ b/ecommerce/subscriptions/conditions.py @@ -43,9 +43,6 @@ def is_satisfied(self, offer, basket): # pylint: disable=unused-argument if not basket.owner: return False - if basket.site.partner != offer.partner: - return False - valid_user_subscription = get_valid_user_subscription(basket.owner, basket.site) if not valid_user_subscription: return False diff --git a/ecommerce/subscriptions/modules.py b/ecommerce/subscriptions/modules.py index 335f5d611b7..efd01118275 100644 --- a/ecommerce/subscriptions/modules.py +++ b/ecommerce/subscriptions/modules.py @@ -55,7 +55,7 @@ def get_supported_lines(self, lines): lines (List of Lines): Order Lines, associated with purchased products in an Order. Returns: - A supported list of unmodified lines associated with "Susbcription" products. + A supported list of unmodified lines associated with "Subscription" products. """ return [line for line in lines if self.supports_line(line)] @@ -144,19 +144,13 @@ def fulfill_product(self, order, lines, email_opt_in=False): return order, lines def revoke_line(self, line): - try: - logger.info('Attempting to revoke fulfillment of Line [%d]...', line.id) - audit_log( - 'line_revoked', - order_line_id=line.id, - order_number=line.order.number, - product_class=line.product.get_product_class().name, - user_id=line.order.user.id - ) - - return True - except Exception: # pylint: disable=broad-except - logger.exception('Failed to revoke fulfillment of Line [%d].', line.id) - - return False + audit_log( + 'line_revoked', + order_line_id=line.id, + order_number=line.order.number, + product_class=line.product.get_product_class().name, + user_id=line.order.user.id + ) + + return True diff --git a/ecommerce/subscriptions/tests/__init__.py b/ecommerce/subscriptions/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ecommerce/subscriptions/tests/test_benefits.py b/ecommerce/subscriptions/tests/test_benefits.py new file mode 100644 index 00000000000..eaa8fd5bffb --- /dev/null +++ b/ecommerce/subscriptions/tests/test_benefits.py @@ -0,0 +1,14 @@ +""" +Unit tests for subscription benefit. +""" +from ecommerce.extensions.test import mixins +from ecommerce.subscriptions.api.v2.tests.factories import SubscriptionBenefitFactory +from ecommerce.tests.testcases import TestCase + + +class SubscriptionBenefitTests(mixins.BenefitTestMixin, TestCase): + """ + Unit tests for "SubscriptionBenefit". + """ + factory_class = SubscriptionBenefitFactory + name_format = '{value}% subscription discount' diff --git a/ecommerce/subscriptions/tests/test_conditions.py b/ecommerce/subscriptions/tests/test_conditions.py new file mode 100644 index 00000000000..0760758ab4d --- /dev/null +++ b/ecommerce/subscriptions/tests/test_conditions.py @@ -0,0 +1,129 @@ +""" +Unit tests for subscription condition. +""" +import mock +from oscar.core.loading import get_model +from oscar.test.factories import BasketFactory + +from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin +from ecommerce.subscriptions.api.v2.tests.factories import ( + SubscriptionConditionFactory, + SubscriptionOfferFactory +) +from ecommerce.subscriptions.api.v2.tests.mixins import SubscriptionProductMixin +from ecommerce.subscriptions.api.v2.tests.utils import mock_user_subscription +from ecommerce.tests.factories import ProductFactory +from ecommerce.tests.testcases import TestCase + +BasketAttribute = get_model('basket', 'BasketAttribute') +Product = get_model('catalogue', 'Product') +ProductClass = get_model('catalogue', 'ProductClass') + +LOGGER_NAME = 'ecommerce.subscriptions.conditions' + + +@mock.patch('ecommerce.subscriptions.conditions.get_valid_user_subscription') +class SubscriptionConditionTests(DiscoveryTestMixin, SubscriptionProductMixin, TestCase): + """ + Unit tests for "SubscriptionCondition". + """ + + def setUp(self): + """ + Setup initial test data. + """ + super(SubscriptionConditionTests, self).setUp() + self.condition = SubscriptionConditionFactory() + self.offer = SubscriptionOfferFactory(partner=self.partner, condition=self.condition) + self.basket = BasketFactory(site=self.site, owner=self.create_user()) + ___, seat = self.create_course_and_seat(partner=self.partner) + self.basket.add_product( + seat, + 1 + ) + + def test_name(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription condition is set correctly. + """ + mocked_user_subscriptions_api_response.return_value = None + expected = 'Basket includes a subscription' + self.assertEqual(self.condition.name, expected) + + def test_is_satisfied_with_empty_basket(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied on an empty basket. + """ + mocked_user_subscriptions_api_response.return_value = None + self.basket.flush() + self.assertTrue(self.basket.is_empty) + self.assertFalse(self.condition.is_satisfied(self.offer, self.basket)) + + def test_is_satisfied_without_owner(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied on a basket without owner. + """ + mocked_user_subscriptions_api_response.return_value = None + self.basket.owner = None + self.assertFalse(self.condition.is_satisfied(self.offer, self.basket)) + + def test_is_satisfied_with_different_basket_and_offer_partner(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied if basket and offer have different partners. + """ + mocked_user_subscriptions_api_response.return_value = None + self.basket.site.partner = None + self.assertFalse(self.condition.is_satisfied(self.offer, self.basket)) + + def test_is_satisfied_with_no_valid_subscription(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied if not valid user subscription exists for the user. + """ + mocked_user_subscriptions_api_response.return_value = None + self.assertFalse(self.condition.is_satisfied(self.offer, self.basket)) + + def test_is_satisfied_with_valid_conditions(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is applied if all the conditions are met. + """ + mocked_user_subscriptions_api_response.return_value = mock_user_subscription() + self.assertTrue(self.condition.is_satisfied(self.offer, self.basket)) + + def test_get_applicable_lines_with_empty_basket(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied if basket is empty. + """ + mocked_user_subscriptions_api_response.return_value = None + self.basket.flush() + self.assertEqual(self.condition.get_applicable_lines(self.offer, self.basket), []) + + def test_get_applicable_lines_without_seat_product(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied if basket does not contain a seat product. + """ + mocked_user_subscriptions_api_response.return_value = None + self.basket.flush() + self.basket.add_product( + ProductFactory(stockrecords__partner=self.partner), + 1 + ) + self.assertEqual(self.condition.get_applicable_lines(self.offer, self.basket), []) + + def test_get_applicable_lines_with_seat_product_without_basket_attribute(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is not applied if subscription basket attribute is not set. + """ + mocked_user_subscriptions_api_response.return_value = None + self.assertEqual(self.condition.get_applicable_lines(self.offer, self.basket), []) + + def test_get_applicable_lines_with_seat_product_with_basket_attribute(self, mocked_user_subscriptions_api_response): + """ + Verify that subscription offer is applied if basket contains a seat product and subscription attribute is set. + """ + mocked_user_subscriptions_api_response.return_value = None + with mock.patch.object(BasketAttribute.objects, 'get') as basket_attribute: + basket_attribute.return_value = {} + applicable_lines = [ + (line.product.stockrecords.first().price_excl_tax, line) for line in self.basket.all_lines() + ] + self.assertEqual(self.condition.get_applicable_lines(self.offer, self.basket), applicable_lines) diff --git a/ecommerce/subscriptions/tests/test_modules.py b/ecommerce/subscriptions/tests/test_modules.py new file mode 100644 index 00000000000..b7380ec8c3f --- /dev/null +++ b/ecommerce/subscriptions/tests/test_modules.py @@ -0,0 +1,181 @@ +""" +Unit tests for subscription module. +""" +import ddt +import httpretty +import json +import mock +from oscar.core.loading import get_model +from oscar.test.factories import ( + BasketFactory, + UserFactory, +) +from requests.exceptions import ConnectionError, Timeout +from testfixtures import LogCapture + +from django.test import override_settings + +from ecommerce.extensions.fulfillment.status import LINE +from ecommerce.extensions.fulfillment.tests.mixins import FulfillmentTestMixin +from ecommerce.extensions.test.factories import create_order +from ecommerce.subscriptions.api.v2.tests.mixins import SubscriptionProductMixin +from ecommerce.subscriptions.modules import SubscriptionFulfillmentModule +from ecommerce.subscriptions.utils import get_lms_user_subscription_api_url, get_subscription_expiration_date +from ecommerce.tests.factories import ProductFactory +from ecommerce.tests.testcases import TestCase + +LOGGER_NAME = 'ecommerce.extensions.analytics.utils' +JSON = 'application/json' + + +@ddt.ddt +@override_settings(EDX_API_KEY='foo') +class SubscriptionFulfillmentModuleTests(FulfillmentTestMixin, SubscriptionProductMixin, TestCase): + """ + Unit tests for "SubscriptionFulfillmentModule". + """ + + def setUp(self): + """ + Setup initial test data. + """ + super(SubscriptionFulfillmentModuleTests, self).setUp() + + self.user = UserFactory() + self.user.tracking_context = { + 'ga_client_id': 'test-client-id', 'lms_user_id': 'test-user-id', 'lms_ip': '127.0.0.1' + } + self.user.save() + self.subscription = self.create_subscription() + self.basket = BasketFactory(owner=self.user, site=self.site) + self.basket.add_product(self.subscription, 1) + self.order = create_order(number=1, basket=self.basket, user=self.user) + + subscription_type = self.subscription.attr.subscription_type.option + subscription_expiration = get_subscription_expiration_date(self.subscription) + number_of_courses = self.subscription.attribute_values.filter(attribute__code='number_of_courses').first() + self.user_subscription_api_payload = { + 'user': self.order.user.username, + 'subscription_id': self.subscription.id, + 'expiration_date': str(subscription_expiration) if subscription_expiration else subscription_expiration, + 'subscription_type': subscription_type, + 'max_allowed_courses': number_of_courses.value if number_of_courses else None, + } + + def test_get_supported_lines(self): + """ + Verify that "SubscriptionFullfillmentModule" gets lines containing subscription product. + """ + basket = BasketFactory(owner=self.user, site=self.site) + basket.add_product( + ProductFactory(stockrecords__partner=self.partner), + 1 + ) + basket.add_product(self.subscription, 1) + order = create_order(number=2, basket=basket, user=self.user) + supported_lines = SubscriptionFulfillmentModule().get_supported_lines(list(order.lines.all())) + self.assertEqual(1, len(supported_lines)) + + @httpretty.activate + def test_subscription_fulfillment_module_fulfills_successfully(self): + """ + Verify that the subscription product line gets fulfilled successfully. + """ + httpretty.register_uri(httpretty.PUT, get_lms_user_subscription_api_url(self.subscription.id), status=201, body='{}', content_type=JSON) + with LogCapture(LOGGER_NAME) as logs: + SubscriptionFulfillmentModule().fulfill_product(self.order, list(self.order.lines.all())) + + line = self.order.lines.get() + logs.check( + ( + LOGGER_NAME, + 'INFO', + 'line_fulfilled: order_line_id="{}", order_number="{}", product_class="{}", user_id="{}"'.format( + line.id, + line.order.number, + line.product.get_product_class().name, + line.order.user.id, + ) + ) + ) + + self.assertEqual(LINE.COMPLETE, line.status) + + last_request = httpretty.last_request() + actual_body = json.loads(last_request.body) + actual_headers = last_request.headers + + expected_headers = { + 'X-Edx-Ga-Client-Id': self.user.tracking_context['ga_client_id'], + 'X-Forwarded-For': self.user.tracking_context['lms_ip'], + } + + self.assertDictContainsSubset(expected_headers, actual_headers) + self.assertEqual(self.user_subscription_api_payload, actual_body) + + @override_settings(EDX_API_KEY=None) + def test_subscription_fulfillment_module_not_configured(self): + """ + Verify that subscription fulfillment fails if valid configuration is not present. + """ + SubscriptionFulfillmentModule().fulfill_product(self.order, list(self.order.lines.all())) + self.assertEqual(LINE.FULFILLMENT_CONFIGURATION_ERROR, self.order.lines.all()[0].status) + + @mock.patch('requests.put', mock.Mock(side_effect=ConnectionError)) + def test_subscription_fulfillment_module_network_error(self): + """ + Verify that subscription fulfillment fails if there is a ConnectionError. + """ + SubscriptionFulfillmentModule().fulfill_product(self.order, list(self.order.lines.all())) + self.assertEqual(LINE.FULFILLMENT_NETWORK_ERROR, self.order.lines.all()[0].status) + + @mock.patch('requests.put', mock.Mock(side_effect=Timeout)) + def test_subscription_fulfillment_module_request_timeout(self): + """ + Verify that subscription fulfillment fails if there is a Timeout error. + """ + SubscriptionFulfillmentModule().fulfill_product(self.order, list(self.order.lines.all())) + self.assertEqual(LINE.FULFILLMENT_TIMEOUT_ERROR, self.order.lines.all()[0].status) + + @httpretty.activate + @ddt.data(None, '{"message": "Error occurred!"}') + def test_subscription_fulfillment_module_server_error(self, body): + """ + Verify that subscription fulfillment fails if there is an internal server error. + """ + httpretty.register_uri(httpretty.PUT, get_lms_user_subscription_api_url(self.subscription.id), status=500, body=body, content_type=JSON) + SubscriptionFulfillmentModule().fulfill_product(self.order, list(self.order.lines.all())) + self.assertEqual(LINE.FULFILLMENT_SERVER_ERROR, self.order.lines.all()[0].status) + + def test_revoke_subscription(self): + """ + Verify that subscription product line revokation logs correctly. + """ + line = self.order.lines.first() + + with LogCapture(LOGGER_NAME) as logs: + self.assertTrue(SubscriptionFulfillmentModule().revoke_line(line)) + + logs.check( + ( + LOGGER_NAME, + 'INFO', + 'line_revoked: order_line_id="{}", order_number="{}", product_class="{}", user_id="{}"'.format( + line.id, + line.order.number, + line.product.get_product_class().name, + line.order.user.id + ) + ) + ) + + def test_subscription_fulfillment_request_headers(self): + """ + Verify that subscription fulfillment request contains analytics headers. + """ + # Now call the subscription api to send PUT request to LMS + # This will raise the exception 'ConnectionError' because the LMS is + # not available for ecommerce tests. + with self.assertRaises(ConnectionError): + # pylint: disable=protected-access + SubscriptionFulfillmentModule()._post_to_user_subscription_api(data=self.user_subscription_api_payload, user=self.user) diff --git a/ecommerce/subscriptions/tests/test_utils.py b/ecommerce/subscriptions/tests/test_utils.py new file mode 100644 index 00000000000..9de4744622c --- /dev/null +++ b/ecommerce/subscriptions/tests/test_utils.py @@ -0,0 +1,238 @@ +""" +Unit tests for subscription utility methods. +""" +import httpretty +import json +import mock +from oscar.test.factories import BasketFactory +from testfixtures import LogCapture + +from ecommerce.core.models import SiteConfiguration +from ecommerce.subscriptions.api.v2.tests.constants import ( + FULL_ACCESS_COURSES, + FULL_ACCESS_TIME_PERIOD, + LIFETIME_ACCESS, + LIMITED_ACCESS, +) +from ecommerce.subscriptions.api.v2.tests.factories import BasketAttributeFactory +from ecommerce.subscriptions.api.v2.tests.mixins import SubscriptionProductMixin +from ecommerce.subscriptions.api.v2.tests.utils import mock_user_subscription +from ecommerce.subscriptions.utils import * +from ecommerce.tests.testcases import TestCase + +Product = get_model('catalogue', 'Product') + + +class SubscriptionUtilsTests(SubscriptionProductMixin, TestCase): + """ + Unit tests for subscription utility methods. + """ + + def setUp(self): + """ + Setup initial test data. + """ + super(SubscriptionUtilsTests, self).setUp() + self.subscription = self.create_subscription(stockrecords__partner=self.site.partner, subscription_status=True) + self.user = self.create_user() + self.basket = BasketFactory(site=self.site, owner=self.user) + + def get_duration_to_add(self, subscription): + """ + Tests utility method to get subscription duration to add to the current date. + """ + subscription_duration_unit = subscription.attr.subscription_duration_unit.option + subscription_duration_value = subscription.attr.subscription_duration_value + return {subscription_duration_unit: subscription_duration_value} + + def test_is_subscription_buyable_with_inactive_subscription(self): + """ + Verify that is_subscription_buyable returns False for an inactive subscription. + """ + Product.objects.all().delete() + subscription = self.create_subscription(stockrecords__partner=self.site.partner, subscription_status=False) + self.assertFalse(is_subscription_buyable(subscription, self.user, self.site)) + + @mock.patch('ecommerce.subscriptions.utils.get_valid_user_subscription') + def test_is_subscription_buyable_with_no_valid_subscription(self, valid_user_subscription): + """ + Verify that is_subscription_buyable returns True for empty subscription result from LMS. + + If there is an empty response for valid user subscriptions from LMS for a user, it means that + the given user has no purchased valid subscription and hence can buy a new subscription. + """ + valid_user_subscription.return_value = [] + self.assertTrue(is_subscription_buyable(self.subscription, self.user, self.site)) + + @mock.patch('ecommerce.subscriptions.utils.get_valid_user_subscription') + def test_is_subscription_buyable_with_valid_subscription(self, valid_user_subscription): + """ + Verify that is_subscription_buyable returns False for subscription result from LMS. + + If there is a non-empty response for valid user subscriptions from LMS for a user, it means that + the given user already owns a valid subscription and hence can not buy a new subscription. + """ + valid_user_subscription.return_value = [mock_user_subscription()] + self.assertFalse(is_subscription_buyable(self.subscription, self.user, self.site)) + + def test_basket_add_subscription_attribute(self): + """ + Verify that basket attribute for subscription application is set successfully. + """ + request_data = { + SUBSCRIPTION_ATTRIBUTE_TYPE: 'false' + } + basket_attribute_type = BasketAttributeType.objects.get(name=SUBSCRIPTION_ATTRIBUTE_TYPE) + basket_add_subscription_attribute(self.basket, request_data) + self.assertFalse(BasketAttribute.objects.filter(basket=self.basket, attribute_type=basket_attribute_type).exists()) + + request_data[SUBSCRIPTION_ATTRIBUTE_TYPE] = 'true' + basket_add_subscription_attribute(self.basket, request_data) + self.assertTrue(BasketAttribute.objects.filter(basket=self.basket, attribute_type=basket_attribute_type).exists()) + + def test_get_subscription_from_basket_attribute(self): + """ + Verify that get_subscription_from_basket_attribute correctly retrieves subscription ID from basket attribute. + """ + self.assertIsNone(get_subscription_from_basket_attribute(self.basket)) + subscription_id_attribute_type, __ = BasketAttributeType.objects.get_or_create(name=SUBSCRIPTION_ID_ATTRIBUTE_TYPE) + BasketAttributeFactory( + basket=self.basket, + attribute_type=subscription_id_attribute_type, + value_text=str(self.subscription.id) + ) + self.assertTrue(get_subscription_from_basket_attribute(self.basket)) + + def get_subscription_expiration_date(self): + """ + Verify that get_subscription_expiry_date method calculates the expiration date correctly. + """ + limited_subscription = self.create_subscription(product_attributes__subscription_type=LIMITED_ACCESS) + limited_subscription_duration_to_add = self.get_duration_to_add(limited_subscription) + limited_subscription_expiration_date = date.today() + relativedelta(**limited_subscription_duration_to_add) + + full_access_courses_subscription = self.create_subscription(product_attributes__subscription_type=FULL_ACCESS_COURSES) + full_access_courses_subscription_duration_to_add = self.get_duration_to_add(full_access_courses_subscription) + full_access_courses_subscription_expiration_date = date.today() + relativedelta(**full_access_courses_subscription_duration_to_add) + + full_access_time_period_subscription = self.create_subscription(product_attributes__subscription_type=FULL_ACCESS_TIME_PERIOD) + lifetime_subscription = self.create_subscription(product_attributes__subscription_type=LIFETIME_ACCESS) + + self.assertEqual(get_subscription_expiration_date(limited_subscription), limited_subscription_expiration_date) + self.assertEqual(get_subscription_expiration_date(full_access_courses_subscription), full_access_courses_subscription_expiration_date) + self.assertIsNone(get_subscription_expiration_date(full_access_time_period_subscription)) + self.assertIsNone(get_subscription_expiration_date(lifetime_subscription)) + + @mock.patch.object(SiteConfiguration, 'access_token', mock.Mock(return_value='foo')) + @mock.patch('ecommerce.subscriptions.utils.get_lms_resource_for_user') + def test_get_valid_user_subscription(self, lms_resource_for_user): + """ + Verify that get_valid_user_subscription method fetches a valid user subscription successfully. + """ + lms_resource_for_user.return_value = [] + valid_user_subscription = get_valid_user_subscription(self.user, self.site) + self.assertEqual(valid_user_subscription, []) + + lms_response = mock_user_subscription() + lms_resource_for_user.return_value = lms_response + valid_user_subscription = get_valid_user_subscription(self.user, self.site) + self.assertEqual(valid_user_subscription, lms_response) + + @httpretty.activate + @mock.patch.object(SiteConfiguration, 'access_token', mock.Mock(return_value='foo')) + def test_get_lms_resource_for_user_with_connection_error(self): + """ + Verify that get_lms_resource_for_user catches ConnectionError correctly. + """ + def request_callback(_method, _uri, _headers): + raise ConnectionError + + endpoint = self.site_configuration.subscriptions_api_client.user_subscriptions + httpretty.register_uri(httpretty.GET, endpoint.url(), body=request_callback) + query_dict = dict(valid=True, user=self.user.username) + with LogCapture(logger.name) as logs: + response = get_lms_resource_for_user( + self.user, self.site, endpoint, resource_name=SUBSCRIPTION_RESOURCE_NAME, query_dict=query_dict + ) + self.assertEqual(response, []) + logs.check( + ( + logger.name, + 'ERROR', + 'Failed to retrieve {} : {}'.format( + SUBSCRIPTION_RESOURCE_NAME, + '(\'Connection aborted.\', BadStatusLine("\'\'",))' + ) + ) + ) + + @httpretty.activate + @mock.patch.object(SiteConfiguration, 'access_token', mock.Mock(return_value='foo')) + def test_get_lms_resource_for_user_with_slumber_exception(self): + """ + Verify that get_lms_resource_for_user catches SlumberBaseException correctly. + """ + def request_callback(_method, _uri, _headers): + raise SlumberBaseException + + endpoint = self.site_configuration.subscriptions_api_client.user_subscriptions + httpretty.register_uri(httpretty.GET, endpoint.url(), body=request_callback) + query_dict = dict(valid=True, user=self.user.username) + with LogCapture(logger.name) as logs: + response = get_lms_resource_for_user( + self.user, self.site, endpoint, resource_name=SUBSCRIPTION_RESOURCE_NAME, query_dict=query_dict + ) + self.assertEqual(response, []) + logs.check( + ( + logger.name, + 'ERROR', + 'Failed to retrieve {} : {}'.format( + SUBSCRIPTION_RESOURCE_NAME, + '(\'Connection aborted.\', BadStatusLine("\'\'",))' + ) + ) + ) + + @httpretty.activate + @mock.patch.object(SiteConfiguration, 'access_token', mock.Mock(return_value='foo')) + def test_get_lms_resource_for_user_with_timeout_error(self): + """ + Verify that get_lms_resource_for_user catches TimeoutError correctly. + """ + def request_callback(_method, _uri, _headers): + raise Timeout + + endpoint = self.site_configuration.subscriptions_api_client.user_subscriptions + httpretty.register_uri(httpretty.GET, endpoint.url(), body=request_callback) + query_dict = dict(valid=True, user=self.user.username) + with LogCapture(logger.name) as logs: + response = get_lms_resource_for_user( + self.user, self.site, endpoint, resource_name=SUBSCRIPTION_RESOURCE_NAME, query_dict=query_dict + ) + self.assertEqual(response, []) + logs.check( + ( + logger.name, + 'ERROR', + 'Failed to retrieve {} : {}'.format( + SUBSCRIPTION_RESOURCE_NAME, + '(\'Connection aborted.\', BadStatusLine("\'\'",))' + ) + ) + ) + + @httpretty.activate + @mock.patch.object(SiteConfiguration, 'access_token', mock.Mock(return_value='foo')) + def test_get_lms_resource_for_user(self): + """ + Verify that get_lms_resource_for_user returns results correctly if no exceptions are raised. + """ + lms_user_subscription_response = mock_user_subscription() + endpoint = self.site_configuration.subscriptions_api_client.user_subscriptions + httpretty.register_uri(httpretty.GET, endpoint.url(), body='[{}]'.format(json.dumps(lms_user_subscription_response)), content_type="application/json") + query_dict = dict(valid=True, user=self.user.username) + response = get_lms_resource_for_user( + self.user, self.site, endpoint, resource_name=SUBSCRIPTION_RESOURCE_NAME, query_dict=query_dict + ) + self.assertEqual(response, lms_user_subscription_response) diff --git a/ecommerce/subscriptions/tests/test_views.py b/ecommerce/subscriptions/tests/test_views.py new file mode 100644 index 00000000000..5afeb7b4d6a --- /dev/null +++ b/ecommerce/subscriptions/tests/test_views.py @@ -0,0 +1,76 @@ +""" +Unit tests for subscription Template views. +""" +from waffle.testutils import override_switch + +from django.conf import settings +from django.urls import reverse + +from ecommerce.core.constants import ENABLE_SUBSCRIPTIONS_ON_RUNTIME_SWITCH +from ecommerce.tests.testcases import TestCase + + +class SubscriptionAppViewTests(TestCase): + """ + Unit tests for "SubscriptionAppView". + """ + path = reverse('subscriptions:app', args=['']) + + def _create_and_login_staff_user(self): + """ + Setup staff user with an OAuth2 access token and log the user in. + """ + user = self.create_user(is_staff=True) + self.create_access_token(user) + self.assertIsNotNone(user.access_token) + self.client.login(username=user.username, password=self.password) + + def test_login_required(self): + """ + Verify that users are required to login before accessing the view. + """ + self.client.logout() + response = self.client.get(self.path) + self.assertEqual(response.status_code, 302) + self.assertIn(settings.LOGIN_URL, response.url) + + def test_staff_user_required(self): + """ + Verify the view is only accessible to staff users. + """ + user = self.create_user(is_staff=False) + self.client.login(username=user.username, password=self.password) + response = self.client.get(self.path) + self.assertEqual(response.status_code, 302) + + self._create_and_login_staff_user() + response = self.client.get(self.path) + self.assertEqual(response.status_code, 200) + + @override_switch(ENABLE_SUBSCRIPTIONS_ON_RUNTIME_SWITCH, active=False) + def test_subscriptions_switch_turned_off(self): + """ + Verify that the view is not accessible with "ENABLE_SUBSCRIPTIONS_ON_RUNTIME_SWITCH" inactive. + """ + self._create_and_login_staff_user() + response = self.client.get(self.path) + self.assertEqual(response.status_code, 404) + + @override_switch(ENABLE_SUBSCRIPTIONS_ON_RUNTIME_SWITCH, active=True) + def test_subscriptions_switch_turned_on(self): + """ + Verify that the view is accessible with "ENABLE_SUBSCRIPTIONS_ON_RUNTIME_SWITCH" active. + """ + self._create_and_login_staff_user() + response = self.client.get(self.path) + self.assertEqual(response.status_code, 200) + + @override_switch(ENABLE_SUBSCRIPTIONS_ON_RUNTIME_SWITCH, active=True) + def test_context(self): + """ + Verify that additional context values are present. + """ + self._create_and_login_staff_user() + response = self.client.get(self.path) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['admin'], 'subscription') diff --git a/ecommerce/tests/factories.py b/ecommerce/tests/factories.py index db4af433adc..fe61b6d11c0 100644 --- a/ecommerce/tests/factories.py +++ b/ecommerce/tests/factories.py @@ -38,6 +38,7 @@ class Meta(object): enable_sdn_check = False enable_embargo_check = False enable_partial_program = False + enable_course_payments = True discovery_api_url = 'http://{}.fake/'.format(Faker().domain_name()) @factory.lazy_attribute