Changeset View
Changeset View
Standalone View
Standalone View
looper/tests/test_clock.py
| import logging | import logging | ||||
| import datetime | import datetime | ||||
| import typing | import typing | ||||
| from unittest import mock | from unittest import mock | ||||
| from django.utils import timezone | from django.utils import timezone | ||||
| from django.test import override_settings | from django.test import override_settings | ||||
| from dateutil.relativedelta import relativedelta | from dateutil.relativedelta import relativedelta | ||||
| from . import AbstractLooperTestCase | from looper.tests.base import AbstractLooperTestCase | ||||
| from ..clock import Clock | from ..clock import Clock | ||||
| from .. import models, exceptions, signals, admin_log | from .. import models, exceptions, signals, admin_log | ||||
| # Margin for clock ticks (the tick happens at the given time + this margin). | # Margin for clock ticks (the tick happens at the given time + this margin). | ||||
| # Just to stabilise floating point comparisons. | # Just to stabilise floating point comparisons. | ||||
| tick_delay = datetime.timedelta(seconds=0.25) | tick_delay = datetime.timedelta(seconds=0.25) | ||||
| ▲ Show 20 Lines • Show All 117 Lines • ▼ Show 20 Lines | def test_renew_active_subscription(self, mock_transact_sale): | ||||
| self.assertEqual(1, len(entries_q)) | self.assertEqual(1, len(entries_q)) | ||||
| self.assertIn('success', entries_q.first().change_message) | self.assertIn('success', entries_q.first().change_message) | ||||
| # Test the calls on the gateway | # Test the calls on the gateway | ||||
| mock_transact_sale.assert_called_with(payment_token, subs.price) | mock_transact_sale.assert_called_with(payment_token, subs.price) | ||||
| # Test the sent signals | # Test the sent signals | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_succesful, | signal=signals.automatic_payment_succesful, sender=new_order, transaction=new_trans | ||||
| sender=new_order, transaction=new_trans) | ) | ||||
| @mock.patch('looper.gateways.MockableGateway.transact_sale') | @mock.patch('looper.gateways.MockableGateway.transact_sale') | ||||
| def test_renew_active_subscription_prev_order_exists(self, mock_transact_sale): | def test_renew_active_subscription_prev_order_exists(self, mock_transact_sale): | ||||
| subs = self.create_active_subscription() | subs = self.create_active_subscription() | ||||
| with mock.patch('django.utils.timezone.now') as mock_now: | with mock.patch('django.utils.timezone.now') as mock_now: | ||||
| mock_now.return_value = self.start_time | mock_now.return_value = self.start_time | ||||
| order = subs.latest_order() | order = subs.latest_order() | ||||
| Show All 38 Lines | def test_renew_active_subscription_prev_order_exists(self, mock_transact_sale): | ||||
| self.assertEqual(1, len(entries_q)) | self.assertEqual(1, len(entries_q)) | ||||
| self.assertIn('success', entries_q.first().change_message) | self.assertIn('success', entries_q.first().change_message) | ||||
| # Test the calls on the gateway | # Test the calls on the gateway | ||||
| mock_transact_sale.assert_called_with(payment_token, subs.price) | mock_transact_sale.assert_called_with(payment_token, subs.price) | ||||
| # Test the sent signals | # Test the sent signals | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_succesful, | signal=signals.automatic_payment_succesful, sender=new_order, transaction=new_trans | ||||
| sender=new_order, transaction=new_trans) | ) | ||||
| @mock.patch('looper.gateways.MockableGateway.transact_sale') | @mock.patch('looper.gateways.MockableGateway.transact_sale') | ||||
| def test_transact_sale_gatewayerror(self, mock_transact_sale): | def test_transact_sale_gatewayerror(self, mock_transact_sale): | ||||
| subs = self.create_active_subscription() | subs = self.create_active_subscription() | ||||
| # Collect some properties before the clock ticks. | # Collect some properties before the clock ticks. | ||||
| last_order_pk = subs.latest_order().pk | last_order_pk = subs.latest_order().pk | ||||
| payment_token = subs.payment_method.token | payment_token = subs.payment_method.token | ||||
| Show All 27 Lines | def test_transact_sale_gatewayerror(self, mock_transact_sale): | ||||
| self.assertEqual(subs.price, new_trans.amount) | self.assertEqual(subs.price, new_trans.amount) | ||||
| entries_q = admin_log.entries_for(new_trans) | entries_q = admin_log.entries_for(new_trans) | ||||
| self.assertEqual(1, len(entries_q)) | self.assertEqual(1, len(entries_q)) | ||||
| self.assertIn('failed', entries_q.first().change_message) | self.assertIn('failed', entries_q.first().change_message) | ||||
| # Test the sent signals | # Test the sent signals | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_failed, | signal=signals.automatic_payment_failed, sender=new_order, transaction=new_trans | ||||
| sender=new_order, transaction=new_trans) | ) | ||||
| @mock.patch('looper.gateways.MockableGateway.transact_sale') | @mock.patch('looper.gateways.MockableGateway.transact_sale') | ||||
| def test_retry_failed(self, mock_transact_sale): | def test_retry_failed(self, mock_transact_sale): | ||||
| subs = self.create_active_subscription() | subs = self.create_active_subscription() | ||||
| pre_renewal_next_payment = subs.next_payment | pre_renewal_next_payment = subs.next_payment | ||||
| pre_renewal_order_pk = subs.latest_order().pk | pre_renewal_order_pk = subs.latest_order().pk | ||||
| # First call should fail, second should succeed. | # First call should fail, second should succeed. | ||||
| Show All 14 Lines | def test_retry_failed(self, mock_transact_sale): | ||||
| self.assertAlmostEqualDateTime(pre_renewal_next_payment, subs.next_payment) | self.assertAlmostEqualDateTime(pre_renewal_next_payment, subs.next_payment) | ||||
| # Test the order | # Test the order | ||||
| renewal_order = subs.latest_order() | renewal_order = subs.latest_order() | ||||
| first_transaction = renewal_order.transaction_set.earliest() | first_transaction = renewal_order.transaction_set.earliest() | ||||
| failed_trans_pk = first_transaction.pk | failed_trans_pk = first_transaction.pk | ||||
| self.assertEqual('soft-failed', renewal_order.status) | self.assertEqual('soft-failed', renewal_order.status) | ||||
| self.assertEqual(1, renewal_order.collection_attempts) | self.assertEqual(1, renewal_order.collection_attempts) | ||||
| self.assertNotEqual(pre_renewal_order_pk, renewal_order.pk, | self.assertNotEqual( | ||||
| 'A new order should have been created for the renewal') | pre_renewal_order_pk, | ||||
| renewal_order.pk, | |||||
| 'A new order should have been created for the renewal', | |||||
| ) | |||||
| self.assertAlmostEqualDateTime(first_tick_time + retry_after, renewal_order.retry_after) | self.assertAlmostEqualDateTime(first_tick_time + retry_after, renewal_order.retry_after) | ||||
| payment_token = subs.payment_method.token | payment_token = subs.payment_method.token | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_soft_failed, | signal=signals.automatic_payment_soft_failed, | ||||
| sender=renewal_order, transaction=first_transaction) | sender=renewal_order, | ||||
| transaction=first_transaction, | |||||
| ) | |||||
| self.signals.reset_mock() | self.signals.reset_mock() | ||||
| # Do another clock tick an hour later. This should pick up on the | # Do another clock tick an hour later. This should pick up on the | ||||
| # failed attempt and renew the subscription. | # failed attempt and renew the subscription. | ||||
| second_tick_time = first_tick_time + relativedelta(hours=1) | second_tick_time = first_tick_time + relativedelta(hours=1) | ||||
| with override_settings(LOOPER_CLOCK_MAX_AUTO_ATTEMPTS=max_collection_attempts): | with override_settings(LOOPER_CLOCK_MAX_AUTO_ATTEMPTS=max_collection_attempts): | ||||
| self._clock_tick(second_tick_time) | self._clock_tick(second_tick_time) | ||||
| # The subscription should be renewed now. | # The subscription should be renewed now. | ||||
| subs.refresh_from_db() | subs.refresh_from_db() | ||||
| self.assertEqual('active', subs.status) | self.assertEqual('active', subs.status) | ||||
| self.assertAlmostEqualDateTime( | self.assertAlmostEqualDateTime( | ||||
| second_tick_time + relativedelta(months=1), | second_tick_time + relativedelta(months=1), subs.next_payment | ||||
| subs.next_payment) | ) | ||||
| self.assertEqual(1, subs.intervals_elapsed) | self.assertEqual(1, subs.intervals_elapsed) | ||||
| # Test the order | # Test the order | ||||
| latest_order = subs.latest_order() | latest_order = subs.latest_order() | ||||
| self.assertEqual('paid', latest_order.status) | self.assertEqual('paid', latest_order.status) | ||||
| self.assertEqual(2, latest_order.collection_attempts) | self.assertEqual(2, latest_order.collection_attempts) | ||||
| self.assertNotEqual(pre_renewal_order_pk, latest_order.pk) | self.assertNotEqual(pre_renewal_order_pk, latest_order.pk) | ||||
| self.assertEqual(renewal_order.pk, latest_order.pk, | self.assertEqual( | ||||
| 'After failing, the renewal order should be reused.') | renewal_order.pk, latest_order.pk, 'After failing, the renewal order should be reused.' | ||||
| ) | |||||
| self.assertAlmostEqualDateTime(first_tick_time + retry_after, renewal_order.retry_after) | self.assertAlmostEqualDateTime(first_tick_time + retry_after, renewal_order.retry_after) | ||||
| # Test the transaction | # Test the transaction | ||||
| new_trans = renewal_order.latest_transaction() | new_trans = renewal_order.latest_transaction() | ||||
| self.assertEqual(2, renewal_order.transaction_set.count()) | self.assertEqual(2, renewal_order.transaction_set.count()) | ||||
| self.assertEqual({failed_trans_pk, new_trans.pk}, | self.assertEqual( | ||||
| {t.pk for t in renewal_order.transaction_set.all()}) | {failed_trans_pk, new_trans.pk}, {t.pk for t in renewal_order.transaction_set.all()} | ||||
| ) | |||||
| self.assertEqual('succeeded', new_trans.status) | self.assertEqual('succeeded', new_trans.status) | ||||
| self.assertEqual(subs.price, new_trans.amount) | self.assertEqual(subs.price, new_trans.amount) | ||||
| entries_q = admin_log.entries_for(new_trans) | entries_q = admin_log.entries_for(new_trans) | ||||
| self.assertEqual(1, len(entries_q)) | self.assertEqual(1, len(entries_q)) | ||||
| self.assertIn('success', entries_q.first().change_message) | self.assertIn('success', entries_q.first().change_message) | ||||
| self.assertGreater(new_trans.pk, failed_trans_pk, | self.assertGreater( | ||||
| 'A new transaction should have been made for the retry.') | new_trans.pk, failed_trans_pk, 'A new transaction should have been made for the retry.' | ||||
| ) | |||||
| # Test the calls on the gateway | # Test the calls on the gateway | ||||
| mock_transact_sale.assert_has_calls([ | mock_transact_sale.assert_has_calls( | ||||
| mock.call(payment_token, subs.price), | [mock.call(payment_token, subs.price), mock.call(payment_token, subs.price)] | ||||
| mock.call(payment_token, subs.price), | ) | ||||
| ]) | |||||
| # Test the sent signals | # Test the sent signals | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_succesful, | signal=signals.automatic_payment_succesful, sender=renewal_order, transaction=new_trans | ||||
| sender=renewal_order, transaction=new_trans) | ) | ||||
| @mock.patch('looper.gateways.MockableGateway.transact_sale') | @mock.patch('looper.gateways.MockableGateway.transact_sale') | ||||
| def test_double_failure(self, mock_transact_sale): | def test_double_failure(self, mock_transact_sale): | ||||
| subs = self.create_active_subscription() | subs = self.create_active_subscription() | ||||
| old_next_payment = subs.next_payment | old_next_payment = subs.next_payment | ||||
| # Both calls should fail. | # Both calls should fail. | ||||
| mock_transact_sale.side_effect = [ | mock_transact_sale.side_effect = [ | ||||
| exceptions.GatewayError('mock gateway error'), | exceptions.GatewayError('mock gateway error'), | ||||
| exceptions.GatewayError('mock gateway error'), | exceptions.GatewayError('mock gateway error'), | ||||
| ] | ] | ||||
| max_collection_attempts = 2 | max_collection_attempts = 2 | ||||
| first_tick_time = self.start_time + subs.interval | first_tick_time = self.start_time + subs.interval | ||||
| with override_settings(LOOPER_CLOCK_MAX_AUTO_ATTEMPTS=max_collection_attempts): | with override_settings(LOOPER_CLOCK_MAX_AUTO_ATTEMPTS=max_collection_attempts): | ||||
| self._clock_tick(first_tick_time) | self._clock_tick(first_tick_time) | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_soft_failed, | signal=signals.automatic_payment_soft_failed, sender=mock.ANY, transaction=mock.ANY | ||||
| sender=mock.ANY, transaction=mock.ANY) | ) | ||||
| self.signals.reset_mock() | self.signals.reset_mock() | ||||
| first_failure_order_pk = subs.latest_order().pk | first_failure_order_pk = subs.latest_order().pk | ||||
| payment_token = subs.payment_method.token | payment_token = subs.payment_method.token | ||||
| # Do another clock tick an hour later. This should pick up on the | # Do another clock tick an hour later. This should pick up on the | ||||
| # failed attempt and fail again. | # failed attempt and fail again. | ||||
| second_tick_time = first_tick_time + relativedelta(hours=1) | second_tick_time = first_tick_time + relativedelta(hours=1) | ||||
| Show All 18 Lines | def test_double_failure(self, mock_transact_sale): | ||||
| new_trans = new_order.latest_transaction() | new_trans = new_order.latest_transaction() | ||||
| self.assertEqual('failed', new_trans.status) | self.assertEqual('failed', new_trans.status) | ||||
| self.assertEqual(subs.price, new_trans.amount) | self.assertEqual(subs.price, new_trans.amount) | ||||
| entries_q = admin_log.entries_for(new_trans) | entries_q = admin_log.entries_for(new_trans) | ||||
| self.assertEqual(1, len(entries_q)) | self.assertEqual(1, len(entries_q)) | ||||
| self.assertIn('failed', entries_q.first().change_message) | self.assertIn('failed', entries_q.first().change_message) | ||||
| # Test the calls on the gateway | # Test the calls on the gateway | ||||
| mock_transact_sale.assert_has_calls([ | mock_transact_sale.assert_has_calls( | ||||
| mock.call(payment_token, subs.price), | [mock.call(payment_token, subs.price), mock.call(payment_token, subs.price)] | ||||
| mock.call(payment_token, subs.price), | ) | ||||
| ]) | |||||
| # Test the sent signals | # Test the sent signals | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_failed, | signal=signals.automatic_payment_failed, sender=new_order, transaction=new_trans | ||||
| sender=new_order, transaction=new_trans) | ) | ||||
| @mock.patch('looper.gateways.MockableGateway.transact_sale') | @mock.patch('looper.gateways.MockableGateway.transact_sale') | ||||
| def test_high_frequency_clock(self, mock_transact_sale): | def test_high_frequency_clock(self, mock_transact_sale): | ||||
| subs = self.create_active_subscription() | subs = self.create_active_subscription() | ||||
| pre_renewal_next_payment = subs.next_payment | pre_renewal_next_payment = subs.next_payment | ||||
| # Only one call should be made, because the clock is running faster | # Only one call should be made, because the clock is running faster | ||||
| # than the LOOPER_ORDER_RETRY_AFTER setting. | # than the LOOPER_ORDER_RETRY_AFTER setting. | ||||
| Show All 25 Lines | def test_high_frequency_clock(self, mock_transact_sale): | ||||
| # The order should not be touched | # The order should not be touched | ||||
| renewal_order.refresh_from_db() | renewal_order.refresh_from_db() | ||||
| self.assertEqual('soft-failed', renewal_order.status) | self.assertEqual('soft-failed', renewal_order.status) | ||||
| self.assertEqual(1, renewal_order.collection_attempts) | self.assertEqual(1, renewal_order.collection_attempts) | ||||
| self.assertAlmostEqualDateTime(first_tick_time + retry_after, renewal_order.retry_after) | self.assertAlmostEqualDateTime(first_tick_time + retry_after, renewal_order.retry_after) | ||||
| # Test the calls on the gateway | # Test the calls on the gateway | ||||
| mock_transact_sale.assert_has_calls([ | mock_transact_sale.assert_has_calls([mock.call(payment_token, subs.price)]) | ||||
| mock.call(payment_token, subs.price), | |||||
| ]) | |||||
| # Test the sent signals | # Test the sent signals | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.automatic_payment_soft_failed, | signal=signals.automatic_payment_soft_failed, | ||||
| sender=renewal_order, transaction=renewal_order.latest_transaction()) | sender=renewal_order, | ||||
| transaction=renewal_order.latest_transaction(), | |||||
| ) | |||||
| class PendingCancellationTestCase(AbstractClockTest): | class PendingCancellationTestCase(AbstractClockTest): | ||||
| def test_pending_cancellation(self): | def test_pending_cancellation(self): | ||||
| subs = self.create_active_subscription() | subs = self.create_active_subscription() | ||||
| last_order_pk = subs.latest_order().pk | last_order_pk = subs.latest_order().pk | ||||
| next_payment = subs.next_payment | next_payment = subs.next_payment | ||||
| Show All 17 Lines | def test_pending_cancellation(self): | ||||
| self.assertAlmostEqualDateTime(next_payment, subs.next_payment) | self.assertAlmostEqualDateTime(next_payment, subs.next_payment) | ||||
| # Test the order | # Test the order | ||||
| last_order = subs.latest_order() | last_order = subs.latest_order() | ||||
| self.assertEqual(last_order_pk, last_order.pk, 'Cancelling should not create an order') | self.assertEqual(last_order_pk, last_order.pk, 'Cancelling should not create an order') | ||||
| class ManagedSubscriptionTest(AbstractClockTest): | class ManagedSubscriptionTest(AbstractClockTest): | ||||
| def _test_renew_managed(self, last_notification: typing.Optional[datetime.datetime]): | def _test_renew_managed(self, last_notification: typing.Optional[datetime.datetime]): | ||||
| subs = self.create_active_subscription(collection_method='managed') | subs = self.create_active_subscription(collection_method='managed') | ||||
| subs.next_payment = self.start_time | subs.next_payment = self.start_time | ||||
| subs.last_notification = last_notification | subs.last_notification = last_notification | ||||
| subs.save() | subs.save() | ||||
| self.assertIsNone(subs.latest_order()) | self.assertIsNone(subs.latest_order()) | ||||
| old_next_payment = subs.next_payment | old_next_payment = subs.next_payment | ||||
| # Tick after the next_payment has passed. | # Tick after the next_payment has passed. | ||||
| tick_time = self.start_time + relativedelta(minutes=10) | tick_time = self.start_time + relativedelta(minutes=10) | ||||
| self._clock_tick(tick_time) | self._clock_tick(tick_time) | ||||
| # The subscription should still be active and unchanged. | # The subscription should still be active and unchanged. | ||||
| subs.refresh_from_db() | subs.refresh_from_db() | ||||
| self.assertEqual('active', subs.status) | self.assertEqual('active', subs.status) | ||||
| self.assertAlmostEqualDateTime(old_next_payment, subs.next_payment) | self.assertAlmostEqualDateTime(old_next_payment, subs.next_payment) | ||||
| self.assertAlmostEqualDateTime(tick_time, subs.last_notification) | self.assertAlmostEqualDateTime(tick_time, subs.last_notification) | ||||
| # Test the order, shouldn't be created all of a sudden. | # Test the order, shouldn't be created all of a sudden. | ||||
| self.assertIsNone(subs.latest_order()) | self.assertIsNone(subs.latest_order()) | ||||
| self.signals.assert_called_once_with( | self.signals.assert_called_once_with( | ||||
| signal=signals.managed_subscription_notification, | signal=signals.managed_subscription_notification, sender=subs | ||||
| sender=subs) | ) | ||||
| def test_renew_managed_subscription(self): | def test_renew_managed_subscription(self): | ||||
| self._test_renew_managed(self.start_time - relativedelta(minutes=1)) | self._test_renew_managed(self.start_time - relativedelta(minutes=1)) | ||||
| def test_renew_managed_subscription_no_last_notif(self): | def test_renew_managed_subscription_no_last_notif(self): | ||||
| self._test_renew_managed(None) | self._test_renew_managed(None) | ||||
| def test_renew_managed_subscription_already_notified(self): | def test_renew_managed_subscription_already_notified(self): | ||||
| Show All 25 Lines | |||||