예제 #1
0
  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
        ],
      }
    )
예제 #2
0
    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()
예제 #3
0
    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,
        })
예제 #4
0
    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,
                )
예제 #5
0
    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)
예제 #6
0
  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)
예제 #7
0
  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,
            },
        )
예제 #9
0
  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],
      },
    )
예제 #10
0
    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)
예제 #11
0
    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,
      },
    )
예제 #13
0
    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)
예제 #14
0
    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],
         },
     )
예제 #16
0
 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)
예제 #23
0
    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))
예제 #24
0
    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,
            },
        )