class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, PaymentEventsMixin, RefundTestMixin, TestCase): """ Tests validating generic behaviors of the EdxOrderPlacementMixin. """ def setUp(self): super(EdxOrderPlacementMixinTests, self).setUp() self.user = UserFactory() self.order = self.create_order(status=ORDER.OPEN) def test_handle_payment_logging(self, __): """ Ensure that we emit a log entry upon receipt of a payment notification, and create Source and PaymentEvent objects. """ user = factories.UserFactory() basket = factories.create_basket() basket.owner = user basket.save() mixin = EdxOrderPlacementMixin() mixin.payment_processor = DummyProcessor(self.site) processor_name = DummyProcessor.NAME total = basket.total_incl_tax reference = basket.id with LogCapture(LOGGER_NAME) as l: mixin.handle_payment({}, basket) l.check(( LOGGER_NAME, 'INFO', 'payment_received: amount="{}", basket_id="{}", currency="{}", ' 'processor_name="{}", reference="{}", user_id="{}"'.format( total, basket.id, basket.currency, processor_name, reference, user.id))) # pylint: disable=protected-access # Validate a payment Source was created source_type = SourceType.objects.get(code=processor_name) label = user.username self.assert_basket_matches_source(basket, mixin._payment_sources[-1], source_type, reference, label) # Validate the PaymentEvent was created paid_type = PaymentEventType.objects.get(code='paid') self.assert_valid_payment_event_fields(mixin._payment_events[-1], total, paid_type, processor_name, reference) def test_handle_successful_order(self, mock_track): """ Ensure that tracking events are fired with correct content when order placement event handling is invoked. """ tracking_context = { 'lms_user_id': 'test-user-id', 'lms_client_id': 'test-client-id', 'lms_ip': '127.0.0.1' } self.user.tracking_context = tracking_context self.user.save() with LogCapture(LOGGER_NAME) as l: EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure event is being tracked self.assertTrue(mock_track.called) # ensure event data is correct self.assert_correct_event(mock_track, self.order, tracking_context['lms_user_id'], tracking_context['lms_client_id'], tracking_context['lms_ip'], self.order.number, self.order.currency, self.order.total_excl_tax) l.check(( LOGGER_NAME, 'INFO', 'order_placed: amount="{}", basket_id="{}", contains_coupon="{}", currency="{}",' ' order_number="{}", user_id="{}"'.format( self.order.total_excl_tax, self.order.basket.id, self.order.contains_coupon, self.order.currency, self.order.number, self.order.user.id))) def test_handle_successful_free_order(self, mock_track): """Verify that tracking events are not emitted for free orders.""" order = self.create_order(free=True, status=ORDER.OPEN) EdxOrderPlacementMixin().handle_successful_order(order) # Verify that no event was emitted. self.assertFalse(mock_track.called) def test_handle_successful_order_no_context(self, mock_track): """ Ensure that expected values are substituted when no tracking_context was available. """ EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure event is being tracked self.assertTrue(mock_track.called) # ensure event data is correct self.assert_correct_event(mock_track, self.order, 'ecommerce-{}'.format(self.user.id), None, None, self.order.number, self.order.currency, self.order.total_excl_tax) def test_handle_successful_order_no_segment_key(self, mock_track): """ Ensure that tracking events do not fire when there is no Segment key configured. """ self.site.siteconfiguration.segment_key = None EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure no event was fired self.assertFalse(mock_track.called) def test_handle_successful_order_segment_error(self, mock_track): """ Ensure that exceptions raised while emitting tracking events are logged, but do not otherwise interrupt program flow. """ with patch('ecommerce.extensions.analytics.utils.logger.exception' ) as mock_log_exc: mock_track.side_effect = Exception("clunk") EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure that analytics.track was called, but the exception was caught self.assertTrue(mock_track.called) # ensure we logged a warning. self.assertTrue( mock_log_exc.called_with( "Failed to emit tracking event upon order placement.")) def test_handle_successful_async_order(self, __): """ Verify that a Waffle Sample can be used to control async order fulfillment. """ sample, created = Sample.objects.get_or_create( name='async_order_fulfillment', defaults={ 'percent': 100.0, 'note': 'Determines what percentage of orders are fulfilled asynchronously.', }) if not created: sample.percent = 100.0 sample.save() with patch('ecommerce.extensions.checkout.mixins.fulfill_order.delay' ) as mock_delay: EdxOrderPlacementMixin().handle_successful_order(self.order) self.assertTrue(mock_delay.called) mock_delay.assert_called_once_with( self.order.number, site_code=self.partner.short_code) def test_place_free_order(self, __): """ Verify an order is placed and the basket is submitted. """ basket = BasketFactory(owner=self.user, site=self.site) basket.add_product(ProductFactory(stockrecords__price_excl_tax=0)) order = EdxOrderPlacementMixin().place_free_order(basket) self.assertIsNotNone(order) self.assertEqual(basket.status, Basket.SUBMITTED) def test_non_free_basket_order(self, __): """ Verify an error is raised for non-free basket. """ basket = BasketFactory(owner=self.user, site=self.site) basket.add_product(ProductFactory(stockrecords__price_excl_tax=10)) with self.assertRaises(BasketNotFreeError): EdxOrderPlacementMixin().place_free_order(basket) def test_send_confirmation_message(self, __): """ Verify the send confirmation message override functions as expected """ request = RequestFactory() user = self.create_user() user.email = '*****@*****.**' request.user = user site_from_email = '*****@*****.**' site_configuration = SiteConfigurationFactory( partner__name='Tester', from_email=site_from_email) request.site = site_configuration.site order = factories.create_order() order.user = user mixin = EdxOrderPlacementMixin() mixin.request = request # Happy path mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site) self.assertEqual(mail.outbox[0].from_email, site_from_email) mail.outbox = [] # Invalid code path (graceful exit) mixin.send_confirmation_message(order, 'INVALID_CODE', request.site) self.assertEqual(len(mail.outbox), 0) # Invalid messages container path (graceful exit) with patch( 'ecommerce.extensions.checkout.mixins.CommunicationEventType.objects.get' ) as mock_get: mock_event_type = Mock() mock_event_type.get_messages.return_value = {} mock_get.return_value = mock_event_type mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site) self.assertEqual(len(mail.outbox), 0) mock_event_type.get_messages.return_value = {'body': None} mock_get.return_value = mock_event_type mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site) self.assertEqual(len(mail.outbox), 0)
class EnrollmentFulfillmentModuleTests(ProgramTestMixin, DiscoveryTestMixin, FulfillmentTestMixin, TestCase): """Test course seat fulfillment.""" course_id = 'edX/DemoX/Demo_Course' certificate_type = 'test-certificate-type' provider = None def setUp(self): super(EnrollmentFulfillmentModuleTests, 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.course = CourseFactory(id=self.course_id, name='Demo Course', site=self.site) self.seat = self.course.create_or_update_seat(self.certificate_type, False, 100, self.partner, self.provider) basket = BasketFactory(owner=self.user, site=self.site) basket.add_product(self.seat, 1) self.order = create_order(number=1, basket=basket, user=self.user) # pylint: disable=attribute-defined-outside-init def create_seat_and_order(self, certificate_type='test-certificate-type', provider=None): """ Create the certificate of given type and seat of given provider. Arguments: certificate_type(str): The type of certificate provider(str): The provider ID. Returns: None """ self.certificate_type = certificate_type self.provider = provider self.seat = self.course.create_or_update_seat(self.certificate_type, False, 100, self.partner, self.provider) basket = BasketFactory(owner=self.user, site=self.site) basket.add_product(self.seat, 1) self.order = create_order(number=2, basket=basket, user=self.user) def prepare_basket_with_voucher(self, program_uuid=None): catalog = Catalog.objects.create(partner=self.partner) coupon_product_class, _ = ProductClass.objects.get_or_create( name=COUPON_PRODUCT_CLASS_NAME) coupon = factories.create_product(product_class=coupon_product_class, title='Test product') stock_record = StockRecord.objects.filter(product=self.seat).first() catalog.stock_records.add(stock_record) vouchers = create_vouchers( benefit_type=Benefit.PERCENTAGE, benefit_value=100.00, catalog=catalog, coupon=coupon, end_datetime=datetime.datetime.now() + datetime.timedelta(days=30), enterprise_customer=None, name="Test Voucher", quantity=10, start_datetime=datetime.datetime.now(), voucher_type=Voucher.SINGLE_USE, program_uuid=program_uuid, ) Applicator().apply_offers(self.order.basket, vouchers[0].offers.all()) def test_enrollment_module_support(self): """Test that we get the correct values back for supported product lines.""" supported_lines = EnrollmentFulfillmentModule().get_supported_lines( list(self.order.lines.all())) self.assertEqual(1, len(supported_lines)) @httpretty.activate def test_enrollment_module_fulfill(self): """Happy path test to ensure we can properly fulfill enrollments.""" httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200, body='{}', content_type=JSON) # Attempt to enroll. with LogCapture(LOGGER_NAME) as l: EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) line = self.order.lines.get() l.check(( LOGGER_NAME, 'INFO', 'line_fulfilled: course_id="{}", credit_provider="{}", mode="{}", order_line_id="{}", ' 'order_number="{}", product_class="{}", user_id="{}"'.format( line.product.attr.course_key, None, mode_for_product(line.product), 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_body = { 'user': self.order.user.username, 'is_active': True, 'mode': self.certificate_type, 'course_details': { 'course_id': self.course_id, }, 'enrollment_attributes': [{ 'namespace': 'order', 'name': 'order_number', 'value': self.order.number }] } 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(expected_body, actual_body) @httpretty.activate def test_enrollment_module_fulfill_order_with_discount_no_voucher(self): """ Test that components of the Fulfillment Module which trigger on the presence of a voucher do not cause failures in cases where a discount does not have a voucher included (such as with a Conditional Offer) """ httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200, body='{}', content_type=JSON) self.create_seat_and_order(certificate_type='credit', provider='MIT') self.order.discounts.create() __, lines = EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) # No exceptions should be raised and the order should be fulfilled self.assertEqual(lines[0].status, 'Complete') @override_settings(EDX_API_KEY=None) def test_enrollment_module_not_configured(self): """Test that lines receive a configuration error status if fulfillment configuration is invalid.""" EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) self.assertEqual(LINE.FULFILLMENT_CONFIGURATION_ERROR, self.order.lines.all()[0].status) def test_enrollment_module_fulfill_bad_attributes(self): """Test that use of the Fulfillment Module fails when the product does not have attributes.""" ProductAttribute.objects.get( product_class__name=SEAT_PRODUCT_CLASS_NAME, code='course_key').delete() EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) self.assertEqual(LINE.FULFILLMENT_CONFIGURATION_ERROR, self.order.lines.all()[0].status) @mock.patch('requests.post', mock.Mock(side_effect=ConnectionError)) def test_enrollment_module_network_error(self): """Test that lines receive a network error status if a fulfillment request experiences a network error.""" EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) self.assertEqual(LINE.FULFILLMENT_NETWORK_ERROR, self.order.lines.all()[0].status) @mock.patch('requests.post', mock.Mock(side_effect=Timeout)) def test_enrollment_module_request_timeout(self): """Test that lines receive a timeout error status if a fulfillment request times out.""" EnrollmentFulfillmentModule().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": "Oops!"}') def test_enrollment_module_server_error(self, body): """Test that lines receive a server-side error status if a server-side error occurs during fulfillment.""" # NOTE: We are testing for cases where the response does and does NOT have data. The module should be able # to handle both cases. httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=500, body=body, content_type=JSON) EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) self.assertEqual(LINE.FULFILLMENT_SERVER_ERROR, self.order.lines.all()[0].status) @httpretty.activate def test_revoke_product(self): """ The method should call the Enrollment API to un-enroll the student, and return True. """ httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200, body='{}', content_type=JSON) line = self.order.lines.first() with LogCapture(LOGGER_NAME) as l: self.assertTrue(EnrollmentFulfillmentModule().revoke_line(line)) l.check(( LOGGER_NAME, 'INFO', 'line_revoked: certificate_type="{}", course_id="{}", order_line_id="{}", order_number="{}", ' 'product_class="{}", user_id="{}"'.format( getattr(line.product.attr, 'certificate_type', ''), line.product.attr.course_key, line.id, line.order.number, line.product.get_product_class().name, line.order.user.id))) last_request = httpretty.last_request() actual_body = json.loads(last_request.body) actual_headers = last_request.headers expected_body = { 'user': self.order.user.username, 'is_active': False, 'mode': self.certificate_type, 'course_details': { 'course_id': self.course_id, }, } 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(expected_body, actual_body) @httpretty.activate def test_revoke_product_expected_error(self): """ If the Enrollment API responds with an expected error, the method should log that revocation was bypassed, and return True. """ message = 'Enrollment mode mismatch: active mode=x, requested mode=y. Won\'t deactivate.' body = '{{"message": "{}"}}'.format(message) httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=400, body=body, content_type=JSON) line = self.order.lines.first() logger_name = 'ecommerce.extensions.fulfillment.modules' with LogCapture(logger_name) as l: self.assertTrue(EnrollmentFulfillmentModule().revoke_line(line)) l.check( (logger_name, 'INFO', 'Attempting to revoke fulfillment of Line [{}]...'.format( line.id)), (logger_name, 'INFO', 'Skipping revocation for line [%d]: %s' % (line.id, message))) @httpretty.activate def test_revoke_product_unexpected_error(self): """ If the Enrollment API responds with a non-200 status, the method should log an error and return False. """ message = 'Meh.' body = '{{"message": "{}"}}'.format(message) httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=500, body=body, content_type=JSON) line = self.order.lines.first() logger_name = 'ecommerce.extensions.fulfillment.modules' with LogCapture(logger_name) as l: self.assertFalse(EnrollmentFulfillmentModule().revoke_line(line)) l.check((logger_name, 'INFO', 'Attempting to revoke fulfillment of Line [{}]...'.format( line.id)), (logger_name, 'ERROR', 'Failed to revoke fulfillment of Line [%d]: %s' % (line.id, message))) @httpretty.activate def test_revoke_product_unknown_exception(self): """ If an exception is raised while contacting the Enrollment API, the method should log an error and return False. """ def request_callback(_method, _uri, _headers): raise Timeout httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), body=request_callback) line = self.order.lines.first() logger_name = 'ecommerce.extensions.fulfillment.modules' with LogCapture(logger_name) as l: self.assertFalse(EnrollmentFulfillmentModule().revoke_line(line)) l.check( (logger_name, 'INFO', 'Attempting to revoke fulfillment of Line [{}]...'.format( line.id)), (logger_name, 'ERROR', 'Failed to revoke fulfillment of Line [{}].'.format(line.id))) @httpretty.activate def test_credit_enrollment_module_fulfill(self): """Happy path test to ensure we can properly fulfill enrollments.""" # Create the credit certificate type and order for the credit certificate type. self.create_seat_and_order(certificate_type='credit', provider='MIT') httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200, body='{}', content_type=JSON) # Attempt to enroll. with LogCapture(LOGGER_NAME) as l: EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) line = self.order.lines.get() l.check(( LOGGER_NAME, 'INFO', 'line_fulfilled: course_id="{}", credit_provider="{}", mode="{}", order_line_id="{}", ' 'order_number="{}", product_class="{}", user_id="{}"'.format( line.product.attr.course_key, line.product.attr.credit_provider, mode_for_product(line.product), line.id, line.order.number, line.product.get_product_class().name, line.order.user.id, ))) self.assertEqual(LINE.COMPLETE, line.status) actual = json.loads(httpretty.last_request().body) expected = { 'user': self.order.user.username, 'is_active': True, 'mode': self.certificate_type, 'course_details': { 'course_id': self.course_id, }, 'enrollment_attributes': [{ 'namespace': 'order', 'name': 'order_number', 'value': self.order.number }, { 'namespace': 'credit', 'name': 'provider_id', 'value': self.provider }] } self.assertEqual(actual, expected) def test_enrollment_headers(self): """ Test that the enrollment module 'EnrollmentFulfillmentModule' is sending enrollment request over to the LMS with proper headers. """ # Create a dummy data for the enrollment request. data = { 'user': '******', 'is_active': True, 'mode': 'honor', 'course_details': { 'course_id': self.course_id }, 'enrollment_attributes': [] } # Now call the enrollment api to send POST request to LMS and verify # that the header of the request being sent contains the analytics # header 'x-edx-ga-client-id'. # This will raise the exception 'ConnectionError' because the LMS is # not available for ecommerce tests. try: # pylint: disable=protected-access EnrollmentFulfillmentModule()._post_to_enrollment_api( data=data, user=self.user) except ConnectionError as exp: # Check that the enrollment request object has the analytics header # 'x-edx-ga-client-id' and 'x-forwarded-for'. self.assertEqual(exp.request.headers.get('x-edx-ga-client-id'), self.user.tracking_context['ga_client_id']) self.assertEqual(exp.request.headers.get('x-forwarded-for'), self.user.tracking_context['lms_ip']) def test_voucher_usage(self): """ Test that using a voucher applies offer discount to reduce order price """ self.prepare_basket_with_voucher() self.assertEqual(self.order.basket.total_excl_tax, 0.00) @httpretty.activate def test_voucher_usage_with_program(self): """ Test that using a voucher with a program basket results in a fulfilled order. """ httpretty.register_uri(httpretty.POST, get_lms_enrollment_api_url(), status=200, body='{}', content_type=JSON) self.create_seat_and_order(certificate_type='credit', provider='MIT') program_uuid = uuid.uuid4() self.mock_program_detail_endpoint( program_uuid, self.site_configuration.discovery_api_url) self.mock_user_data(self.user.username) self.prepare_basket_with_voucher(program_uuid=program_uuid) __, lines = EnrollmentFulfillmentModule().fulfill_product( self.order, list(self.order.lines.all())) # No exceptions should be raised and the order should be fulfilled self.assertEqual(lines[0].status, 'Complete')
class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, RefundTestMixin, TestCase): """ Tests validating generic behaviors of the EdxOrderPlacementMixin. """ def setUp(self): super(EdxOrderPlacementMixinTests, self).setUp() self.user = UserFactory() self.order = self.create_order(status=ORDER.OPEN) def test_handle_payment_logging(self, __): """ Ensure that we emit a log entry upon receipt of a payment notification. """ amount = Decimal('9.99') basket_id = 'test-basket-id' currency = 'USD' processor_name = 'test-processor-name' reference = 'test-reference' user_id = '1' mock_source = Mock(currency=currency) mock_payment_event = Mock( amount=amount, processor_name=processor_name, reference=reference ) mock_handle_processor_response = Mock(return_value=(mock_source, mock_payment_event)) mock_payment_processor = Mock(handle_processor_response=mock_handle_processor_response) with patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.payment_processor', mock_payment_processor): mock_basket = Mock(id=basket_id, owner=Mock(id=user_id)) with LogCapture(LOGGER_NAME) as l: EdxOrderPlacementMixin().handle_payment(Mock(), mock_basket) l.check( ( LOGGER_NAME, 'INFO', 'payment_received: amount="{}", basket_id="{}", currency="{}", ' 'processor_name="{}", reference="{}", user_id="{}"'.format( amount, basket_id, currency, processor_name, reference, user_id ) ) ) def test_handle_successful_order(self, mock_track): """ Ensure that tracking events are fired with correct content when order placement event handling is invoked. """ tracking_context = {'lms_user_id': 'test-user-id', 'lms_client_id': 'test-client-id', 'lms_ip': '127.0.0.1'} self.user.tracking_context = tracking_context self.user.save() with LogCapture(LOGGER_NAME) as l: EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure event is being tracked self.assertTrue(mock_track.called) # ensure event data is correct self.assert_correct_event( mock_track, self.order, tracking_context['lms_user_id'], tracking_context['lms_client_id'], tracking_context['lms_ip'], self.order.number, self.order.currency, self.order.total_excl_tax ) l.check( ( LOGGER_NAME, 'INFO', 'order_placed: amount="{}", basket_id="{}", currency="{}", order_number="{}", user_id="{}"'.format( self.order.total_excl_tax, self.order.basket.id, self.order.currency, self.order.number, self.order.user.id ) ) ) def test_handle_successful_free_order(self, mock_track): """Verify that tracking events are not emitted for free orders.""" order = self.create_order(free=True, status=ORDER.OPEN) EdxOrderPlacementMixin().handle_successful_order(order) # Verify that no event was emitted. self.assertFalse(mock_track.called) def test_handle_successful_order_no_context(self, mock_track): """ Ensure that expected values are substituted when no tracking_context was available. """ EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure event is being tracked self.assertTrue(mock_track.called) # ensure event data is correct self.assert_correct_event( mock_track, self.order, 'ecommerce-{}'.format(self.user.id), None, None, self.order.number, self.order.currency, self.order.total_excl_tax ) @override_settings(SEGMENT_KEY=None) def test_handle_successful_order_no_segment_key(self, mock_track): """ Ensure that tracking events do not fire when there is no Segment key configured. """ EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure no event was fired self.assertFalse(mock_track.called) def test_handle_successful_order_segment_error(self, mock_track): """ Ensure that exceptions raised while emitting tracking events are logged, but do not otherwise interrupt program flow. """ with patch('ecommerce.extensions.analytics.utils.logger.exception') as mock_log_exc: mock_track.side_effect = Exception("clunk") EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure that analytics.track was called, but the exception was caught self.assertTrue(mock_track.called) # ensure we logged a warning. self.assertTrue(mock_log_exc.called_with("Failed to emit tracking event upon order placement."))
class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, RefundTestMixin, TestCase): """ Tests validating generic behaviors of the EdxOrderPlacementMixin. """ def setUp(self): super(EdxOrderPlacementMixinTests, self).setUp() self.user = UserFactory() self.order = self.create_order(status=ORDER.OPEN) def test_handle_payment_logging(self, __): """ Ensure that we emit a log entry upon receipt of a payment notification. """ amount = Decimal('9.99') basket_id = 'test-basket-id' currency = 'USD' processor_name = 'test-processor-name' reference = 'test-reference' user_id = '1' mock_source = Mock(currency=currency) mock_payment_event = Mock( amount=amount, processor_name=processor_name, reference=reference ) mock_handle_processor_response = Mock(return_value=(mock_source, mock_payment_event)) mock_payment_processor = Mock(handle_processor_response=mock_handle_processor_response) with patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.payment_processor', mock_payment_processor): mock_basket = Mock(id=basket_id, owner=Mock(id=user_id)) with LogCapture(LOGGER_NAME) as l: EdxOrderPlacementMixin().handle_payment(Mock(), mock_basket) l.check( ( LOGGER_NAME, 'INFO', 'payment_received: amount="{}", basket_id="{}", currency="{}", ' 'processor_name="{}", reference="{}", user_id="{}"'.format( amount, basket_id, currency, processor_name, reference, user_id ) ) ) def test_handle_successful_order(self, mock_track): """ Ensure that tracking events are fired with correct content when order placement event handling is invoked. """ tracking_context = {'lms_user_id': 'test-user-id', 'lms_client_id': 'test-client-id', 'lms_ip': '127.0.0.1'} self.user.tracking_context = tracking_context self.user.save() with LogCapture(LOGGER_NAME) as l: EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure event is being tracked self.assertTrue(mock_track.called) # ensure event data is correct self.assert_correct_event( mock_track, self.order, tracking_context['lms_user_id'], tracking_context['lms_client_id'], tracking_context['lms_ip'], self.order.number, self.order.currency, self.order.total_excl_tax ) l.check( ( LOGGER_NAME, 'INFO', 'order_placed: amount="{}", basket_id="{}", contains_coupon="{}", currency="{}",' ' order_number="{}", user_id="{}"'.format( self.order.total_excl_tax, self.order.basket.id, self.order.contains_coupon, self.order.currency, self.order.number, self.order.user.id ) ) ) def test_handle_successful_free_order(self, mock_track): """Verify that tracking events are not emitted for free orders.""" order = self.create_order(free=True, status=ORDER.OPEN) EdxOrderPlacementMixin().handle_successful_order(order) # Verify that no event was emitted. self.assertFalse(mock_track.called) def test_handle_successful_order_no_context(self, mock_track): """ Ensure that expected values are substituted when no tracking_context was available. """ EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure event is being tracked self.assertTrue(mock_track.called) # ensure event data is correct self.assert_correct_event( mock_track, self.order, 'ecommerce-{}'.format(self.user.id), None, None, self.order.number, self.order.currency, self.order.total_excl_tax ) def test_handle_successful_order_no_segment_key(self, mock_track): """ Ensure that tracking events do not fire when there is no Segment key configured. """ self.site.siteconfiguration.segment_key = None EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure no event was fired self.assertFalse(mock_track.called) def test_handle_successful_order_segment_error(self, mock_track): """ Ensure that exceptions raised while emitting tracking events are logged, but do not otherwise interrupt program flow. """ with patch('ecommerce.extensions.analytics.utils.logger.exception') as mock_log_exc: mock_track.side_effect = Exception("clunk") EdxOrderPlacementMixin().handle_successful_order(self.order) # ensure that analytics.track was called, but the exception was caught self.assertTrue(mock_track.called) # ensure we logged a warning. self.assertTrue(mock_log_exc.called_with("Failed to emit tracking event upon order placement.")) def test_handle_successful_async_order(self, __): """ Verify that a Waffle Sample can be used to control async order fulfillment. """ sample, created = Sample.objects.get_or_create( name='async_order_fulfillment', defaults={ 'percent': 100.0, 'note': 'Determines what percentage of orders are fulfilled asynchronously.', } ) if not created: sample.percent = 100.0 sample.save() with patch('ecommerce.extensions.checkout.mixins.fulfill_order.delay') as mock_delay: EdxOrderPlacementMixin().handle_successful_order(self.order) self.assertTrue(mock_delay.called) def test_place_free_order(self, __): """ Verify an order is placed and the basket is submitted. """ basket = BasketFactory(owner=self.user, site=self.site) basket.add_product(ProductFactory(stockrecords__price_excl_tax=0)) order = EdxOrderPlacementMixin().place_free_order(basket) self.assertIsNotNone(order) self.assertEqual(basket.status, Basket.SUBMITTED) def test_non_free_basket_order(self, __): """ Verify an error is raised for non-free basket. """ basket = BasketFactory(owner=self.user, site=self.site) basket.add_product(ProductFactory(stockrecords__price_excl_tax=10)) with self.assertRaises(BasketNotFreeError): EdxOrderPlacementMixin().place_free_order(basket) def test_send_confirmation_message(self, __): """ Verify the send confirmation message override functions as expected """ request = RequestFactory() user = self.create_user() user.email = '*****@*****.**' request.user = user site_from_email = '*****@*****.**' site_configuration = SiteConfigurationFactory(partner__name='Tester', from_email=site_from_email) request.site = site_configuration.site order = factories.create_order() order.user = user mixin = EdxOrderPlacementMixin() mixin.request = request # Happy path mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site) self.assertEqual(mail.outbox[0].from_email, site_from_email) mail.outbox = [] # Invalid code path (graceful exit) mixin.send_confirmation_message(order, 'INVALID_CODE', request.site) self.assertEqual(len(mail.outbox), 0) # Invalid messages container path (graceful exit) with patch('ecommerce.extensions.checkout.mixins.CommunicationEventType.objects.get') as mock_get: mock_event_type = Mock() mock_event_type.get_messages.return_value = {} mock_get.return_value = mock_event_type mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site) self.assertEqual(len(mail.outbox), 0) mock_event_type.get_messages.return_value = {'body': None} mock_get.return_value = mock_event_type mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site) self.assertEqual(len(mail.outbox), 0)