diff --git a/README.rst b/README.rst
index d5077e78..e567bd0d 100644
--- a/README.rst
+++ b/README.rst
@@ -9,12 +9,6 @@ This is the official Python library for the IOTA Core.
It implements both the `official API`_, as well as newly-proposed functionality
(such as signing, bundles, utilities and conversion).
-.. warning::
- This is pre-release software!
- There may be performance and stability issues.
-
- Please report any issues using the `PyOTA Bug Tracker`_.
-
Join the Discussion
===================
If you want to get involved in the community, need help with getting setup,
@@ -23,6 +17,9 @@ Distributed Ledgers and IoT with other people, feel free to join our `Slack`_.
You can also ask questions on our `dedicated forum`_.
+If you encounter any issues while using PyOTA, please report them using the
+`PyOTA Bug Tracker`_.
+
============
Dependencies
============
@@ -33,12 +30,7 @@ Installation
============
To install the latest version::
- pip install --pre pyota
-
-**Important:** PyOTA is currently pre-release software.
-There may be performance and stability issues.
-
-Please report any issues using the `PyOTA Bug Tracker`_.
+ pip install pyota
Installing from Source
======================
diff --git a/setup.py b/setup.py
index 9926378b..497d6e33 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@
name = 'PyOTA',
description = 'IOTA API library for Python',
url = 'https://github.com/iotaledger/iota.lib.py',
- version = '1.0.0b7',
+ version = '1.0.0',
packages = find_packages('src'),
include_package_data = True,
@@ -52,7 +52,7 @@
license = 'MIT',
classifiers = [
- 'Development Status :: 4 - Beta',
+ 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2',
diff --git a/src/iota/adapter/__init__.py b/src/iota/adapter/__init__.py
index b69a2df2..f3444f89 100644
--- a/src/iota/adapter/__init__.py
+++ b/src/iota/adapter/__init__.py
@@ -4,6 +4,7 @@
import json
from abc import ABCMeta, abstractmethod as abstract_method
+from collections import deque
from inspect import isabstract as is_abstract
from socket import getdefaulttimeout as get_default_timeout
from typing import Dict, List, Text, Tuple, Union
@@ -312,7 +313,7 @@ def configure(cls, uri):
def __init__(self):
super(MockAdapter, self).__init__()
- self.responses = {} # type: Dict[Text, List[dict]]
+ self.responses = {} # type: Dict[Text, deque]
self.requests = [] # type: List[dict]
def seed_response(self, command, response):
@@ -337,7 +338,7 @@ def seed_response(self, command, response):
# {'message': 'Hello!'}
"""
if command not in self.responses:
- self.responses[command] = []
+ self.responses[command] = deque()
self.responses[command].append(response)
return self
@@ -350,7 +351,7 @@ def send_request(self, payload, **kwargs):
command = payload['command']
try:
- response = self.responses[command].pop(0)
+ response = self.responses[command].popleft()
except KeyError:
raise with_context(
exc = BadApiResponse(
diff --git a/src/iota/api.py b/src/iota/api.py
index a7b04b20..d37882d5 100644
--- a/src/iota/api.py
+++ b/src/iota/api.py
@@ -385,55 +385,68 @@ def get_bundles(self, transaction):
"""
return self.getBundles(transaction=transaction)
- def get_inputs(self, start=None, end=None, threshold=None):
- # type: (Optional[int], Optional[int], Optional[int]) -> dict
+ def get_inputs(self, start=0, stop=None, threshold=None):
+ # type: (int, Optional[int], Optional[int]) -> dict
"""
Gets all possible inputs of a seed and returns them with the total
balance.
This is either done deterministically (by generating all addresses
- until :py:meth:`find_transactions` returns an empty
- result and then doing :py:meth:`get_balances`), or by providing a
- key range to search.
+ until :py:meth:`find_transactions` returns an empty result), or by
+ providing a key range to search.
:param start:
Starting key index.
+ Defaults to 0.
- :param end:
+ :param stop:
Stop before this index.
Note that this parameter behaves like the ``stop`` attribute in a
- :py:class:`slice` object; the end index is _not_ included in the
+ :py:class:`slice` object; the stop index is *not* included in the
result.
- If not specified, then this method will not stop until it finds
- an unused address.
+ If ``None`` (default), then this method will not stop until it
+ finds an unused address.
:param threshold:
- Determines the minimum threshold for a successful result.
+ If set, determines the minimum threshold for a successful result:
- As soon as this threshold is reached, iteration will stop.
- If the command runs out of addresses before the threshold is
reached, an exception is raised.
+ Note that this method does not attempt to "optimize" the result
+ (e.g., smallest number of inputs, get as close to ``threshold``
+ as possible, etc.); it simply accumulates inputs in order until
+ the threshold is met.
+
+ If ``threshold`` is 0, the first address in the key range with
+ a non-zero balance will be returned (if it exists).
+
+ If ``threshold`` is ``None`` (default), this method will return
+ **all** inputs in the specified key range.
+
:return:
Dict with the following structure::
{
- 'inputs': [
- {
- 'address':
,
- 'balance': ,
- 'keyIndex`: ,
- },
- ...
- ]
-
+ 'inputs':
'totalBalance': ,
}
+ Note that each Address in the result has its ``balance``
+ attribute set.
+
+ Example::
+
+ response = iota.get_inputs(...)
+
+ input0 = response['inputs'][0] # type: Address
+ input0.balance # 42
+
:raise:
- :py:class:`iota.adapter.BadApiResponse` if ``threshold`` is not
- met.
+ met. Not applicable if ``threshold`` is ``None``.
References:
- https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs
@@ -441,7 +454,7 @@ def get_inputs(self, start=None, end=None, threshold=None):
return self.getInputs(
seed = self.seed,
start = start,
- end = end,
+ stop = stop,
threshold = threshold,
)
diff --git a/src/iota/commands/extended/get_inputs.py b/src/iota/commands/extended/get_inputs.py
index 1181c01b..50db9f80 100644
--- a/src/iota/commands/extended/get_inputs.py
+++ b/src/iota/commands/extended/get_inputs.py
@@ -34,7 +34,7 @@ def get_response_filter(self):
pass
def _execute(self, request):
- end = request['end'] # type: Optional[int]
+ stop = request['stop'] # type: Optional[int]
seed = request['seed'] # type: Seed
start = request['start'] # type: int
threshold = request['threshold'] # type: Optional[int]
@@ -42,7 +42,7 @@ def _execute(self, request):
generator = AddressGenerator(seed)
# Determine the addresses we will be scanning.
- if end is None:
+ if stop is None:
# This is similar to the ``getNewAddresses`` command, except it
# is interested in all the addresses that `getNewAddresses`
# skips.
@@ -55,7 +55,7 @@ def _execute(self, request):
else:
break
else:
- addresses = generator.get_addresses(start, end - start)
+ addresses = generator.get_addresses(start, stop)
# Load balances for the addresses that we generated.
gb_response = GetBalancesCommand(self.adapter)(addresses=addresses)
@@ -71,12 +71,7 @@ def _execute(self, request):
addresses[i].balance = balance
if balance:
- result['inputs'].append({
- 'address': addresses[i],
- 'balance': balance,
- 'keyIndex': addresses[i].key_index,
- })
-
+ result['inputs'].append(addresses[i])
result['totalBalance'] += balance
if (threshold is not None) and (result['totalBalance'] >= threshold):
@@ -113,15 +108,15 @@ class GetInputsRequestFilter(RequestFilter):
CODE_INTERVAL_TOO_BIG = 'interval_too_big'
templates = {
- CODE_INTERVAL_INVALID: '``start`` must be <= ``end``',
- CODE_INTERVAL_TOO_BIG: '``end`` - ``start`` must be <= {max_interval}',
+ CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``',
+ CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}',
}
def __init__(self):
super(GetInputsRequestFilter, self).__init__(
{
# These arguments are optional.
- 'end': f.Type(int) | f.Min(0),
+ 'stop': f.Type(int) | f.Min(0),
'start': f.Type(int) | f.Min(0) | f.Optional(0),
'threshold': f.Type(int) | f.Min(0),
@@ -130,7 +125,7 @@ def __init__(self):
},
allow_missing_keys = {
- 'end',
+ 'stop',
'start',
'threshold',
}
@@ -143,8 +138,8 @@ def _apply(self, value):
if self._has_errors:
return filtered
- if filtered['end'] is not None:
- if filtered['start'] > filtered['end']:
+ if filtered['stop'] is not None:
+ if filtered['start'] > filtered['stop']:
filtered['start'] = self._invalid_value(
value = filtered['start'],
reason = self.CODE_INTERVAL_INVALID,
@@ -152,18 +147,18 @@ def _apply(self, value):
context = {
'start': filtered['start'],
- 'end': filtered['end'],
+ 'stop': filtered['stop'],
},
)
- elif (filtered['end'] - filtered['start']) > self.MAX_INTERVAL:
- filtered['end'] = self._invalid_value(
- value = filtered['end'],
+ elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL:
+ filtered['stop'] = self._invalid_value(
+ value = filtered['stop'],
reason = self.CODE_INTERVAL_TOO_BIG,
- sub_key = 'end',
+ sub_key = 'stop',
context = {
'start': filtered['start'],
- 'end': filtered['end'],
+ 'stop': filtered['stop'],
},
template_vars = {
diff --git a/src/iota/commands/extended/get_new_addresses.py b/src/iota/commands/extended/get_new_addresses.py
index 3a1b712e..75192ecf 100644
--- a/src/iota/commands/extended/get_new_addresses.py
+++ b/src/iota/commands/extended/get_new_addresses.py
@@ -2,6 +2,8 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
+from typing import Optional
+
import filters as f
from iota.commands import FilterCommand, RequestFilter
@@ -30,12 +32,9 @@ def get_response_filter(self):
pass
def _execute(self, request):
- # Optional parameters.
- count = request.get('count')
- index = request.get('index')
-
- # Required parameters.
- seed = request['seed']
+ count = request['count'] # type: Optional[int]
+ index = request['index'] # type: int
+ seed = request['seed'] # type: Seed
generator = AddressGenerator(seed)
@@ -57,7 +56,7 @@ def __init__(self):
{
# ``count`` and ``index`` are optional.
'count': f.Type(int) | f.Min(1),
- 'index': f.Type(int) | f.Min(0),
+ 'index': f.Type(int) | f.Min(0) | f.Optional(default=0),
'seed': f.Required | Trytes(result_type=Seed),
},
diff --git a/src/iota/commands/extended/get_transfers.py b/src/iota/commands/extended/get_transfers.py
index e388b3d0..4e1b9f0d 100644
--- a/src/iota/commands/extended/get_transfers.py
+++ b/src/iota/commands/extended/get_transfers.py
@@ -71,40 +71,49 @@ def _execute(self, request):
if hashes:
# Sort transactions into tail and non-tail.
- tails = set()
- non_tails = set()
+ tail_transaction_hashes = set()
+ non_tail_bundle_hashes = set()
gt_response = GetTrytesCommand(self.adapter)(hashes=hashes)
- transactions = list(map(
+ all_transactions = list(map(
Transaction.from_tryte_string,
gt_response['trytes'],
- ))
+ )) # type: List[Transaction]
- for txn in transactions:
+ for txn in all_transactions:
if txn.is_tail:
- tails.add(txn.hash)
+ tail_transaction_hashes.add(txn.hash)
else:
# Capture the bundle ID instead of the transaction hash so that
# we can query the node to find the tail transaction for that
# bundle.
- non_tails.add(txn.bundle_hash)
+ non_tail_bundle_hashes.add(txn.bundle_hash)
- if non_tails:
- for txn in self._find_transactions(bundles=non_tails):
+ if non_tail_bundle_hashes:
+ for txn in self._find_transactions(bundles=list(non_tail_bundle_hashes)):
if txn.is_tail:
- tails.add(txn.hash)
+ if txn.hash not in tail_transaction_hashes:
+ all_transactions.append(txn)
+ tail_transaction_hashes.add(txn.hash)
+
+ # Filter out all non-tail transactions.
+ tail_transactions = [
+ txn
+ for txn in all_transactions
+ if txn.hash in tail_transaction_hashes
+ ]
# Attach inclusion states, if requested.
if inclusion_states:
gli_response = GetLatestInclusionCommand(self.adapter)(
- hashes = list(tails),
+ hashes = list(tail_transaction_hashes),
)
- for txn in transactions:
+ for txn in tail_transactions:
txn.is_confirmed = gli_response['states'].get(txn.hash)
# Find the bundles for each transaction.
- for txn in transactions:
+ for txn in tail_transactions:
gb_response = GetBundlesCommand(self.adapter)(transaction=txn.hash)
txn_bundles = gb_response['bundles'] # type: List[Bundle]
diff --git a/src/iota/commands/extended/prepare_transfer.py b/src/iota/commands/extended/prepare_transfer.py
index 481dabde..38253857 100644
--- a/src/iota/commands/extended/prepare_transfer.py
+++ b/src/iota/commands/extended/prepare_transfer.py
@@ -54,10 +54,7 @@ def _execute(self, request):
threshold = want_to_spend,
)
- confirmed_inputs = [
- input_['address']
- for input_ in gi_response['inputs']
- ]
+ confirmed_inputs = gi_response['inputs']
else:
# Inputs provided. Check to make sure we have sufficient
# balance.
diff --git a/src/iota/commands/extended/send_transfer.py b/src/iota/commands/extended/send_transfer.py
index 48ceed17..4eecab28 100644
--- a/src/iota/commands/extended/send_transfer.py
+++ b/src/iota/commands/extended/send_transfer.py
@@ -34,7 +34,7 @@ def get_response_filter(self):
def _execute(self, request):
change_address = request['changeAddress'] # type: Optional[Address]
depth = request['depth'] # type: int
- inputs = request['inputs'] or [] # type: List[Address]
+ inputs = request['inputs'] # type: Optional[List[Address]]
min_weight_magnitude = request['minWeightMagnitude'] # type: int
seed = request['seed'] # type: Seed
transfers = request['transfers'] # type: List[ProposedTransaction]
diff --git a/src/iota/transaction.py b/src/iota/transaction.py
index 9ef02296..f67a4035 100644
--- a/src/iota/transaction.py
+++ b/src/iota/transaction.py
@@ -8,8 +8,8 @@
from typing import Generator, Iterable, Iterator, List, MutableSequence, \
Optional, Sequence, Text, Tuple
-from iota import Address, Hash, Tag, TrytesCompatible, TryteString, \
- int_from_trits, trits_from_int
+from iota import Address, Hash, Tag, TryteString, TrytesCompatible, \
+ TrytesDecodeError, int_from_trits, trits_from_int
from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH
from iota.crypto.addresses import AddressGenerator
from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator, \
@@ -125,7 +125,7 @@ def from_tryte_string(cls, trytes):
return cls(
hash_ = TransactionHash.from_trits(hash_),
- signature_message_fragment = tryte_string[0:2187],
+ signature_message_fragment = Fragment(tryte_string[0:2187]),
address = Address(tryte_string[2187:2268]),
value = int_from_trits(tryte_string[2268:2295].as_trits()),
tag = Tag(tryte_string[2295:2322]),
@@ -523,6 +523,58 @@ def tail_transaction(self):
"""
return self[0]
+ def get_messages(self, errors='drop'):
+ # type: () -> List[Text]
+ """
+ Attempts to decipher encoded messages from the transactions in the
+ bundle.
+
+ :param errors:
+ How to handle trytes that can't be converted, or bytes that can't
+ be decoded using UTF-8:
+ - 'drop': drop the trytes from the result.
+ - 'strict': raise an exception.
+ - 'replace': replace with a placeholder character.
+ - 'ignore': omit the invalid tryte/byte sequence.
+ """
+ decode_errors = 'strict' if errors == 'drop' else errors
+
+ messages = []
+
+ i = 0
+ while i < len(self):
+ txn = self[i]
+
+ # Ignore inputs. Note that inputs are split into multiple
+ # transactions due to how big the signatures are.
+ if txn.value < 0:
+ i += AddressGenerator.DIGEST_ITERATIONS
+ continue
+
+ message_trytes = TryteString(txn.signature_message_fragment)
+
+ # If the message is long enough, it has to be split across
+ # multiple transactions.
+ for j in range(i+1, len(self)):
+ aux_txn = self[j]
+ if (aux_txn.address == txn.address) and (aux_txn.value == 0):
+ message_trytes += aux_txn.signature_message_fragment
+ i += 1
+ else:
+ break
+
+ # Ignore empty messages.
+ if message_trytes:
+ try:
+ messages.append(message_trytes.as_string(decode_errors))
+ except (TrytesDecodeError, UnicodeDecodeError):
+ if errors != 'drop':
+ raise
+
+ i += 1
+
+ return messages
+
def as_tryte_strings(self, head_to_tail=True):
# type: (bool) -> List[TransactionTrytes]
"""
diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py
index 3d06fa43..709bf146 100644
--- a/test/commands/extended/get_bundles_test.py
+++ b/test/commands/extended/get_bundles_test.py
@@ -6,7 +6,8 @@
import filters as f
from filters.test import BaseFilterTestCase
-from iota import Iota, TransactionHash
+from iota import Address, BadApiResponse, Bundle, BundleHash, Fragment, Hash, \
+ Iota, Tag, Transaction, TransactionHash, TransactionTrytes
from iota.adapter import MockAdapter
from iota.commands.extended.get_bundles import GetBundlesCommand
from iota.filters import Trytes
@@ -116,11 +117,13 @@ def test_fail_transaction_not_trytes(self):
)
+# noinspection SpellCheckingInspection
class GetBundlesCommandTestCase(TestCase):
def setUp(self):
super(GetBundlesCommandTestCase, self).setUp()
self.adapter = MockAdapter()
+ self.command = GetBundlesCommand(self.adapter)
def test_wireup(self):
"""
@@ -131,4 +134,303 @@ def test_wireup(self):
GetBundlesCommand,
)
- # :todo: Unit tests.
+ def test_single_transaction(self):
+ """
+ Getting a bundle that contains a single transaction.
+ """
+ transaction =\
+ Transaction(
+ current_index = 0,
+ last_index = 0,
+ tag = Tag(b''),
+ timestamp = 1484960990,
+ value = 0,
+
+ # These values are not relevant for 0-value transactions.
+ nonce = Hash(b''),
+ signature_message_fragment = Fragment(b''),
+
+ # This value is computed automatically, so it has to be real.
+ hash_ =
+ TransactionHash(
+ b'TAOICZV9ZSXIZINMNRLOLCWNLL9IDKGVWTJITNGU'
+ b'HAIKLHZLBZWOQ9HJSODUDISTYGIYPWTYDCFMVRBQN'
+ ),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999OCSGVF'
+ b'IBQA99KGTCPCZ9NHR9VGLGADDDIEGGPCGBDEDDTBC'
+ ),
+
+ bundle_hash =
+ BundleHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999DIOAZD'
+ b'M9AIUHXGVGBC9EMGI9SBVBAIXCBFJ9EELCPDRAD9U'
+ ),
+
+ branch_transaction_hash =
+ TransactionHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999BBCEDI'
+ b'ZHUDWBYDJEXHHAKDOCKEKDFIMB9AMCLFW9NBDEOFV'
+ ),
+
+ trunk_transaction_hash =
+ TransactionHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION999999ARAYA'
+ b'MHCB9DCFEIWEWDLBCDN9LCCBQBKGDDAECFIAAGDAS'
+ ),
+ )
+
+ self.adapter.seed_response('getTrytes', {
+ 'trytes': [transaction.as_tryte_string()],
+ })
+
+ response = self.command(transaction=transaction.hash)
+
+ bundle = response['bundles'][0] # type: Bundle
+ self.assertEqual(len(bundle), 1)
+
+ self.maxDiff = None
+ self.assertDictEqual(
+ bundle[0].as_json_compatible(),
+ transaction.as_json_compatible(),
+ )
+
+ def test_multiple_transactions(self):
+ """
+ Getting a bundle that contains multiple transactions.
+ """
+ bundle = Bundle.from_tryte_strings([
+ TransactionTrytes(
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P'
+ b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999999999999999999'
+ b'999999999999HNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX'
+ b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ'
+ b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9'
+ b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L'
+ b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW'
+ b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR'
+ ),
+
+ # Well, it was bound to happen sooner or later... the ASCII
+ # representation of this tryte sequence contains a very naughty
+ # phrase. But I don't feel like doing another POW, so... enjoy.
+ TransactionTrytes(
+ b'NBTCPCFDEACCPCBDVC9DTCQAJ9RBTC9D9DCDQAEAKDCDFD9DSCFAJ9VBCDJDTCQAJ9'
+ b'ZBMDYBCCKB99999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999HNLFMVD99999999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX'
+ b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXFSEWUNJOEGNU'
+ b'I9QOCRFMYSIFAZLJHKZBPQZZYFG9ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9'
+ b'999BGUEHHGAIWWQBCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJL'
+ b'EDAMYVRGABAWBY9999SYRABNN9JD9PNDLIKUNCECUELTOQZPSBDILVHJQVCEOICFAD'
+ b'YKZVGMOAXJRQNTCKMHGTAUMPGJJMX9LNF'
+ ),
+ ])
+
+ for txn in bundle:
+ self.adapter.seed_response('getTrytes', {
+ 'trytes': [txn.as_tryte_string()],
+ })
+
+ self.adapter.seed_response('getTrytes', {
+ 'trytes': [
+ 'SPAMSPAMSPAM999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999999999999999999'
+ '999999999999999999999999999999999999999999999999999JECDITWO9999999'
+ '999999999999ONLFMVD99999999999999999999VVCHSQSRVFKSBONDWB9EAQEMQOY'
+ 'YRBIZHTBJLYNAVDHZPUZAZ9LYHXWKBEJ9IPR9FAMFLT9EEOHVYWUPRHHSRCILCLWFD'
+ 'GBYBFFOKMCSAPVD9VGZZRRGBLGMZMXD9RMZQDBLMGN9BATWZGULRBCYQEIKIRBPHC9'
+ '999KTLTRSYOWBD9HVNP9GCUABARNGMYXUZKXWRPGOPETZLKYYC9Z9EYXIWVARUBMBM'
+ 'BPXGORN9WPBLY99999ZRBVQWULRFXDNDYZKRKIXPZQT9JJJH9FZU9PVWZJWLXBPODP'
+ 'EHMKTTAGEPLPHUQCZNLDSHERONOMHJCOI'
+ ],
+ })
+
+ response = self.command(
+ transaction =
+ TransactionHash(
+ b'TOYJPHKMLQNDVLDHDILARUJCCIUMQBLUSWPCTIVA'
+ b'DRXICGYDGSVPXFTILFFGAPICYHGGJ9OHXINFX9999'
+ ),
+ )
+
+ self.maxDiff = None
+ self.assertListEqual(
+ response['bundles'][0].as_json_compatible(),
+ bundle.as_json_compatible(),
+ )
+
+ def test_non_tail_transaction(self):
+ """
+ Trying to get a bundle for a non-tail transaction.
+
+ This is not valid; you have to start with a tail transaction.
+ """
+ self.adapter.seed_response('getTrytes', {
+ 'trytes': [
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999999999999999999999999999999999999999999999999999999999999'
+ b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P'
+ b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999999999999999999'
+ b'999999999999HNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX'
+ b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ'
+ b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9'
+ b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L'
+ b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW'
+ b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR'
+ ],
+ })
+
+ with self.assertRaises(BadApiResponse):
+ self.command(
+ transaction =
+ TransactionHash(
+ b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9'
+ b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999'
+ ),
+ )
+
+ def test_missing_transaction(self):
+ """
+ Unable to find the requested transaction.
+ """
+ self.adapter.seed_response('getTrytes', {'trytes': []})
+
+ with self.assertRaises(BadApiResponse):
+ self.command(
+ transaction =
+ TransactionHash(
+ b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9'
+ b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999'
+ ),
+ )
diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py
index 41a17619..27c0a58c 100644
--- a/test/commands/extended/get_inputs_test.py
+++ b/test/commands/extended/get_inputs_test.py
@@ -6,13 +6,15 @@
import filters as f
from filters.test import BaseFilterTestCase
-from iota import Iota
+from mock import Mock, patch
+from six import binary_type, text_type
+
+from iota import Address, BadApiResponse, Iota, TransactionHash
from iota.adapter import MockAdapter
from iota.commands.extended.get_inputs import GetInputsCommand, \
GetInputsRequestFilter
from iota.crypto.types import Seed
from iota.filters import Trytes
-from six import binary_type, text_type
class GetInputsRequestFilterTestCase(BaseFilterTestCase):
@@ -33,7 +35,7 @@ def test_pass_happy_path(self):
request = {
'seed': Seed(self.seed),
'start': 0,
- 'end': 10,
+ 'stop': 10,
'threshold': 100,
}
@@ -54,7 +56,7 @@ def test_pass_compatible_types(self):
# These values must still be integers, however.
'start': 42,
- 'end': 86,
+ 'stop': 86,
'threshold': 99,
})
@@ -65,7 +67,7 @@ def test_pass_compatible_types(self):
{
'seed': Seed(self.seed),
'start': 42,
- 'end': 86,
+ 'stop': 86,
'threshold': 99,
},
)
@@ -85,7 +87,7 @@ def test_pass_optional_parameters_excluded(self):
{
'seed': Seed(self.seed),
'start': 0,
- 'end': None,
+ 'stop': None,
'threshold': None,
}
)
@@ -212,65 +214,65 @@ def test_fail_start_too_small(self):
},
)
- def test_fail_end_string(self):
+ def test_fail_stop_string(self):
"""
- ``end`` is a string.
+ ``stop`` is a string.
"""
self.assertFilterErrors(
{
# Not valid; it must be an int.
- 'end': '0',
+ 'stop': '0',
'seed': Seed(self.seed),
},
{
- 'end': [f.Type.CODE_WRONG_TYPE],
+ 'stop': [f.Type.CODE_WRONG_TYPE],
},
)
- def test_fail_end_float(self):
+ def test_fail_stop_float(self):
"""
- ``end`` is a float.
+ ``stop`` is a float.
"""
self.assertFilterErrors(
{
# Even with an empty fpart, floats are not valid.
# It's gotta be an int.
- 'end': 8.0,
+ 'stop': 8.0,
'seed': Seed(self.seed),
},
{
- 'end': [f.Type.CODE_WRONG_TYPE],
+ 'stop': [f.Type.CODE_WRONG_TYPE],
},
)
- def test_fail_end_too_small(self):
+ def test_fail_stop_too_small(self):
"""
- ``end`` is less than 0.
+ ``stop`` is less than 0.
"""
self.assertFilterErrors(
{
- 'end': -1,
+ 'stop': -1,
'seed': Seed(self.seed),
},
{
- 'end': [f.Min.CODE_TOO_SMALL],
+ 'stop': [f.Min.CODE_TOO_SMALL],
},
)
- def test_fail_end_occurs_before_start(self):
+ def test_fail_stop_occurs_before_start(self):
"""
- ``end`` is less than ``start``.
+ ``stop`` is less than ``start``.
"""
self.assertFilterErrors(
{
'start': 1,
- 'end': 0,
+ 'stop': 0,
'seed': Seed(self.seed),
},
@@ -282,18 +284,18 @@ def test_fail_end_occurs_before_start(self):
def test_fail_interval_too_large(self):
"""
- ``end`` is way more than ``start``.
+ ``stop`` is way more than ``start``.
"""
self.assertFilterErrors(
{
'start': 0,
- 'end': GetInputsRequestFilter.MAX_INTERVAL + 1,
+ 'stop': GetInputsRequestFilter.MAX_INTERVAL + 1,
'seed': Seed(self.seed),
},
{
- 'end': [GetInputsRequestFilter.CODE_INTERVAL_TOO_BIG],
+ 'stop': [GetInputsRequestFilter.CODE_INTERVAL_TOO_BIG],
},
)
@@ -350,12 +352,42 @@ def test_fail_threshold_too_small(self):
class GetInputsCommandTestCase(TestCase):
+ # noinspection SpellCheckingInspection
def setUp(self):
super(GetInputsCommandTestCase, self).setUp()
self.adapter = MockAdapter()
self.command = GetInputsCommand(self.adapter)
+ # Define some valid tryte sequences that we can reuse between
+ # tests.
+ self.addy0 =\
+ Address(
+ trytes =
+ b'TESTVALUE9DONTUSEINPRODUCTION99999FIODSG'
+ b'IC9CCIFCNBTBDFIEHHE9RBAEVGK9JECCLCPBIINAX',
+
+ key_index = 0,
+ )
+
+ self.addy1 =\
+ Address(
+ trytes =
+ b'TESTVALUE9DONTUSEINPRODUCTION999999EPCNH'
+ b'MBTEH9KDVFMHHESDOBTFFACCGBFGACEDCDDCGICIL',
+
+ key_index = 1,
+ )
+
+ self.addy2 =\
+ Address(
+ trytes =
+ b'TESTVALUE9DONTUSEINPRODUCTION99999YDOHWF'
+ b'U9PFOFHGKFACCCBGDALGI9ZBEBABFAMBPDSEQ9XHJ',
+
+ key_index = 2,
+ )
+
def test_wireup(self):
"""
Verify that the command is wired up correctly.
@@ -365,30 +397,423 @@ def test_wireup(self):
GetInputsCommand,
)
- def test_start_and_end_with_threshold(self):
+ def test_stop_threshold_met(self):
+ """
+ ``stop`` provided, balance meets ``threshold``.
+ """
+ self.adapter.seed_response('getBalances', {
+ 'balances': [42, 29],
+ })
+
+ # 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(
+ 'iota.crypto.addresses.AddressGenerator.get_addresses',
+ mock_address_generator,
+ ):
+ response = self.command(
+ seed = Seed.random(),
+ stop = 2,
+ threshold = 71,
+ )
+
+ self.assertEqual(response['totalBalance'], 71)
+ self.assertEqual(len(response['inputs']), 2)
+
+ input0 = response['inputs'][0]
+ self.assertIsInstance(input0, Address)
+
+ self.assertEqual(input0, self.addy0)
+ self.assertEqual(input0.balance, 42)
+ self.assertEqual(input0.key_index, 0)
+
+ input1 = response['inputs'][1]
+ self.assertIsInstance(input1, Address)
+
+ self.assertEqual(input1, self.addy1)
+ self.assertEqual(input1.balance, 29)
+ self.assertEqual(input1.key_index, 1)
+
+ def test_stop_threshold_not_met(self):
"""
- ``start`` and ``end`` values provided, with ``threshold``.
+ ``stop`` provided, balance does not meet ``threshold``.
"""
- # :todo: Implement test.
- self.skipTest('Not implemented yet.')
+ self.adapter.seed_response('getBalances', {
+ 'balances': [42, 29],
+ })
+
+ # 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(
+ 'iota.crypto.addresses.AddressGenerator.get_addresses',
+ mock_address_generator,
+ ):
+ with self.assertRaises(BadApiResponse):
+ self.command(
+ seed = Seed.random(),
+ stop = 2,
+ 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(
+ 'iota.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)
- def test_start_and_end_no_threshold(self):
+ # 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_stop_no_threshold(self):
"""
- ``start`` and ``end`` values provided, no ``threshold``.
+ ``stop`` provided, no ``threshold``.
"""
- # :todo: Implement test.
- self.skipTest('Not implemented yet.')
+ self.adapter.seed_response('getBalances', {
+ 'balances': [42, 29],
+ })
+
+ # 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(
+ 'iota.crypto.addresses.AddressGenerator.get_addresses',
+ mock_address_generator,
+ ):
+ response = self.command(
+ seed = Seed.random(),
+ start = 0,
+ stop = 2,
+ )
+
+ self.assertEqual(response['totalBalance'], 71)
+ self.assertEqual(len(response['inputs']), 2)
+
+ input0 = response['inputs'][0]
+ self.assertIsInstance(input0, Address)
+
+ self.assertEqual(input0, self.addy0)
+ self.assertEqual(input0.balance, 42)
+ self.assertEqual(input0.key_index, 0)
+
+ input1 = response['inputs'][1]
+ self.assertIsInstance(input1, Address)
+
+ self.assertEqual(input1, self.addy1)
+ self.assertEqual(input1.balance, 29)
+ self.assertEqual(input1.key_index, 1)
+
+ def test_no_stop_threshold_met(self):
+ """
+ No ``stop`` provided, balance meets ``threshold``.
+ """
+ self.adapter.seed_response('getBalances', {
+ 'balances': [42, 29],
+ })
+
+ # ``getInputs`` uses ``findTransactions`` to identify unused
+ # addresses.
+ # noinspection SpellCheckingInspection
+ self.adapter.seed_response('findTransactions', {
+ 'hashes': [
+ TransactionHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999WBL9KD'
+ b'EIZDMEDFPEYDIIA9LEMEUCC9MFPBY9TEVCUGSEGGN'
+ ),
+ ],
+ })
+
+ # 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):
+ 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(
+ 'iota.crypto.addresses.AddressGenerator.create_generator',
+ mock_address_generator,
+ ):
+ response = self.command(
+ seed = Seed.random(),
+ threshold = 71,
+ )
+
+ self.assertEqual(response['totalBalance'], 71)
+ self.assertEqual(len(response['inputs']), 2)
+
+ input0 = response['inputs'][0]
+ self.assertIsInstance(input0, Address)
+
+ self.assertEqual(input0, self.addy0)
+ self.assertEqual(input0.balance, 42)
+ self.assertEqual(input0.key_index, 0)
+
+ input1 = response['inputs'][1]
+ self.assertIsInstance(input1, Address)
- def test_no_end_with_threshold(self):
+ self.assertEqual(input1, self.addy1)
+ self.assertEqual(input1.balance, 29)
+ self.assertEqual(input1.key_index, 1)
+
+ def test_no_stop_threshold_not_met(self):
"""
- No ``end`` value provided, with ``threshold``.
+ No ``stop`` provided, balance does not meet ``threshold``.
"""
- # :todo: Implement test.
- self.skipTest('Not implemented yet.')
+ 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(
+ 'iota.crypto.addresses.AddressGenerator.create_generator',
+ mock_address_generator,
+ ):
+ with self.assertRaises(BadApiResponse):
+ self.command(
+ seed = Seed.random(),
+ threshold = 72,
+ )
+
+ def test_no_stop_threshold_zero(self):
+ """
+ No ``stop`` provided, ``threshold`` is 0.
+ """
+ # Note that the first address has a zero balance.
+ self.adapter.seed_response('getBalances', {
+ 'balances': [0, 1],
+ })
+
+ # ``getInputs`` uses ``findTransactions`` to identify unused
+ # addresses.
+ # noinspection SpellCheckingInspection
+ self.adapter.seed_response('findTransactions', {
+ 'hashes': [
+ TransactionHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999WBL9KD'
+ b'EIZDMEDFPEYDIIA9LEMEUCC9MFPBY9TEVCUGSEGGN'
+ ),
+ ],
+ })
+
+ # noinspection SpellCheckingInspection
+ self.adapter.seed_response('findTransactions', {
+ 'hashes': [
+ TransactionHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD'
+ b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H'
+ ),
+ ],
+ })
+
+ self.adapter.seed_response('findTransactions', {
+ 'hashes': [],
+ })
- def test_no_end_no_threshold(self):
+ # 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(
+ 'iota.crypto.addresses.AddressGenerator.create_generator',
+ mock_address_generator,
+ ):
+ response = self.command(
+ seed = Seed.random(),
+ threshold = 0,
+ )
+
+ self.assertEqual(response['totalBalance'], 1)
+ self.assertEqual(len(response['inputs']), 1)
+
+ # Because the first address had a zero balance, it was skipped.
+ 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_no_stop_no_threshold(self):
+ """
+ No ``stop`` provided, no ``threshold``.
+ """
+ self.adapter.seed_response('getBalances', {
+ 'balances': [42, 29],
+ })
+
+ # ``getInputs`` uses ``findTransactions`` to identify unused
+ # addresses.
+ # noinspection SpellCheckingInspection
+ self.adapter.seed_response('findTransactions', {
+ 'hashes': [
+ TransactionHash(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999WBL9KD'
+ b'EIZDMEDFPEYDIIA9LEMEUCC9MFPBY9TEVCUGSEGGN'
+ ),
+ ],
+ })
+
+ # 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):
+ 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(
+ 'iota.crypto.addresses.AddressGenerator.create_generator',
+ mock_address_generator,
+ ):
+ response = self.command(
+ seed = Seed.random(),
+ )
+
+ self.assertEqual(response['totalBalance'], 71)
+ self.assertEqual(len(response['inputs']), 2)
+
+ input0 = response['inputs'][0]
+ self.assertIsInstance(input0, Address)
+
+ self.assertEqual(input0, self.addy0)
+ self.assertEqual(input0.balance, 42)
+ self.assertEqual(input0.key_index, 0)
+
+ input1 = response['inputs'][1]
+ self.assertIsInstance(input1, Address)
+
+ self.assertEqual(input1, self.addy1)
+ self.assertEqual(input1.balance, 29)
+ self.assertEqual(input1.key_index, 1)
+
+ def test_start(self):
"""
- No ``end`` value provided, no ``threshold``.
+ Using ``start`` to offset the key range.
"""
- # :todo: Implement test.
- self.skipTest('Not implemented yet.')
+ 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(
+ 'iota.crypto.addresses.AddressGenerator.create_generator',
+ 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)
diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py
index b7d30ae8..e1131f0f 100644
--- a/test/commands/extended/get_new_addresses_test.py
+++ b/test/commands/extended/get_new_addresses_test.py
@@ -55,7 +55,7 @@ def test_pass_optional_parameters_excluded(self):
{
'seed': Seed(self.seed),
- 'index': None,
+ 'index': 0,
'count': None,
},
)
diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py
index de467393..b01b3cf9 100644
--- a/test/commands/extended/prepare_transfer_test.py
+++ b/test/commands/extended/prepare_transfer_test.py
@@ -1372,35 +1372,23 @@ def test_pass_inputs_implicit_no_change(self):
# - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand`
mock_get_inputs = Mock(return_value={
'inputs': [
- {
- 'address':
- Address(
- trytes =
- b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N'
- b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW',
-
- balance = 13,
- key_index = 4,
- ),
-
- 'balance': 13,
- 'keyIndex': 4,
- },
+ Address(
+ trytes =
+ b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N'
+ b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW',
- {
- 'address':
- Address(
- trytes =
- b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ'
- b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY',
+ balance = 13,
+ key_index = 4,
+ ),
- balance = 29,
- key_index = 5,
- ),
+ Address(
+ trytes =
+ b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ'
+ b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY',
- 'balance': 29,
- 'keyIndex': 5,
- },
+ balance = 29,
+ key_index = 5,
+ ),
],
'totalBalance': 42,
@@ -1832,20 +1820,14 @@ def test_pass_inputs_implicit_with_change(self):
# - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand`
mock_get_inputs = Mock(return_value={
'inputs': [
- {
- 'address':
- Address(
- trytes =
- b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N'
- b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW',
+ Address(
+ trytes =
+ b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N'
+ b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW',
- balance = 86,
- key_index = 4,
- ),
-
- 'balance': 86,
- 'keyIndex': 4,
- },
+ balance = 86,
+ key_index = 4,
+ ),
],
'totalBalance': 86,
diff --git a/test/transaction_test.py b/test/transaction_test.py
index 8238672f..f09567b0 100644
--- a/test/transaction_test.py
+++ b/test/transaction_test.py
@@ -6,6 +6,7 @@
from unittest import TestCase
from mock import patch
+from six import binary_type
from iota import Address, Bundle, BundleHash, Fragment, Hash, ProposedBundle, \
ProposedTransaction, Tag, Transaction, TransactionHash, TransactionTrytes, \
@@ -13,7 +14,350 @@
from iota.crypto.addresses import AddressGenerator
from iota.crypto.signing import KeyGenerator
from iota.transaction import BundleValidator
-from six import binary_type
+
+
+class BundleTestCase(TestCase):
+ # noinspection SpellCheckingInspection
+ def setUp(self):
+ super(BundleTestCase, self).setUp()
+
+ self.bundle = Bundle([
+ # This transaction does not have a message.
+ Transaction(
+ signature_message_fragment = Fragment(b''),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999A9PG9A'
+ b'XCQANAWGJBTFWEAEQCN9WBZB9BJAIIY9UDLIGFOAA'
+ ),
+
+ current_index = 0,
+ last_index = 7,
+ value = 0,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ # This transaction has something that can't be decoded as a UTF-8
+ # sequence.
+ Transaction(
+ signature_message_fragment =
+ Fragment(b'OHCFVELH9GYEMHCF9GPHBGIEWHZFU'),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999HAA9UA'
+ b'MHCGKEUGYFUBIARAXBFASGLCHCBEVGTBDCSAEBTBM'
+ ),
+
+ current_index = 1,
+ last_index = 7,
+ value = 10,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ # This transaction has a message that fits into a single
+ # fragment.
+ Transaction(
+ signature_message_fragment =
+ Fragment.from_string('Hello, world!'),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999D99HEA'
+ b'M9XADCPFJDFANCIHR9OBDHTAGGE9TGCI9EO9ZCRBN'
+ ),
+
+ current_index = 2,
+ last_index = 7,
+ value = 20,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ # This transaction has a message that spans multiple fragments.
+ Transaction(
+ signature_message_fragment =
+ Fragment(
+ b'J9GAQBCDCDSCEAADCDFDBDXCBDVCQAGAEAGDPCXCSCEANBTCTCDDEACCWCCDIDVC'
+ b'WCHDEAPCHDEA9DPCGDHDSAJ9GAOBFDSASASAEAQBCDCDSCEAADCDFDBDXCBDVCQA'
+ b'EAYBEANBTCTCDDEACCWCCDIDVCWCHDQAGAEAGDPCXCSCEAVBCDCDBDEDIDPCKD9D'
+ b'EABDTCFDJDCDIDGD9DMDSAJ9EAEAGANBCDEAMDCDIDEAWCPCJDTCSASASAEATCFD'
+ b'QAEAHDWCPCHDEAXCGDSASASAGAJ9GASASASAEAPCBDEAPCBDGDKDTCFDEAUCCDFD'
+ b'EAMDCDIDIBGAEAXCBDHDTCFDFDIDDDHDTCSCEANBTCTCDDEACCWCCDIDVCWCHDEA'
+ b'ADPCYCTCGDHDXCRCPC9D9DMDSAEAGAHCTCGDSAEASBEAWCPCJDTCSAGAJ9CCWCTC'
+ b'EAHDKDCDEAADTCBDEAGDWCXCJDTCFDTCSCEAKDXCHDWCEATCLDDDTCRCHDPCBDRC'
+ b'MDSAEACCWCTCXCFDEAKDPCXCHDXCBDVCEAWCPCSCEABDCDHDEAQCTCTCBDEAXCBD'
+ b'EAJDPCXCBDSAJ9GACCWCTCFDTCEAFDTCPC9D9DMDEAXCGDEACDBDTCIBGAEAQCFD'
+ b'TCPCHDWCTCSCEAZBWCCDIDRCWCVCSAJ9GACCWCTCFDTCEAFDTCPC9D9DMDEAXCGD'
+ b'EACDBDTCQAGAEARCCDBDUCXCFDADTCSCEANBTCTCDDEACCWCCDIDVCWCHDSAJ9GA'
+ b'CCCDEAOBJDTCFDMDHDWCXCBDVCIBEACCCDEAHDWCTCEAVCFDTCPCHDEA9CIDTCGD'
+ b'HDXCCDBDEACDUCEAVBXCUCTCQAEAHDWCTCEADCBDXCJDTCFDGDTCEAPCBDSCEAOB'
+ b'JDTCFDMDHDWCXCBDVCIBGAJ9GAHCTCGDSAGAJ9LBCDHDWCEACDUCEAHDWCTCEAAD'
+ b'TCBDEAWCPCSCEAQCTCTCBDEAHDFDPCXCBDTCSCEAUCCDFDEAHDWCXCGDEAADCDAD'
+ b'TCBDHDEBEAHDWCTCXCFDEA9DXCJDTCGDEAWCPCSCEAQCTCTCBDEAPCJ9EAEADDFD'
+ b'TCDDPCFDPCHDXCCDBDEAUCCDFDEAXCHDEBEAHDWCTCMDEAWCPCSCEAQCTCTCBDEA'
+ b'GDTC9DTCRCHDTCSCEAPCHDEAQCXCFDHDWCEAPCGDEAHDWCCDGDTCEAKDWCCDEAKD'
+ b'CDID9DSCJ9EAEAKDXCHDBDTCGDGDEAHDWCTCEAPCBDGDKDTCFDEBEAQCIDHDEATC'
+ b'JDTCBDEAGDCDEAHDWCTCMDEAUCCDIDBDSCEAHDWCTCADGDTC9DJDTCGDEAVCPCGD'
+ b'DDXCBDVCEAPCBDSCEAGDEDIDXCFDADXCBDVCJ9EAEA9DXCZCTCEATCLDRCXCHDTC'
+ b'SCEARCWCXC9DSCFDTCBDSAJ9GAKBBDSCEAMDCDIDLAFDTCEAFDTCPCSCMDEAHDCD'
+ b'EAVCXCJDTCEAXCHDEAHDCDEAIDGDIBGAEAIDFDVCTCSCEAVBCDCDBDEDIDPCKD9D'
+ b'SAJ9GASBEAPCADSAGAJ9GAXBCDKDIBGAJ9GAXBCDKDQAGAEAGDPCXCSCEANBTCTC'
+ b'DDEACCWCCDIDVCWCHDSAJ9CCWCTCMDEAQCCDHDWCEA9DXCRCZCTCSCEAHDWCTCXC'
+ b'FDEASCFDMDEA9DXCDDGDSAJ9GACCWCCDIDVCWCEASBEASCCDBDLAHDEAHDWCXCBD'
+ b'ZCQAGAEAPCSCSCTCSCEANBTCTCDDEACCWCCDIDVCWCHDQAEAGAHDWCPCHDEAMDCD'
+ b'IDLAFDTCEAVCCDXCBDVCEAHDCDEA9DXCZCTCEAXCHDSAGAJ9GANBCDTCGDBDLAHD'
+ b'EAADPCHDHDTCFDQAGAEAGDPCXCSCEAZBWCCDIDRCWCVCSAEAGAFCTCEAADIDGDHD'
+ b'EAZCBDCDKDEAXCHDFAEAXBCDKDFAGAJ9GAXBCDKDIBGAEATCBDEDIDXCFDTCSCEA'
+ b'NBTCTCDDEACCWCCDIDVCWCHDSAJ9GAHCTCGDFAEAXBCDKDFAGAJ9GAKB9D9DEAFD'
+ b'XCVCWCHDQAGAEAGDPCXCSCEAHDWCTCEARCCDADDDIDHDTCFDEAPCBDSCEAGDTCHD'
+ b'HD9DTCSCEAXCBDHDCDEAGDXC9DTCBDRCTCEAPCVCPCXCBDSAJ9EAEACCWCTCEAHD'
+ b'KDCDEAADTCB'
+ ),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999A9PG9A'
+ b'XCQANAWGJBTFWEAEQCN9WBZB9BJAIIY9UDLIGFOAA'
+ ),
+
+ current_index = 3,
+ last_index = 7,
+ value = 30,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ Transaction(
+ signature_message_fragment =
+ Fragment(
+ b'DEAUCXCSCVCTCHDTCSCSAEACCWCTCEAHDTCBDGDXCCDBDEAKDPCGDEAIDBDQCTCP'
+ b'CFDPCQC9DTCSAJ9GAHCCDIDLAFDTCEAFDTCPC9D9DMDEABDCDHDEAVCCDXCBDVCE'
+ b'AHDCDEA9DXCZCTCEAXCHDQAGAEACDQCGDTCFDJDTCSCEANBTCTCDDEACCWCCDIDV'
+ b'CWCHDSAJ9GACCTC9D9DEAIDGDFAGAJ9GAKB9D9DEAFDXCVCWCHDQAGAEAGDPCXCS'
+ b'CEANBTCTCDDEACCWCCDIDVCWCHDSAEAGACCWCTCEAKBBDGDKDTCFDEAHDCDEAHDW'
+ b'CTCEAQBFDTCPCHDEA9CIDTCGDHDXCCDBDSASASAGAJ9GAHCTCGDIBGAJ9GAYBUCE'
+ b'AVBXCUCTCQAEAHDWCTCEADCBDXCJDTCFDGDTCEAPCBDSCEAOBJDTCFDMDHDWCXCB'
+ b'DVCSASASAGAEAGDPCXCSCEANBTCTCDDEACCWCCDIDVCWCHDSAJ9GAHCTCGDIBIBG'
+ b'AJ9GASBGDSASASAGAJ9GAHCTCGDIBFAGAJ9GAPBCDFDHDMDRAHDKDCDQAGAEAGDP'
+ b'CXCSCEANBTCTCDDEACCWCCDIDVCWCHDQAEAKDXCHDWCEAXCBDUCXCBDXCHDTCEAA'
+ b'DPCYCTCGDHDMDEAPCBDSCEARCPC9DADSAJ9EAEAEAEAEAEAEAEA'
+ ),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999A9PG9A'
+ b'XCQANAWGJBTFWEAEQCN9WBZB9BJAIIY9UDLIGFOAA'
+ ),
+
+ current_index = 4,
+ last_index = 7,
+ value = 0,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ # Input, Part 1 of 2
+ Transaction(
+ # Make the signature look like a message, so we can verify that
+ # the Bundle skips it correctly.
+ signature_message_fragment =
+ Fragment.from_string('This is a signature, not a message!'),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999WGSBUA'
+ b'HDVHYHOBHGP9VCGIZHNCAAQFJGE9YHEHEFTDAGXHY'
+ ),
+
+ current_index = 5,
+ last_index = 7,
+ value = -100,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ # Input, Part 2 of 2
+ Transaction(
+ # Make the signature look like a message, so we can verify that
+ # the Bundle skips it correctly.
+ signature_message_fragment =
+ Fragment.from_string('This is a signature, not a message!'),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999WGSBUA'
+ b'HDVHYHOBHGP9VCGIZHNCAAQFJGE9YHEHEFTDAGXHY'
+ ),
+
+ current_index = 6,
+ last_index = 7,
+ value = 0,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+
+ # Change
+ Transaction(
+ # It's unusual for a change transaction to have a message, but
+ # half the fun of unit tests is designing unusual scenarios!
+ signature_message_fragment =
+ Fragment.from_string('I can haz change?'),
+
+ address =
+ Address(
+ b'TESTVALUE9DONTUSEINPRODUCTION99999FFYALH'
+ b'N9ACYCP99GZBSDK9CECFI9RAIH9BRCCAHAIAWEFAN'
+ ),
+
+ current_index = 7,
+ last_index = 7,
+ value = 40,
+
+ # These values are not relevant to the tests.
+ branch_transaction_hash = TransactionHash(b''),
+ bundle_hash = BundleHash(b''),
+ hash_ = TransactionHash(b''),
+ nonce = Hash(b''),
+ tag = Tag(b''),
+ timestamp = 1485020456,
+ trunk_transaction_hash = TransactionHash(b''),
+ ),
+ ])
+
+ def test_get_messages_errors_drop(self):
+ """
+ Decoding messages from a bundle, with ``errors='drop'``.
+ """
+ messages = self.bundle.get_messages('drop')
+
+ self.assertEqual(len(messages), 3)
+
+ self.assertEqual(messages[0], 'Hello, world!')
+
+ # noinspection SpellCheckingInspection
+ self.assertEqual(
+ messages[1],
+
+ '''
+"Good morning," said Deep Thought at last.
+"Er... Good morning, O Deep Thought," said Loonquawl nervously.
+ "Do you have... er, that is..."
+"... an answer for you?" interrupted Deep Thought majestically. "Yes. I have."
+The two men shivered with expectancy. Their waiting had not been in vain.
+"There really is one?" breathed Phouchg.
+"There really is one," confirmed Deep Thought.
+"To Everything? To the great Question of Life, the Universe and Everything?"
+"Yes."
+Both of the men had been trained for this moment; their lives had been a
+ preparation for it; they had been selected at birth as those who would
+ witness the answer; but even so they found themselves gasping and squirming
+ like excited children.
+"And you're ready to give it to us?" urged Loonquawl.
+"I am."
+"Now?"
+"Now," said Deep Thought.
+They both licked their dry lips.
+"Though I don't think," added Deep Thought, "that you're going to like it."
+"Doesn't matter," said Phouchg. "We must know it! Now!"
+"Now?" enquired Deep Thought.
+"Yes! Now!"
+"All right," said the computer and settled into silence again.
+ The two men fidgeted. The tension was unbearable.
+"You're really not going to like it," observed Deep Thought.
+"Tell us!"
+"All right," said Deep Thought. "The Answer to the Great Question..."
+"Yes?"
+"Of Life, the Universe and Everything..." said Deep Thought.
+"Yes??"
+"Is..."
+"Yes?!"
+"Forty-two," said Deep Thought, with infinite majesty and calm.
+ ''',
+ )
+
+ self.assertEqual(messages[2], 'I can haz change?')
+
+ def test_get_messages_errors_strict(self):
+ """
+ Decoding messages from a bundle, with ``errors='strict'``.
+ """
+ with self.assertRaises(UnicodeDecodeError):
+ self.bundle.get_messages('strict')
+
+ def test_get_messages_errors_ignore(self):
+ """
+ Decoding messages from a bundle, with ``errors='ignore'``.
+ """
+ messages = self.bundle.get_messages('ignore')
+
+ self.assertEqual(len(messages), 4)
+
+ # The only message that is treated differently is the invalid one.
+ self.assertEqual(messages[0], '祝你好运\x15')
+
+ def test_get_messages_errors_replace(self):
+ """
+ Decoding messages from a bundle, with ``errors='replace'``.
+ """
+ messages = self.bundle.get_messages('replace')
+
+ self.assertEqual(len(messages), 4)
+
+ # The only message that is treated differently is the invalid one.
+ self.assertEqual(messages[0], '祝你好运�\x15')
class BundleValidatorTestCase(TestCase):