def test_main(self): from billy.models.transaction import TransactionModel from billy.models.processors.balanced_payments import BalancedProcessor from billy.scripts import initializedb from billy.scripts import process_transactions def mock_process_transactions(processor, maximum_retry): self.assertIsInstance(processor, BalancedProcessor) self.assertEqual(maximum_retry, 5566) ( flexmock(TransactionModel) .should_receive('process_transactions') .replace_with(mock_process_transactions) .once() ) cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write(textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite billy.processor_factory = billy.models.processors.balanced_payments.BalancedProcessor billy.transaction.maximum_retry = 5566 """)) initializedb.main([initializedb.__file__, cfg_path]) process_transactions.main([process_transactions.__file__, cfg_path])
def test_main(self): from billy.models.transaction import TransactionModel from billy.models.processors.balanced_payments import BalancedProcessor from billy.scripts import initializedb from billy.scripts import process_transactions def mock_process_transactions(processor, maximum_retry): self.assertIsInstance(processor, BalancedProcessor) self.assertEqual(maximum_retry, 5566) (flexmock(TransactionModel).should_receive('process_transactions'). replace_with(mock_process_transactions).once()) cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write( textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite billy.processor_factory = billy.models.processors.balanced_payments.BalancedProcessor billy.transaction.maximum_retry = 5566 """)) initializedb.main([initializedb.__file__, cfg_path]) process_transactions.main([process_transactions.__file__, cfg_path])
def test_main(self, process_transactions_method): cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write(textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite billy.processor_factory = billy.models.processors.balanced_payments.BalancedProcessor billy.transaction.maximum_retry = 5566 """)) initializedb.main([initializedb.__file__, cfg_path]) process_transactions.main([process_transactions.__file__, cfg_path]) # ensure process_transaction method is called correctly process_transactions_method.assert_called_once()
def test_main(self, process_transactions_method): cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write( textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite billy.processor_factory = billy.models.processors.balanced_payments.BalancedProcessor billy.transaction.maximum_retry = 5566 """)) initializedb.main([initializedb.__file__, cfg_path]) process_transactions.main([process_transactions.__file__, cfg_path]) # ensure process_transaction method is called correctly process_transactions_method.assert_called_once()
def test_usage(self): filename = '/path/to/process_transactions' old_stdout = sys.stdout usage_out = StringIO.StringIO() sys.stdout = usage_out try: with self.assertRaises(SystemExit): main([filename]) finally: sys.stdout = old_stdout expected = textwrap.dedent("""\ usage: process_transactions <config_uri> (example: "process_transactions development.ini") """) self.assertMultiLineEqual(usage_out.getvalue(), expected)
def test_main_with_crash(self): dummy_processor = DummyProcessor() dummy_processor.debit = mock.Mock() tx_guids = set() debits = [] def mock_charge(transaction): if dummy_processor.debit.call_count == 2: raise KeyboardInterrupt uri = 'MOCK_DEBIT_URI_FOR_{}'.format(transaction.guid) if transaction.guid in tx_guids: return dict( processor_uri=uri, status=TransactionModel.statuses.SUCCEEDED, ) tx_guids.add(transaction.guid) debits.append(uri) return dict( processor_uri=uri, status=TransactionModel.statuses.SUCCEEDED, ) dummy_processor.debit.side_effect = mock_charge cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write( textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite """)) initializedb.main([initializedb.__file__, cfg_path]) settings = get_appsettings(cfg_path) settings = setup_database({}, **settings) session = settings['session'] factory = ModelFactory( session=session, processor_factory=lambda: dummy_processor, settings=settings, ) company_model = factory.create_company_model() customer_model = factory.create_customer_model() plan_model = factory.create_plan_model() subscription_model = factory.create_subscription_model() with db_transaction.manager: company = company_model.create('my_secret_key') plan = plan_model.create( company=company, plan_type=plan_model.types.DEBIT, amount=10, frequency=plan_model.frequencies.MONTHLY, ) customer = customer_model.create(company=company, ) subscription_model.create( customer=customer, plan=plan, funding_instrument_uri='/v1/cards/tester', ) subscription_model.create( customer=customer, plan=plan, funding_instrument_uri='/v1/cards/tester', ) with self.assertRaises(KeyboardInterrupt): process_transactions.main( [process_transactions.__file__, cfg_path], processor=dummy_processor) process_transactions.main([process_transactions.__file__, cfg_path], processor=dummy_processor) # here is the story, we have two subscriptions here # # Subscription1 # Subscription2 # # And the time is not advanced, so we should only have two transactions # to be yielded and processed. However, we assume bad thing happens # durring the process. We let the second call to charge of processor # raises a KeyboardInterrupt error. So, it would look like this # # charge for transaction from Subscription1 # charge for transaction from Subscription2 (Crash) # # Then, we perform the process_transactions again, if it works # correctly, the first transaction is already yield and processed. # # charge for transaction from Subscription2 # # So, there would only be two charges in processor. This is mainly # for making sure we won't duplicate charges/payouts self.assertEqual(len(debits), 2)
def test_main_with_crash(self): from pyramid.paster import get_appsettings from billy.models import setup_database from billy.models.company import CompanyModel from billy.models.customer import CustomerModel from billy.models.plan import PlanModel from billy.models.subscription import SubscriptionModel from billy.scripts import initializedb from billy.scripts import process_transactions class MockProcessor(object): def __init__(self): self.charges = {} self.tx_sn = 0 self.called_times = 0 def create_customer(self, customer): return 'MOCK_PROCESSOR_CUSTOMER_ID' def prepare_customer(self, customer, payment_uri=None): pass def charge(self, transaction): self.called_times += 1 if self.called_times == 2: raise KeyboardInterrupt guid = transaction.guid if guid in self.charges: return self.charges[guid] self.charges[guid] = self.tx_sn self.tx_sn += 1 mock_processor = MockProcessor() cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write(textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite """)) initializedb.main([initializedb.__file__, cfg_path]) settings = get_appsettings(cfg_path) settings = setup_database({}, **settings) session = settings['session'] company_model = CompanyModel(session) customer_model = CustomerModel(session) plan_model = PlanModel(session) subscription_model = SubscriptionModel(session) with db_transaction.manager: company_guid = company_model.create('my_secret_key') plan_guid = plan_model.create( company_guid=company_guid, plan_type=plan_model.TYPE_CHARGE, amount=10, frequency=plan_model.FREQ_MONTHLY, ) customer_guid = customer_model.create( company_guid=company_guid, ) subscription_model.create( customer_guid=customer_guid, plan_guid=plan_guid, payment_uri='/v1/cards/tester', ) subscription_model.create( customer_guid=customer_guid, plan_guid=plan_guid, payment_uri='/v1/cards/tester', ) with self.assertRaises(KeyboardInterrupt): process_transactions.main([process_transactions.__file__, cfg_path], processor=mock_processor) process_transactions.main([process_transactions.__file__, cfg_path], processor=mock_processor) # here is the story, we have two subscriptions here # # Subscription1 # Subscription2 # # And the time is not advanced, so we should only have two transactions # to be yielded and processed. However, we assume bad thing happens # durring the process. We let the second call to charge of processor # raises a KeyboardInterrupt error. So, it would look like this # # charge for transaction from Subscription1 # charge for transaction from Subscription2 (Crash) # # Then, we perform the process_transactions again, if it works # correctly, the first transaction is already yield and processed. # # charge for transaction from Subscription2 # # So, there would only be two charges in processor. This is mainly # for making sure we won't duplicate charges/payouts self.assertEqual(len(mock_processor.charges), 2)
def test_main_with_crash(self): from pyramid.paster import get_appsettings from billy.models import setup_database from billy.models.company import CompanyModel from billy.models.customer import CustomerModel from billy.models.plan import PlanModel from billy.models.subscription import SubscriptionModel from billy.scripts import initializedb from billy.scripts import process_transactions class MockProcessor(object): def __init__(self): self.charges = {} self.tx_sn = 0 self.called_times = 0 def create_customer(self, customer): return 'MOCK_PROCESSOR_CUSTOMER_ID' def prepare_customer(self, customer, payment_uri=None): pass def charge(self, transaction): self.called_times += 1 if self.called_times == 2: raise KeyboardInterrupt guid = transaction.guid if guid in self.charges: return self.charges[guid] self.charges[guid] = self.tx_sn self.tx_sn += 1 mock_processor = MockProcessor() cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write( textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite """)) initializedb.main([initializedb.__file__, cfg_path]) settings = get_appsettings(cfg_path) settings = setup_database({}, **settings) session = settings['session'] company_model = CompanyModel(session) customer_model = CustomerModel(session) plan_model = PlanModel(session) subscription_model = SubscriptionModel(session) with db_transaction.manager: company_guid = company_model.create('my_secret_key') plan_guid = plan_model.create( company_guid=company_guid, plan_type=plan_model.TYPE_CHARGE, amount=10, frequency=plan_model.FREQ_MONTHLY, ) customer_guid = customer_model.create(company_guid=company_guid, ) subscription_model.create( customer_guid=customer_guid, plan_guid=plan_guid, payment_uri='/v1/cards/tester', ) subscription_model.create( customer_guid=customer_guid, plan_guid=plan_guid, payment_uri='/v1/cards/tester', ) with self.assertRaises(KeyboardInterrupt): process_transactions.main( [process_transactions.__file__, cfg_path], processor=mock_processor) process_transactions.main([process_transactions.__file__, cfg_path], processor=mock_processor) # here is the story, we have two subscriptions here # # Subscription1 # Subscription2 # # And the time is not advanced, so we should only have two transactions # to be yielded and processed. However, we assume bad thing happens # durring the process. We let the second call to charge of processor # raises a KeyboardInterrupt error. So, it would look like this # # charge for transaction from Subscription1 # charge for transaction from Subscription2 (Crash) # # Then, we perform the process_transactions again, if it works # correctly, the first transaction is already yield and processed. # # charge for transaction from Subscription2 # # So, there would only be two charges in processor. This is mainly # for making sure we won't duplicate charges/payouts self.assertEqual(len(mock_processor.charges), 2)
def test_main_with_crash(self): dummy_processor = DummyProcessor() dummy_processor.debit = mock.Mock() tx_guids = set() debits = [] def mock_charge(transaction): if dummy_processor.debit.call_count == 2: raise KeyboardInterrupt uri = 'MOCK_DEBIT_URI_FOR_{}'.format(transaction.guid) if transaction.guid in tx_guids: return dict( processor_uri=uri, status=TransactionModel.statuses.SUCCEEDED, ) tx_guids.add(transaction.guid) debits.append(uri) return dict( processor_uri=uri, status=TransactionModel.statuses.SUCCEEDED, ) dummy_processor.debit.side_effect = mock_charge cfg_path = os.path.join(self.temp_dir, 'config.ini') with open(cfg_path, 'wt') as f: f.write(textwrap.dedent("""\ [app:main] use = egg:billy sqlalchemy.url = sqlite:///%(here)s/billy.sqlite """)) initializedb.main([initializedb.__file__, cfg_path]) settings = get_appsettings(cfg_path) settings = setup_database({}, **settings) session = settings['session'] factory = ModelFactory( session=session, processor_factory=lambda: dummy_processor, settings=settings, ) company_model = factory.create_company_model() customer_model = factory.create_customer_model() plan_model = factory.create_plan_model() subscription_model = factory.create_subscription_model() with db_transaction.manager: company = company_model.create('my_secret_key') plan = plan_model.create( company=company, plan_type=plan_model.types.DEBIT, amount=10, frequency=plan_model.frequencies.MONTHLY, ) customer = customer_model.create( company=company, ) subscription_model.create( customer=customer, plan=plan, funding_instrument_uri='/v1/cards/tester', ) subscription_model.create( customer=customer, plan=plan, funding_instrument_uri='/v1/cards/tester', ) with self.assertRaises(KeyboardInterrupt): process_transactions.main([process_transactions.__file__, cfg_path], processor=dummy_processor) process_transactions.main([process_transactions.__file__, cfg_path], processor=dummy_processor) # here is the story, we have two subscriptions here # # Subscription1 # Subscription2 # # And the time is not advanced, so we should only have two transactions # to be yielded and processed. However, we assume bad thing happens # durring the process. We let the second call to charge of processor # raises a KeyboardInterrupt error. So, it would look like this # # charge for transaction from Subscription1 # charge for transaction from Subscription2 (Crash) # # Then, we perform the process_transactions again, if it works # correctly, the first transaction is already yield and processed. # # charge for transaction from Subscription2 # # So, there would only be two charges in processor. This is mainly # for making sure we won't duplicate charges/payouts self.assertEqual(len(debits), 2)