def test_one_callback(self): action = Action('test', sources=['draft'], callbacks=[disable_invoice]) self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') action.change_state(state) self.assertEqual(self.invoice.status, 'draft') self.assertFalse(self.invoice.is_available) self.assertFalse(state.is_locked())
def test_many_side_effects(self): action = Action('test', sources=['draft'], side_effects=[disable_invoice, enable_invoice]) self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') action.change_state(state) self.assertEqual(self.invoice.status, 'draft') self.assertTrue(self.invoice.is_available) self.assertFalse(state.is_locked())
def test_many_side_effects(self): transition = Transition('test', sources=[], target='cancelled', side_effects=[disable_invoice, enable_invoice]) self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') transition.change_state(state) self.assertEqual(self.invoice.status, transition.target) self.assertTrue(self.invoice.is_available) self.assertFalse(state.is_locked())
def test_one_callback(self): transition = Transition('test', sources=[], target='cancelled', callbacks=[disable_invoice]) self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') transition.change_state(state) self.assertEqual(self.invoice.status, transition.target) self.assertFalse(self.invoice.is_available) self.assertFalse(state.is_locked())
def complete_transition(self, state: State, **kwargs): """ It completes the transition process for provided state. The instance will be unlocked and callbacks executed :param state: State object """ state.set_state(self.target) logging.info(f'{state.instance_key} state changed to {self.target}') state.unlock() logging.info(f'{state.instance_key} has been unlocked') self.callbacks.execute(state, **kwargs)
def test_failure_during_callbacks_with_failed_state(self): action = Action( 'test', failed_state='failed', sources=['draft'], side_effects=[disable_invoice, fail_invoice, enable_invoice]) self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') action.change_state(state) self.assertEqual(self.invoice.status, 'failed') self.assertFalse(self.invoice.is_available) self.assertFalse(state.is_locked())
def fail_transition(self, state: State, exception: Exception, **kwargs): """ It triggers fail transition in case of any failure during the side effects execution. :param state: State object :param exception: Exception that caused transition failure """ if self.failed_state: state.set_state(self.failed_state) logging.info( f'{state.instance_key} state changed to {self.failed_state}') state.unlock() logging.info(f'{state.instance_key} has been unlocked') self.failure_callbacks.execute(state, exception=exception, **kwargs)
def test_failure_during_side_effect_with_failed_state(self): transition = Transition( 'test', sources=[], target='cancelled', failed_state='failed', side_effects=[disable_invoice, fail_invoice, enable_invoice]) self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') transition.change_state(state) self.assertEqual(self.invoice.status, 'failed') self.assertFalse(self.invoice.is_available) self.assertFalse(state.is_locked())
def test_one_callback(self): transition = Transition('test', sources=[], target='success', side_effects=[fail_invoice], failure_callbacks=[disable_invoice], failed_state='failed') self.assertTrue(self.invoice.is_available) state = State(self.invoice, 'status') transition.change_state(state) self.assertEqual(self.invoice.status, 'failed') self.assertFalse(self.invoice.is_available) self.assertFalse(state.is_locked())
def test_many_callback(self): action = Action('test', side_effects=[fail_invoice], sources=['draft'], failure_callbacks=[disable_invoice, receive_invoice], failed_state='failed') self.assertTrue(self.invoice.is_available) self.assertFalse(self.invoice.customer_received) state = State(self.invoice, 'status') action.change_state(state) self.assertEqual(self.invoice.status, 'failed') self.assertFalse(self.invoice.is_available) self.assertTrue(self.invoice.customer_received) self.assertFalse(state.is_locked())
def test_callbacks_with_parameters(self): update_invoice(self.invoice, is_available=True, customer_received=True) action = Action('test', failed_state='failed', sources=['draft'], callbacks=[update_invoice]) self.invoice.refresh_from_db() self.assertTrue(self.invoice.is_available) self.assertTrue(self.invoice.customer_received) state = State(self.invoice, 'status') action.change_state(state, is_available=False, customer_received=False) self.invoice.refresh_from_db() self.assertFalse(self.invoice.is_available) self.assertFalse(self.invoice.customer_received) self.assertFalse(state.is_locked())
def test_side_effect_with_parameters(self): update_invoice(self.invoice, is_available=True, customer_received=True) transition = Transition('test', sources=[], target='cancelled', failed_state='failed', side_effects=[update_invoice]) self.invoice.refresh_from_db() self.assertTrue(self.invoice.is_available) self.assertTrue(self.invoice.customer_received) state = State(self.invoice, 'status') transition.change_state(state, is_available=False, customer_received=False) self.invoice.refresh_from_db() self.assertFalse(self.invoice.is_available) self.assertFalse(self.invoice.customer_received) self.assertFalse(state.is_locked())
def is_valid(self, state: State, user=None) -> bool: """ It validates this process to meet conditions and pass permissions :param state: State object :param user: any object used to pass permissions :return: True or False """ return (not state.is_locked() and self.permissions.execute(state, user) and self.conditions.execute(state))
def change_state(self, state: State, **kwargs): """ This method changes a state by the following algorithm: - Lock state - Change state to `in progress` if such exists - Run side effects which should run `complete_transition` in case of success or `fail_transition` in case of failure. :param state: State object """ if state.is_locked(): logging.info(f'{state.instance_key} is locked') raise TransitionNotAllowed("State is locked") state.lock() logging.info(f'{state.instance_key} has been locked') if self.in_progress_state: state.set_state(self.in_progress_state) logging.info( f'{state.instance_key} state changed to {self.in_progress_state}' ) self.side_effects.execute(state, **kwargs)
def test_failure_callback_exception_passed(self, debug_mock): update_invoice(self.invoice, is_available=True, customer_received=True) action = Action('test', failed_state='failed', side_effects=[fail_invoice], sources=['draft'], failure_callbacks=[debug_action]) self.invoice.refresh_from_db() state = State(self.invoice, 'status') action.change_state(state, foo="bar") self.assertTrue(debug_mock.called) self.assertEqual(debug_mock.call_count, 1) call_args = debug_mock.call_args[0] call_kwargs = debug_mock.call_args[1] self.assertEqual(call_args, (self.invoice, )) self.assertEqual(len(call_kwargs), 2) self.assertTrue(isinstance(call_kwargs['exception'], Exception)) self.assertEqual(call_kwargs['foo'], 'bar')
def setUp(self) -> None: self.state = State(Invoice.objects.create(status='draft'), 'status')
class StateTestCase(TestCase): def setUp(self) -> None: self.state = State(Invoice.objects.create(status='draft'), 'status') def test_hash_remains_the_same(self): self.assertEqual(self.state._get_hash(), self.state._get_hash()) def test_get_db_state(self): self.assertEqual(self.state.get_db_state(), 'draft') def test_lock(self): self.assertFalse(self.state.is_locked()) self.state.lock() self.assertTrue(self.state.is_locked()) # nothing should happen self.state.lock() self.assertTrue(self.state.is_locked()) self.state.unlock() self.assertFalse(self.state.is_locked()) def test_set_state(self): self.state.set_state('void') self.assertEqual(self.state.instance.status, 'void') # make sure it was saved to db self.state.instance.refresh_from_db() self.assertEqual(self.state.instance.status, 'void')