예제 #1
0
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)
예제 #2
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')
예제 #3
0
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."))
예제 #4
0
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)