def test_pass_optional_parameters_omitted(self): """ Request omits optional parameters. """ filter_ = self._filter({ 'depth': 100, 'minWeightMagnitude': 13, 'seed': Seed(self.trytes2), 'transfers': [ self.transfer1, self.transfer2 ], }) self.assertFilterPasses(filter_) self.assertDictEqual( filter_.cleaned_data, { 'changeAddress': None, 'inputs': None, 'depth': 100, 'minWeightMagnitude': 13, 'seed': Seed(self.trytes2), 'transfers': [ self.transfer1, self.transfer2 ], } )
def __init__(self, adapter, seed=None, testnet=False): # type: (AdapterSpec, Optional[TrytesCompatible], bool) -> None """ :param seed: Seed used to generate new addresses. If not provided, a random one will be generated. Note: This value is never transferred to the node/network. """ super(cornode, self).__init__(adapter, testnet) self.seed = Seed(seed) if seed else Seed.random()
def test_pass_optional_parameters_excluded(self): """ The request contains only required parameters. """ filter_ = self._filter({ 'seed': Seed(self.seed), }) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, { 'seed': Seed(self.seed), 'start': 0, 'stop': None, 'threshold': None, })
def test_no_stop_threshold_not_met(self): """ No ``stop`` provided, balance does not meet ``threshold``. """ self.adapter.seed_response('getBalances', { 'balances': [42, 29, 0], }) # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. # noinspection PyUnusedLocal def mock_address_generator(ag, start, step=1): for addy in [self.addy0, self.addy1, self.addy2][start::step]: yield addy # When ``stop`` is None, the command uses a generator internally. with patch( 'cornode.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): with self.assertRaises(BadApiResponse): self.command( seed=Seed.random(), threshold=72, )
def test_stop_threshold_zero(self): """ ``stop`` provided, ``threshold`` is 0. """ # Note that the first address has a zero balance. self.adapter.seed_response('getBalances', { 'balances': [0, 1], }) # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. mock_address_generator = Mock(return_value=[self.addy0, self.addy1]) with patch( 'cornode.crypto.addresses.AddressGenerator.get_addresses', mock_address_generator, ): response = self.command( seed=Seed.random(), stop=2, threshold=0, ) self.assertEqual(response['totalBalance'], 1) self.assertEqual(len(response['inputs']), 1) # Address 0 was skipped because it has a zero balance. input0 = response['inputs'][0] self.assertIsInstance(input0, Address) self.assertEqual(input0, self.addy1) self.assertEqual(input0.balance, 1) self.assertEqual(input0.key_index, 1)
def test_pass_happy_path(self): """ Request is valid. """ request = { 'changeAddress': Address(self.trytes1), 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes2), 'inputs': [ Address(self.trytes3), Address(self.trytes4), ], 'transfers': [ self.transfer1, self.transfer2 ], } filter_ = self._filter(request) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request)
def test_no_transactions(self): """ There are no transactions for the specified seed. """ # To speed up the test, we will mock the address generator. # :py:class:`cornode.crypto.addresses.AddressGenerator` already has # its own test case, so this does not impact the stability of the # codebase. # noinspection PyUnusedLocal def create_generator(ag, start, step=1): for addy in [self.addy1][start::step]: yield addy self.adapter.seed_response( 'findTransactions', { 'duration': 1, 'hashes': [], }, ) with patch( 'cornode.crypto.addresses.AddressGenerator.create_iterator', create_generator, ): response = self.command(seed=Seed.random()) self.assertDictEqual(response, {'bundles': []})
def test_pass_compatible_types(self): """ The request contains values that can be converted to the expected types. """ filter_ = self._filter({ # ``seed`` can be any value that is convertible into a # TryteString. 'seed': binary_type(self.seed), # These values must still be integers/bools, however. 'start': 42, 'stop': 86, 'inclusionStates': True, }) self.assertFilterPasses(filter_) self.assertDictEqual( filter_.cleaned_data, { 'seed': Seed(self.seed), 'start': 42, 'stop': 86, 'inclusionStates': True, }, )
def test_fail_transfers_contents_invalid(self): """ ``transfers`` is a non-empty array, but it contains invalid values. """ self.assertFilterErrors( { 'transfers': [ None, # This value is valid; just adding it to make sure the filter # doesn't cheat! ProposedTransaction(address=Address(self.trytes2), value=42), {'address': Address(self.trytes2), 'value': 42}, ], 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, { 'transfers.0': [f.Required.CODE_EMPTY], 'transfers.2': [f.Type.CODE_WRONG_TYPE], }, )
def test_global_cache(self): """ Installing a cache that affects all :py:class:`AddressGenerator` instances. """ # Install the cache globally. AddressGenerator.cache = MemoryAddressCache() mock_generate_address = Mock(return_value=self.addy) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): generator1 = AddressGenerator(Seed.random()) addy1 = generator1.get_addresses(42) mock_generate_address.assert_called_once() # The second time we try to generate the same address, it is # fetched from the cache. addy2 = generator1.get_addresses(42) mock_generate_address.assert_called_once() self.assertEqual(addy2, addy1) # Create a new AddressGenerator and verify it uses the same # cache. generator2 = AddressGenerator(generator1.seed) # Cache is global, so the cached address is returned again. addy3 = generator2.get_addresses(42) mock_generate_address.assert_called_once() self.assertEqual(addy3, addy1)
def test_local_cache(self): """ Installing a cache only for a single instance of :py:class:`AddressGenerator`. """ mock_generate_address = Mock(return_value=self.addy) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): generator1 = AddressGenerator(Seed.random()) # Install the cache locally. generator1.cache = MemoryAddressCache() addy1 = generator1.get_addresses(42) mock_generate_address.assert_called_once() # The second time we try to generate the same address, it is # fetched from the cache. addy2 = generator1.get_addresses(42) mock_generate_address.assert_called_once() self.assertEqual(addy2, addy1) # Create a new instance to verify it has its own cache. generator2 = AddressGenerator(generator1.seed) # The generator has its own cache instance, so even though the # resulting address is the same, it is not fetched from cache. addy3 = generator2.get_addresses(42) self.assertEqual(mock_generate_address.call_count, 2) self.assertEqual(addy3, addy1)
def test_pass_optional_parameters_excluded(self): """ Request omits ``index`` and ``count``. """ filter_ = self._filter({ 'seed': Seed(self.seed), }) self.assertFilterPasses(filter_) self.assertDictEqual( filter_.cleaned_data, { 'seed': Seed(self.seed), 'index': 0, 'count': None, }, )
def test_cache_miss_seed(self): """ Cached addresses are keyed by seed. """ AddressGenerator.cache = MemoryAddressCache() mock_generate_address = Mock(return_value=self.addy) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): generator1 = AddressGenerator(Seed.random()) generator1.get_addresses(42) mock_generate_address.assert_called_once() generator2 = AddressGenerator(Seed.random()) generator2.get_addresses(42) self.assertEqual(mock_generate_address.call_count, 2)
def test_start(self): """ Using ``start`` to offset the key range. """ self.adapter.seed_response('getBalances', { 'balances': [86], }) # ``getInputs`` uses ``findTransactions`` to identify unused # addresses. # noinspection SpellCheckingInspection self.adapter.seed_response( 'findTransactions', { 'hashes': [ TransactionHash( b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H'), ], }) self.adapter.seed_response('findTransactions', { 'hashes': [], }) # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that # functionality, so we can get away with mocking it here. # noinspection PyUnusedLocal def mock_address_generator(ag, start, step=1): # If ``start`` has the wrong value, return garbage to make the # test asplode. for addy in [None, self.addy1, self.addy2][start::step]: yield addy # When ``stop`` is None, the command uses a generator internally. with patch( 'cornode.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): response = self.command( seed=Seed.random(), start=1, ) self.assertEqual(response['totalBalance'], 86) self.assertEqual(len(response['inputs']), 1) input0 = response['inputs'][0] self.assertIsInstance(input0, Address) self.assertEqual(input0, self.addy1) self.assertEqual(input0.balance, 86) self.assertEqual(input0.key_index, 1)
def test_fail_stop_too_small(self): """ ``stop`` is less than 0. """ self.assertFilterErrors( { 'stop': -1, 'seed': Seed(self.seed), }, { 'stop': [f.Min.CODE_TOO_SMALL], }, )
def test_fail_threshold_too_small(self): """ ``threshold`` is less than 0. """ self.assertFilterErrors( { 'threshold': -1, 'seed': Seed(self.seed), }, { 'threshold': [f.Min.CODE_TOO_SMALL], }, )
def test_fail_inclusion_states_wrong_type(self): """ ``inclusionStates`` is not a boolean. """ self.assertFilterErrors( { 'inclusionStates': '1', 'seed': Seed(self.seed), }, { 'inclusionStates': [f.Type.CODE_WRONG_TYPE], }, )
def test_fail_interval_too_large(self): """ ``stop`` is way more than ``start``. """ self.assertFilterErrors( { 'start': 0, 'stop': GetAccountDataRequestFilter.MAX_INTERVAL + 1, 'seed': Seed(self.seed), }, { 'stop': [GetAccountDataRequestFilter.CODE_INTERVAL_TOO_BIG], }, )
def test_fail_stop_occurs_before_start(self): """ ``stop`` is less than ``start``. """ self.assertFilterErrors( { 'start': 1, 'stop': 0, 'seed': Seed(self.seed), }, { 'start': [GetAccountDataRequestFilter.CODE_INTERVAL_INVALID], }, )
def test_fail_count_too_small(self): """ ``count`` is less than 1. """ self.assertFilterErrors( { 'count': 0, 'seed': Seed(self.seed), }, { 'count': [f.Min.CODE_TOO_SMALL], }, )
def test_fail_stop_string(self): """ ``stop`` is a string. """ self.assertFilterErrors( { # Not valid; it must be an int. 'stop': '0', 'seed': Seed(self.seed), }, { 'stop': [f.Type.CODE_WRONG_TYPE], }, )
def test_pass_happy_path(self): """ Request is valid. """ request = { 'seed': Seed(self.seed), 'index': 1, 'count': 1, } filter_ = self._filter(request) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request)
def test_thread_safety(self): """ Address cache is thread-safe, eliminating invalid cache misses when multiple threads attempt to access the cache concurrently. """ AddressGenerator.cache = MemoryAddressCache() seed = Seed.random() generated = [] def get_address(): generator = AddressGenerator(seed) generated.extend(generator.get_addresses(0)) # noinspection PyUnusedLocal def mock_generate_address(address_generator, key_iterator): # type: (AddressGenerator, KeyIterator) -> Address # Insert a teensy delay, to make it more likely that multiple # threads hit the cache concurrently. sleep(0.01) # Note that in this test, the address generator always returns a # new instance. return Address(self.addy, key_index=key_iterator.current) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): threads = [Thread(target=get_address) for _ in range(100)] for t in threads: t.start() for t in threads: t.join() # Quick sanity check. self.assertEqual(len(generated), len(threads)) # If the cache is operating in a thread-safe manner, then it will # always return the exact same instance, given the same seed and # key index. expected = generated[0] for actual in generated[1:]: # Compare `id` values instead of using ``self.assertIs`` because # the failure message is a bit easier to understand. self.assertEqual(id(actual), id(expected))
def test_fail_unexpected_parameters(self): """ The request contains unexpected parameters. """ self.assertFilterErrors( { 'seed': Seed(self.seed), # Told you I did. Reckless is he. Now, matters are worse. 'foo': 'bar', }, { 'foo': [f.FilterMapper.CODE_EXTRA_KEY], }, )
def test_fail_stop_float(self): """ ``stop`` is a float. """ self.assertFilterErrors( { # Even with an empty fpart, floats are not valid. # It's gotta be an int. 'stop': 8.0, 'seed': Seed(self.seed), }, { 'stop': [f.Type.CODE_WRONG_TYPE], }, )
def test_fail_unexpected_parameters(self): """ The request contains unexpected parameters. """ self.assertFilterErrors( { 'seed': Seed(self.seed), # Your rules are really beginning to annoy me. 'foo': 'bar', }, { 'foo': [f.FilterMapper.CODE_EXTRA_KEY], }, )
def test_pass_happy_path(self): """ Request is valid. """ request = { 'seed': Seed(self.seed), 'start': 0, 'stop': 10, 'inclusionStates': True, } filter_ = self._filter(request) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request)
def test_fail_index_string(self): """ ``index`` is a string value. """ self.assertFilterErrors( { # Not valid; it must be an int. 'index': '42', 'seed': Seed(self.seed), }, { 'index': [f.Type.CODE_WRONG_TYPE], }, )
def test_fail_index_float(self): """ ``index`` is a float value. """ self.assertFilterErrors( { # Not valid, even with an empty fpart; it must be an int. 'index': 42.0, 'seed': Seed(self.seed), }, { 'index': [f.Type.CODE_WRONG_TYPE], }, )
def test_happy_path(self): """ Loading account data for an account. """ # noinspection PyUnusedLocal def mock_iter_used_addresses(adapter, seed, start): """ Mocks the ``iter_used_addresses`` function, so that we can simulate its functionality without actually connecting to the Tangle. References: - :py:func:`cornode.commands.extended.utils.iter_used_addresses` """ yield self.addy1, [self.hash1] yield self.addy2, [self.hash2] mock_get_balances = Mock(return_value={'balances': [42, 0]}) # Not particularly realistic, but good enough to prove that the # mocked function was invoked correctly. bundles = [Bundle(), Bundle()] mock_get_bundles_from_transaction_hashes = Mock(return_value=bundles) with patch( 'cornode.commands.extended.get_account_data.iter_used_addresses', mock_iter_used_addresses, ): with patch( 'cornode.commands.extended.get_account_data.get_bundles_from_transaction_hashes', mock_get_bundles_from_transaction_hashes, ): with patch( 'cornode.commands.core.get_balances.GetBalancesCommand._execute', mock_get_balances, ): response = self.command(seed=Seed.random()) self.assertDictEqual( response, { 'addresses': [self.addy1, self.addy2], 'balance': 42, 'bundles': bundles, }, )