From b80d36012f154c08a7326d8edfb22c6d2f60f113 Mon Sep 17 00:00:00 2001 From: Levente Pap Date: Wed, 26 Feb 2020 17:58:47 +0100 Subject: [PATCH] Add tests for get_bundles_from_transactions_hashes util method --- iota/commands/extended/utils.py | 18 +- test/commands/extended/utils_test.py | 562 ++++++++++++++++++++++++++- 2 files changed, 575 insertions(+), 5 deletions(-) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 0989850..62f962b 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -5,8 +5,9 @@ from typing import Generator, Iterable, List, Optional, Tuple from iota import Address, Bundle, Transaction, \ - TransactionHash + TransactionHash, TransactionTrytes, BadApiResponse from iota.adapter import BaseAdapter +from iota.exceptions import with_context from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.core.get_trytes import GetTrytesCommand from iota.commands.core.were_addresses_spent_from import \ @@ -79,6 +80,21 @@ async def get_bundles_from_transaction_hashes( non_tail_bundle_hashes = set() gt_response = await GetTrytesCommand(adapter)(hashes=transaction_hashes) + for tx_hash, tx_trytes in zip(transaction_hashes, gt_response['trytes']): + # If no tx was found by the node for tx_hash, it returns 9s, + # so we check here if it returned all 9s trytes. + if tx_trytes == TransactionTrytes(''): + raise with_context( + exc=BadApiResponse( + 'Could not get trytes of transaction {hash} from the Tangle. ' + '(``exc.context`` has more info).'.format(hash=tx_hash), + ), + + context={ + 'transaction_hash': tx_hash, + 'returned_transaction_trytes': tx_trytes, + }, + ) all_transactions = list(map( Transaction.from_tryte_string, gt_response['trytes'], diff --git a/test/commands/extended/utils_test.py b/test/commands/extended/utils_test.py index 480c924..229fc5a 100644 --- a/test/commands/extended/utils_test.py +++ b/test/commands/extended/utils_test.py @@ -3,10 +3,12 @@ unicode_literals from unittest import TestCase -from iota.commands.extended.utils import iter_used_addresses -from iota import MockAdapter +from iota.commands.extended.utils import iter_used_addresses, \ + get_bundles_from_transaction_hashes +from iota.adapter import MockAdapter, async_return from iota.crypto.types import Seed -from test import mock, async_test +from test import mock, async_test, MagicMock +from iota import TransactionTrytes, TransactionHash, Bundle, BadApiResponse class IterUsedAddressesTestCase(TestCase): @@ -246,4 +248,556 @@ async def test_multiple_addresses_return(self): ] ) -# TODO: add tests for `get_bundles_from_transaction_hashes` \ No newline at end of file + +class GetBundlesFromTransactionHashesTestCase(TestCase): + def setUp(self) -> None: + # Need two valid bundles + super().setUp() + self.adapter = MockAdapter() + + self.single_bundle = Bundle.from_tryte_strings([ + TransactionTrytes( + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999POALVTTGQHJFGINKJ9' + 'EWRJZBQLLWMMNMNRUT9VFWDDMDWHPJMNDOFZXUQUABGCXZRH9OI9NWEUSHVYXDO' + '999999999999999999999999999C99999999999999999999999999RIGEHBD99' + '999999999999999999RPCKQTYDOV9IYVYYALBTBLHRFCLFMTCC9ZLOKKGENTDFY' + 'COKFUITXUIUJLBNWAEKBJKBYDSRLVHSGELCCCZGNHCYEAKJ9OPRZFIBYEEBTRFT' + 'QTWJUKRDKNSEESICPJRTDNZQQYNXOFVXI9CPRNBO9APJMEXATA9999CZGNHCYEA' + 'KJ9OPRZFIBYEEBTRFTQTWJUKRDKNSEESICPJRTDNZQQYNXOFVXI9CPRNBO9APJM' + 'EXATA9999C99999999999999999999999999FQFFNIHPF999999999MMMMMMMMM' + 'BCDJOVFVODAQEPAXIWDRFKCTOFI' + ) + ]) + + self.three_tx_bundle = Bundle.from_tryte_strings(([ + TransactionTrytes( + 'PBXCFDGDHDEAHDFDPCBDGDPCRCHDXCCDBDEAXCBDEAHDWCTCEAQCIDBDSC9DTCS' + 'A99999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999M9OVNPOWKUNQYDHFN9' + 'YAL9WIQJDVAFKBU9ZPIHSGTZLGFJODRZINZMDALS9ERTNAJ9VTENWYLBSYALQQL' + '999999999999999999999999999EYOTA9TESTS9999999999999999BOPIHBD99' + '999999999B99999999JWFDGHYGEQIKSPCWEAHHQACOYHQWINSA9GELCEZNQEUHV' + 'DH9UAYJVSTIIKW9URTHHIJYGWXGE9AEWISYWZSLPKSJETGKZEQVPISQSNDHIAXQ' + 'RZVFJXFOXZAVMRUGALCQRHUEZPDFNLCIKQGWEKDJURLZLMUZVA99999BSJCSWTG' + 'RTJSGZPOXRPICUDATCLCVTF9BEDHSZZRLSH9IRMTFRVAMSSHC9TRYZGHPWRDVTX' + 'EXWTZ9999PYOTA9TESTS9999999999999999OSZRBMHPF999999999MMMMMMMMM' + 'IVL9PTSTAIRGJLGXFQGIWOJHBKF' + ), + TransactionTrytes( + 'BCTCRCCDBDSCEAHDFDPCBDGDPCRCHDXCCDBDEAXCBDEAHDWCTCEAQCIDBDSC9DT' + 'CSA999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999LSTTHILAJWQEXWVOJQ' + 'GRANRLNHQLKYXVQFBYJ9QDFRISQR9WJYMSSZUBOCVLXF9TACHKGQUEGMJPICXVY' + '999999999999999999999999999PYOTA9TESTS9999999999999999BOPIHBD99' + 'A99999999B99999999JWFDGHYGEQIKSPCWEAHHQACOYHQWINSA9GELCEZNQEUHV' + 'DH9UAYJVSTIIKW9URTHHIJYGWXGE9AEWISYWQQAWNWHDSGZWFTKTYSV99PJIFFM' + 'OPFWONAOTRBUEDGLORTHNMXM9EZNILYEIWCQIAVMAGDBHYWWOA99999BSJCSWTG' + 'RTJSGZPOXRPICUDATCLCVTF9BEDHSZZRLSH9IRMTFRVAMSSHC9TRYZGHPWRDVTX' + 'EXWTZ9999PYOTA9TESTS9999999999999999EMSRBMHPF999999999MMMMMMMMM' + 'NXTVOIJXAAJUS9SRVJEVDVOSIUE' + ), + TransactionTrytes( + 'CCWCXCFDSCEAHDFDPCBDGDPCRCHDXCCDBDEAXCBDEAHDWCTCEAQCIDBDSC9DTCS' + 'A99999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999FSXLFSGAHTGSFPK9FH' + 'HURWZJAWQDQCRIFUHMSZWUTNRAIDNGEHGPHLNJOEAIDGLYQRCYSCYDTBZQFDGQK' + '999999999999999999999999999PYOTA9TESTS9999999999999999BOPIHBD99' + 'B99999999B99999999JWFDGHYGEQIKSPCWEAHHQACOYHQWINSA9GELCEZNQEUHV' + 'DH9UAYJVSTIIKW9URTHHIJYGWXGE9AEWISYW9BSJCSWTGRTJSGZPOXRPICUDATC' + 'LCVTF9BEDHSZZRLSH9IRMTFRVAMSSHC9TRYZGHPWRDVTXEXWTZ99999BSJCSWTG' + 'RTJSGZPOXRPICUDATCLCVTF9BEDHSZZRLSH9IRMTFRVAMSSHC9TRYZGHPWRDVTX' + 'EXWTZ9999PYOTA9TESTS9999999999999999LUSRBMHPF999999999MMMMMMMMM' + 'BOCWSYQAKMZXDR9ZPHXTXZORELC' + ), + ])) + + @async_test + async def test_happy_path(self): + """ + A bundle is successfully fetched with inclusion state. + """ + self.adapter.seed_response( + 'getTrytes', + { + 'trytes': self.single_bundle.as_tryte_strings() + } + ) + + with mock.patch( + 'iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', + MagicMock(return_value=async_return({ + 'states': {self.single_bundle.tail_transaction.hash: True}})) + ) as mocked_glis: + with mock.patch( + 'iota.commands.extended.get_bundles.GetBundlesCommand.__call__', + MagicMock(return_value=async_return({'bundles': [self.single_bundle]})) + ) as mocked_get_bundles: + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=[self.single_bundle.tail_transaction.hash], + inclusion_states=True, + ) + + self.assertListEqual( + response, + [self.single_bundle], + ) + + mocked_glis.assert_called_once_with( + hashes=[self.single_bundle.tail_transaction.hash] + ) + + mocked_get_bundles.assert_called_once_with( + transactions=[self.single_bundle.tail_transaction.hash] + ) + + self.assertTrue( + response[0].is_confirmed + ) + + @async_test + async def test_happy_path_no_inclusion(self): + """ + A bundle is successfully fetched without inclusion states. + """ + self.adapter.seed_response( + 'getTrytes', + { + 'trytes': self.single_bundle.as_tryte_strings() + } + ) + + with mock.patch( + 'iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', + MagicMock(return_value=async_return({'states': { + self.single_bundle.tail_transaction.hash: True + }})) + ) as mocked_glis: + with mock.patch( + 'iota.commands.extended.get_bundles.GetBundlesCommand.__call__', + MagicMock(return_value=async_return({'bundles': [self.single_bundle]})) + ) as mocked_get_bundles: + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=[self.single_bundle.tail_transaction.hash], + inclusion_states=False, + ) + + self.assertListEqual( + response, + [self.single_bundle], + ) + + self.assertFalse( + mocked_glis.called + ) + + mocked_get_bundles.assert_called_once_with( + transactions=[self.single_bundle.tail_transaction.hash] + ) + + self.assertFalse( + response[0].is_confirmed + ) + + @async_test + async def test_empty_list(self): + """ + Called with empty list of hashes. + """ + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=[], + inclusion_states=True, + ) + + self.assertListEqual( + response, + [] + ) + + @async_test + async def test_no_transaction_trytes(self): + """ + Node doesn't have the requested transaction trytes. + """ + self.adapter.seed_response( + 'getTrytes', + { + 'trytes': [ + self.single_bundle.tail_transaction.as_tryte_string(), + TransactionTrytes(''), + ] + } + ) + with self.assertRaises(BadApiResponse): + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=[ + self.single_bundle.tail_transaction.hash, + TransactionHash('') + ], + inclusion_states=False, + ) + + @async_test + async def test_multiple_tail_transactions(self): + """ + Multiple tail transactions are requested. + """ + self.adapter.seed_response( + 'getTrytes', + { + 'trytes': [ + self.single_bundle.tail_transaction.as_tryte_string(), + self.three_tx_bundle.tail_transaction.as_tryte_string(), + ] + } + ) + + with mock.patch( + 'iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', + MagicMock(return_value=async_return({'states': { + self.single_bundle.tail_transaction.hash: True, + self.three_tx_bundle.tail_transaction.hash: True + }})) + ) as mocked_glis: + with mock.patch( + 'iota.commands.extended.get_bundles.GetBundlesCommand.__call__', + MagicMock(return_value=async_return({ + 'bundles': [ + self.single_bundle, + self.three_tx_bundle, + ] + })) + ) as mocked_get_bundles: + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=[ + self.single_bundle.tail_transaction.hash, + self.three_tx_bundle.tail_transaction.hash + ], + inclusion_states=True, + ) + + self.assertListEqual( + response, + [ + self.single_bundle, + self.three_tx_bundle, + ], + ) + + # Check if it was called only once + mocked_glis.assert_called_once() + + # Get the keyword arguments from that call + _, _, mocked_glis_kwargs = mocked_glis.mock_calls[0] + + # 'hashes' keyword's value should be a list of hashes it was called + # with. Due to the set -> list conversion in the src code, we can't + # be sure of the order of the elements, so we check by value. + self.assertCountEqual( + mocked_glis_kwargs.get('hashes'), + [ + self.three_tx_bundle.tail_transaction.hash, + self.single_bundle.tail_transaction.hash, + ] + ) + + mocked_get_bundles.assert_called_once_with( + transactions=[ + self.single_bundle.tail_transaction.hash, + self.three_tx_bundle.tail_transaction.hash, + ] + ) + + self.assertTrue( + response[0].is_confirmed + ) + self.assertTrue( + response[1].is_confirmed + ) + + @async_test + async def test_non_tail(self): + """ + Called with a non-tail transaction. + """ + # For mocking GetTrytesCommand call + self.adapter.seed_response( + 'getTrytes', + { + # Tx with ID=1 + 'trytes': [self.three_tx_bundle[1].as_tryte_string()] + } + ) + + # For mocking FindTransactionObjectsCommand call + self.adapter.seed_response( + 'findTransactions', + { + 'hashes': [tx.hash for tx in self.three_tx_bundle] + } + ) + + self.adapter.seed_response( + 'getTrytes', + { + 'trytes': [tx.as_tryte_string() for tx in self.three_tx_bundle] + } + ) + + with mock.patch( + 'iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', + MagicMock(return_value=async_return({'states': { + self.three_tx_bundle.tail_transaction.hash: True + }})) + ) as mocked_glis: + with mock.patch( + 'iota.commands.extended.get_bundles.GetBundlesCommand.__call__', + MagicMock(return_value=async_return({ + 'bundles': [ + self.three_tx_bundle, + ] + })) + ) as mocked_get_bundles: + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=[self.three_tx_bundle[1].hash], + inclusion_states=True, + ) + + self.assertListEqual( + response, + [ + self.three_tx_bundle, + ], + ) + + self.assertTrue( + response[0].is_confirmed + ) + + mocked_glis.assert_called_once_with( + hashes=[self.three_tx_bundle.tail_transaction.hash] + ) + + mocked_get_bundles.assert_called_once_with( + transactions=[ + self.three_tx_bundle.tail_transaction.hash + ] + ) + + @async_test + async def test_ordered_by_timestamp(self): + """ + Returned bundles are sorted by tail transaction timestamp. + """ + self.adapter.seed_response( + 'getTrytes', + { + 'trytes': [ + self.three_tx_bundle.tail_transaction.as_tryte_string(), + self.single_bundle.tail_transaction.as_tryte_string(), + ] + } + ) + + with mock.patch( + 'iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', + MagicMock(return_value=async_return({'states': { + self.three_tx_bundle.tail_transaction.hash: True, + self.single_bundle.tail_transaction.hash: True, + }})) + ) as mocked_glis: + with mock.patch( + 'iota.commands.extended.get_bundles.GetBundlesCommand.__call__', + MagicMock(return_value=async_return({ + 'bundles': [ + self.three_tx_bundle, + self.single_bundle, + ] + })) + ) as mocked_get_bundles: + response = await get_bundles_from_transaction_hashes( + adapter=self.adapter, + # three_tx_bundle is the first now, which should be newer + # than single_bundle + transaction_hashes=[ + self.three_tx_bundle.tail_transaction.hash, + self.single_bundle.tail_transaction.hash + ], + inclusion_states=True, + ) + + self.assertListEqual( + response, + [ + # Response is sorted in ascending order based on timestamp! + # (single_bundle is older than three_tx_bundle) + self.single_bundle, + self.three_tx_bundle, + ], + ) + + # Check if it was called only once + mocked_glis.assert_called_once() + + # Get the keyword arguments from that call + _, _, mocked_glis_kwargs = mocked_glis.mock_calls[0] + + # 'hashes' keyword's value should be a list of hashes it was called + # with. Due to the set -> list conversion in the src code, we can't + # be sure of the order of the elements, so we check by value. + self.assertCountEqual( + mocked_glis_kwargs.get('hashes'), + [ + self.three_tx_bundle.tail_transaction.hash, + self.single_bundle.tail_transaction.hash, + ] + ) + + mocked_get_bundles.assert_called_once_with( + transactions=[ + self.three_tx_bundle.tail_transaction.hash, + self.single_bundle.tail_transaction.hash, + ] + ) + + self.assertTrue( + response[0].is_confirmed + ) + self.assertTrue( + response[1].is_confirmed + ) \ No newline at end of file