From a8a5079d02bc8ae41ac3198fd9801513c323ca60 Mon Sep 17 00:00:00 2001 From: Dmytro Striletskyi Date: Wed, 8 May 2019 17:26:54 +0300 Subject: [PATCH] Release v0.3.0: get transaction and transactions, atomic swap info, node info and state commands (#43) --- README.md | 179 ++++++- cli/atomic_swap/cli.py | 38 +- cli/atomic_swap/forms.py | 14 +- cli/atomic_swap/help.py | 1 + cli/atomic_swap/interfaces.py | 6 + cli/atomic_swap/service.py | 18 + cli/constants.py | 13 +- cli/entrypoint.py | 4 + cli/generic/forms/fields.py | 84 ++++ cli/node/cli.py | 30 ++ cli/node/forms.py | 8 + cli/node/interfaces.py | 6 + cli/node/service.py | 14 + cli/state/__init__.py | 0 cli/state/cli.py | 60 +++ cli/state/forms.py | 18 + cli/state/help.py | 4 + cli/state/interfaces.py | 15 + cli/state/service.py | 46 ++ cli/transaction/__init__.py | 0 cli/transaction/cli.py | 124 +++++ cli/transaction/forms.py | 44 ++ cli/transaction/help.py | 10 + cli/transaction/interfaces.py | 35 ++ cli/transaction/service.py | 81 ++++ setup.py | 2 +- tests/atomic_swap/test_get_info.py | 208 +++++++++ tests/conftest.py | 59 ++- tests/node/test_get_information.py | 138 ++++++ .../test_get_public_key_addresses.py | 29 -- tests/public_key/test_get_public_key_info.py | 6 +- tests/state/test_get_state.py | 175 +++++++ .../transaction/test_get_list_transactions.py | 436 ++++++++++++++++++ tests/transaction/test_get_transaction.py | 176 +++++++ 34 files changed, 2043 insertions(+), 38 deletions(-) create mode 100644 cli/state/__init__.py create mode 100644 cli/state/cli.py create mode 100644 cli/state/forms.py create mode 100644 cli/state/help.py create mode 100644 cli/state/interfaces.py create mode 100644 cli/state/service.py create mode 100644 cli/transaction/__init__.py create mode 100644 cli/transaction/cli.py create mode 100644 cli/transaction/forms.py create mode 100644 cli/transaction/help.py create mode 100644 cli/transaction/interfaces.py create mode 100644 cli/transaction/service.py create mode 100644 tests/atomic_swap/test_get_info.py create mode 100644 tests/node/test_get_information.py create mode 100644 tests/state/test_get_state.py create mode 100644 tests/transaction/test_get_list_transactions.py create mode 100644 tests/transaction/test_get_transaction.py diff --git a/README.md b/README.md index c2ac3c6..f26e3e9 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ * [Requirements](#getting-started-requirements) * [Installation](#installation) * [Usage](#usage) - * [Nodes](#nodes) * [Configuration file](#configuration-file) * [Service](#service) * [Account](#account) + * [Atomic Swap](#atomic-swap) * [Node](#node) * [Public key](#public-key) + * [State](#state) + * [Transaction](#transaction) * [Development](#development) * [Requirements](#development-requirements) * [Docker](#docker) @@ -158,6 +160,36 @@ $ remme atomic-swap get-public-key --node-url=node-6-testnet.remme.io } ``` +Get information about atomic swap by its identifier — ``remme atomic-swap get-info``: + +| Arguments | Type | Required | Description | +| :-------: | :----: | :------: | ------------------------------------------------- | +| id | String | Yes | Swap identifier to get information about swap by. | +| node-url | String | No | Node URL to apply a command to. | + +```bash +$ remme atomic-swap get-info \ + --id=033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808 \ + --node-url=node-genesis-testnet.remme.io +{ + "result": { + "information": { + "amount": "10.0000", + "created_at": 1556803765, + "email_address_encrypted_optional": "", + "is_initiator": false, + "receiver_address": "112007484def48e1c6b77cf784aeabcac51222e48ae14f3821697f4040247ba01558b1", + "secret_key": "", + "secret_lock": "0728356568862f9da0825aa45ae9d3642d64a6a732ad70b8857b2823dbf2a0b8", + "sender_address": "1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf", + "sender_address_non_local": "0xe6ca0e7c974f06471759e9a05d18b538c5ced11e", + "state": "OPENED", + "swap_id": "033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808" + } + } +} +``` + ### Node Get node configurations — ``remme node get-configs``: @@ -197,6 +229,24 @@ $ remme node get-peers --node-url=node-genesis-testnet.remme.io } ``` +Get node information — ``remme node get-info``: + +| Arguments | Type | Required | Description | +| :-------: | :----: | :------: | ------------------------------- | +| node-url | String | No | Node URL to apply a command to. | + +```bash +$ remme node get-info --node-url=node-27-testnet.remme.io +{ + "result": { + "information": { + "is_synced": true, + "peer_count": 3 + } + } +} +``` + ### Public key Get a list of the addresses of the public keys by account address — ``remme public-key get-list``: @@ -249,6 +299,133 @@ $ remme public-key get-info \ } ``` +### State + +Get a state by its address — ``remme state get``: + +| Arguments | Type | Required | Description | +| :-------: | :----: | :------: | ---------------------------------- | +| address | String | Yes | Account address to get a state by. | +| node-url | String | No | Node URL to apply a command to. | + +```bash +$ remme state get \ + --address=000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c12840f169a04216b7 \ + --node-url=node-6-testnet.remme.io +{ + "result": { + "state": { + "data": "CmwKJnNhd3Rvb3RoLnNldHRpbmdzLnZvdGUuYXV0aG9yaXplZF9rZXlzEkIwMmE2NTc5NmYyNDkwOTFjMzA4NzYxNGI0ZDljMjkyYjAwYjhlYmE1ODBkMDQ1YWMyZmQ3ODEyMjRiODdiNmYxM2U=", + "head": "95d78133eb98628d5ff17c7d1972b9ab03e50fceeb8e199d98cb52078550f5473bb001e57c116238697bdc1958eaf6d5f096f7b66974e1ea46b9c9da694be9d9" + } + } +} +``` + +### Transaction + +Get a list of transactions — ``remme transaction get-list``: + +| Arguments | Type | Required | Description | +| :---------: | :----: | :-------: | ----------------------------------------------------- | +| ids | String | No | Identifiers to get a list of transactions by. | +| start | String | No | Transaction identifier to get a list transaction starting from.| +| limit | Integer| No | Maximum amount of transactions to return. | +| head | String | No | Block identifier to get a list of transactions from. | +| reverse | Bool | No | Parameter to reverse result. | +| node-url | String | No | Node URL to apply a command to. | +| family-name | String | No | List of transactions by its family name. | + +```bash +$ remme transaction get-list \ + --ids='568a1094e574747c757c1f5028d9b929105984e509c4f2f3cb76e5f46f03ca4c3681ca0eeca86a4bd4bb5a3eaaa52fd73b08ebc5d5d85fbb1957b064f8b71972, + d9b891d3efdd51cd47156ad2083bf5cabd5b35bb2ebe66813996d1a0f783e58721bbc50917ff284a40696f24058ef1e22e48600abf37d500ace78eadf7f4ecff' \ + --start=568a1094e574747c757c1f5028d9b929105984e509c4f2f3cb76e5f46f03ca4c3681ca0eeca86a4bd4bb5a3eaaa52fd73b08ebc5d5d85fbb1957b064f8b71972 \ + --limit=2 \ + --head=39566f24561727f5ab2d19eb23612f1a38ff5f0cf9491caa0275261706a7cf8b080d38da0a38fa5b1cbef0cced889fdf6da679cc616a9711380f76b33e53efdf \ + --reverse \ + --family-name=account \ + --node-url=node-6-testnet.remme.io +{ + "result": { + "data": [ + { + "header": { + "batcher_public_key": "03d4613540ce29cd1f5f28ea9169a5cb5853bd53dede635903af9383bc9ffaf079", + "dependencies": [], + "family_name": "account", + "family_version": "0.1", + "inputs": [ + "112007db16c75019f59423da4de3cd5c79609989d7dc1697c9975307ea846e1d4af91f", + "1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf" + ], + "nonce": "99ccdbcfeb008e2c8407870b7033117e316b4b12df4173f3e2ffd510676e524a77ac64a0b65e6c7889a797fbd4e4462830548f455497e2362dde1bbf35d5372f", + "outputs": [ + "112007db16c75019f59423da4de3cd5c79609989d7dc1697c9975307ea846e1d4af91f", + "1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf" + ], + "payload_sha512": "1f0313cb9cd67559c1d33d61104882b3ebca80dfcd091d5ae3b0ee99bd27723af591551dfeea43be05e2b24a2f9a54adc6c357b60fc5c5720b161c5ff9d10ae1", + "signer_public_key": "03738df3f4ac3621ba8e89413d3ff4ad036c3a0a4dbb164b695885aab6aab614ad" + }, + "header_signature": "d9b891d3efdd51cd47156ad2083bf5cabd5b35bb2ebe66813996d1a0f783e58721bbc50917ff284a40696f24058ef1e22e48600abf37d500ace78eadf7f4ecff", + "payload": "CAASTQgAEkYxMTIwMDdkYjE2Yzc1MDE5ZjU5NDIzZGE0ZGUzY2Q1Yzc5NjA5OTg5ZDdkYzE2OTdjOTk3NTMwN2VhODQ2ZTFkNGFmOTFmGOgH" + } + ], + "head": "39566f24561727f5ab2d19eb23612f1a38ff5f0cf9491caa0275261706a7cf8b080d38da0a38fa5b1cbef0cced889fdf6da679cc616a9711380f76b33e53efdf", + "paging": { + "limit": 2, + "next": "", + "start": "568a1094e574747c757c1f5028d9b929105984e509c4f2f3cb76e5f46f03ca4c3681ca0eeca86a4bd4bb5a3eaaa52fd73b08ebc5d5d85fbb1957b064f8b71972" + } + } +} +``` + +Get a transaction by identifier — ``remme transaction get``: + +| Arguments | Type | Required | Description | +| :---------: | :----: | :-------: | -------------------------------- | +| id | String | Yes | Identifier to get transaction by. | +| node-url | String | No | Node URL to apply a command to. | + +```bash +$ remme transaction get \ + --id=64d032fbaae9bc59f9e5484ec6f52cbceef567923456039a26a1cfb8bc9ee2431ac2b5de43efce28ef11820a3734dab9fa56db57a1b2fbdc2323036cceeab6ab \ + --node-url=node-6-testnet.remme.io +{ + "result": { + "data": { + "header": { + "batcher_public_key": "03738df3f4ac3621ba8e89413d3ff4ad036c3a0a4dbb164b695885aab6aab614ad", + "dependencies": [], + "family_name": "consensus_account", + "family_version": "0.1", + "inputs": [ + "116829", + "112007", + "0000007ca83d6bbb759da9cde0fb0dec1400c54773f137ea7cfe91e3b0c44298fc1c14", + "0000007ca83d6bbb759da9cde0fb0dec1400c5034223fb6c3e825ee3b0c44298fc1c14", + "0000007ca83d6bbb759da9cde0fb0dec1400c5e64de9aa6a37ac92e3b0c44298fc1c14", + "00b10c0100000000000000000000000000000000000000000000000000000000000000", + "00b10c00", + "fd0e4f0000000000000000000000000000000000000000000000000000000000000000" + ], + "nonce": "b8baa6c54ab9463590627c18fb9c10ed", + "outputs": [ + "116829", + "112007", + "fd0e4f0000000000000000000000000000000000000000000000000000000000000000" + ], + "payload_sha512": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "signer_public_key": "03738df3f4ac3621ba8e89413d3ff4ad036c3a0a4dbb164b695885aab6aab614ad" + }, + "header_signature": "64d032fbaae9bc59f9e5484ec6f52cbceef567923456039a26a1cfb8bc9ee2431ac2b5de43efce28ef11820a3734dab9fa56db57a1b2fbdc2323036cceeab6ab", + "payload": "" + } + } +} +``` + ## Development

Requirements

diff --git a/cli/atomic_swap/cli.py b/cli/atomic_swap/cli.py index b1cbfd3..14af8d6 100644 --- a/cli/atomic_swap/cli.py +++ b/cli/atomic_swap/cli.py @@ -6,7 +6,11 @@ import click from remme import Remme -from cli.atomic_swap.forms import GetAtomicSwapPublicKeyForm +from cli.atomic_swap.forms import ( + GetAtomicSwapInformationForm, + GetAtomicSwapPublicKeyForm, +) +from cli.atomic_swap.help import SWAP_IDENTIFIER_ARGUMENT_HELP_MESSAGE from cli.atomic_swap.service import AtomicSwap from cli.constants import ( FAILED_EXIT_FROM_COMMAND_CODE, @@ -54,3 +58,35 @@ def get_public_key(node_url): sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) print_result(result=result) + + +@click.option('--id', type=str, required=True, help=SWAP_IDENTIFIER_ARGUMENT_HELP_MESSAGE) +@click.option('--node-url', type=str, required=False, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) +@atomic_swap_commands.command('get-info') +def get_swap_info(id, node_url): + """ + Get information about atomic swap by its identifier. + """ + arguments, errors = GetAtomicSwapInformationForm().load({ + 'id': id, + 'node_url': node_url, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + swap_id = arguments.get('id') + node_url = arguments.get('node_url') + + remme = Remme(network_config={ + 'node_address': str(node_url) + ':8080', + }) + + result, errors = AtomicSwap(service=remme).get(swap_id=swap_id) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=result) diff --git a/cli/atomic_swap/forms.py b/cli/atomic_swap/forms.py index fd65a79..7969484 100644 --- a/cli/atomic_swap/forms.py +++ b/cli/atomic_swap/forms.py @@ -3,7 +3,19 @@ """ from marshmallow import Schema -from cli.generic.forms.fields import NodeUrlField +from cli.generic.forms.fields import ( + NodeUrlField, + SwapIdentifierField, +) + + +class GetAtomicSwapInformationForm(Schema): + """ + Get information about atomic swap by its identifier form. + """ + + id = SwapIdentifierField(required=True) + node_url = NodeUrlField(required=False) class GetAtomicSwapPublicKeyForm(Schema): diff --git a/cli/atomic_swap/help.py b/cli/atomic_swap/help.py index 738ea13..69a355d 100644 --- a/cli/atomic_swap/help.py +++ b/cli/atomic_swap/help.py @@ -1,3 +1,4 @@ """ Provide help messages for command line interface's atomic swap commands. """ +SWAP_IDENTIFIER_ARGUMENT_HELP_MESSAGE = 'Swap identifier to get information about swap by.' diff --git a/cli/atomic_swap/interfaces.py b/cli/atomic_swap/interfaces.py index 89a8bc3..8e88e87 100644 --- a/cli/atomic_swap/interfaces.py +++ b/cli/atomic_swap/interfaces.py @@ -13,3 +13,9 @@ def get_public_key(self): Get public key of atomic swap. """ pass + + def get(self, swap_id): + """ + Get information about atomic swap by its identifier. + """ + pass diff --git a/cli/atomic_swap/service.py b/cli/atomic_swap/service.py index d996d53..a48d503 100644 --- a/cli/atomic_swap/service.py +++ b/cli/atomic_swap/service.py @@ -4,6 +4,7 @@ import asyncio from accessify import implements +from aiohttp_json_rpc import RpcGenericServerDefinedError from cli.atomic_swap.interfaces import AtomicSwapInterface @@ -38,3 +39,20 @@ def get_public_key(self): return { 'public_key': public_key, }, None + + def get(self, swap_id): + """ + Get information about atomic swap by its identifier. + """ + try: + swap_info = loop.run_until_complete(self.service.swap.get_info(swap_id=swap_id)) + + except RpcGenericServerDefinedError as error: + return None, str(error.message) + + except Exception as error: + return None, str(error) + + return { + 'information': swap_info.data, + }, None diff --git a/cli/constants.py b/cli/constants.py index d8821fb..336a077 100644 --- a/cli/constants.py +++ b/cli/constants.py @@ -1,12 +1,16 @@ """ Provide constants for command line interface. """ +from remme.models.utils.family_name import RemmeFamilyName + ADDRESS_REGEXP = r'^[0-9a-f]{70}$' BATCH_ID_REGEXP = r'^[0-9a-f]{128}$' -PRIVATE_KEY_REGEXP = r'^[a-f0-9]{64}$' PUBLIC_KEY_REGEXP = r'^[0-9a-f]{66}$' +PRIVATE_KEY_REGEXP = r'^[a-f0-9]{64}$' PUBLIC_KEY_ADDRESS_REGEXP = r'^[0-9a-f]{70}$' HEADER_SIGNATURE_REGEXP = r'^[0-9a-f]{128}$' +SWAP_IDENTIFIER_REGEXP = r'^[a-f0-9]{64}$' +TRANSACTION_HEADER_SIGNATURE_REGEXP = r'^[0-9a-f]{128}$' DOMAIN_NAME_REGEXP = r'(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]' PASSED_EXIT_FROM_COMMAND_CODE = 0 @@ -17,6 +21,13 @@ CLI_CONFIG_FILE_NAME = 'remme-core-cli' +FAMILY_NAMES = [ + RemmeFamilyName.ACCOUNT.value, + RemmeFamilyName.NODE_ACCOUNT.value, + RemmeFamilyName.PUBLIC_KEY.value, + RemmeFamilyName.SWAP.value, +] + NODE_IP_ADDRESS_FOR_TESTING = '159.89.104.9' LATEST_RELEASE_NODE_IP_ADDRESS_FOR_TESTING = '165.22.75.163' RELEASE_0_9_0_ALPHA_NODE_ADDRESS = '165.227.169.119' diff --git a/cli/entrypoint.py b/cli/entrypoint.py index de76827..2c1ec57 100644 --- a/cli/entrypoint.py +++ b/cli/entrypoint.py @@ -7,6 +7,8 @@ from cli.atomic_swap.cli import atomic_swap_commands from cli.node.cli import node_commands from cli.public_key.cli import public_key_commands +from cli.state.cli import state_command +from cli.transaction.cli import transaction_command @click.group() @@ -23,3 +25,5 @@ def cli(): cli.add_command(atomic_swap_commands) cli.add_command(node_commands) cli.add_command(public_key_commands) +cli.add_command(state_command) +cli.add_command(transaction_command) diff --git a/cli/generic/forms/fields.py b/cli/generic/forms/fields.py index 21e6051..352cda2 100644 --- a/cli/generic/forms/fields.py +++ b/cli/generic/forms/fields.py @@ -11,8 +11,11 @@ from cli.constants import ( ADDRESS_REGEXP, DOMAIN_NAME_REGEXP, + FAMILY_NAMES, PRIVATE_KEY_REGEXP, PUBLIC_KEY_ADDRESS_REGEXP, + SWAP_IDENTIFIER_REGEXP, + TRANSACTION_HEADER_SIGNATURE_REGEXP, ) @@ -36,6 +39,67 @@ def _deserialize(self, value, attr, obj, **kwargs): return address +class FamilyNameField(fields.Field): + """ + Implements validation of the family name. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, obj, **kwargs): + """ + Validate data (family name) that was passed to field. + """ + if value not in FAMILY_NAMES: + raise ValidationError(f'The following family name `{value}` is invalid.') + + return value + + +class TransactionIdentifiersListField(fields.Field): + """ + Implements validation of the list of the identifiers. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, obj, **kwargs): + """ + Validate data (list of the identifiers) that was passed to field. + """ + validated_identifiers = [] + + for identifier in value.split(','): + identifier = identifier.strip() + + if re.match(pattern=TRANSACTION_HEADER_SIGNATURE_REGEXP, string=identifier) is None: + raise ValidationError(f'The following identifier `{identifier}` is invalid.') + + validated_identifiers.append(identifier) + + return validated_identifiers + + +class TransactionIdentifierField(fields.Field): + """ + Implements validation of the identifier. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, obj, **kwargs): + """ + Validate data (identifier) that was passed to field. + """ + if re.match(pattern=TRANSACTION_HEADER_SIGNATURE_REGEXP, string=value) is None: + raise ValidationError(f'The following identifier `{value}` is invalid.') + + return value + + class NodeUrlField(fields.Field): """ Implements validation of the node URL. @@ -102,3 +166,23 @@ def _deserialize(self, value, attr, data, **kwargs): raise ValidationError(f'The following public key address `{public_key_address}` is invalid.') return value + + +class SwapIdentifierField(fields.Field): + """ + Implements validation of the swap identifier. + + References: + - https://marshmallow.readthedocs.io/en/3.0/custom_fields.html + """ + + def _deserialize(self, value, attr, data, **kwargs): + """ + Validate data (swap identifier) that was passed to field. + """ + swap_identifier = value + + if re.match(pattern=SWAP_IDENTIFIER_REGEXP, string=swap_identifier) is None: + raise ValidationError(f'The following swap identifier `{swap_identifier}` is invalid.') + + return swap_identifier diff --git a/cli/node/cli.py b/cli/node/cli.py index d57abcc..05b9a23 100644 --- a/cli/node/cli.py +++ b/cli/node/cli.py @@ -12,6 +12,7 @@ ) from cli.node.forms import ( GetNodeConfigurationsForm, + GetNodeInformationForm, GetNodePeersForm, ) from cli.node.service import Node @@ -86,3 +87,32 @@ def get_peers(node_url): sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) print_result(result=result) + + +@click.option('--node-url', type=str, required=False, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) +@node_commands.command('get-info') +def get_node_info(node_url): + """ + Get information about synchronization and peer count of the node. + """ + arguments, errors = GetNodeInformationForm().load({ + 'node_url': node_url, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + node_url = arguments.get('node_url') + + remme = Remme(network_config={ + 'node_address': str(node_url) + ':8080', + }) + + result, errors = Node(service=remme).get_info() + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=result) diff --git a/cli/node/forms.py b/cli/node/forms.py index 7463702..067828d 100644 --- a/cli/node/forms.py +++ b/cli/node/forms.py @@ -20,3 +20,11 @@ class GetNodePeersForm(Schema): """ node_url = NodeUrlField(required=False) + + +class GetNodeInformationForm(Schema): + """ + Get the node information form. + """ + + node_url = NodeUrlField(required=False) diff --git a/cli/node/interfaces.py b/cli/node/interfaces.py index 5fccbf1..67aa4df 100644 --- a/cli/node/interfaces.py +++ b/cli/node/interfaces.py @@ -19,3 +19,9 @@ def get_peers(self): Get the node's peers. """ pass + + def get_info(self): + """ + Get information about synchronization and peer count of the node. + """ + pass diff --git a/cli/node/service.py b/cli/node/service.py index bb42189..74b11c0 100644 --- a/cli/node/service.py +++ b/cli/node/service.py @@ -52,3 +52,17 @@ def get_peers(self): return { 'peers': peers, }, None + + def get_info(self): + """ + Get information about synchronization and peer count of the node form. + """ + try: + node_information = loop.run_until_complete(self.service.node_management.get_node_info()) + + except Exception as error: + return None, str(error) + + return { + 'information': node_information.data, + }, None diff --git a/cli/state/__init__.py b/cli/state/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/state/cli.py b/cli/state/cli.py new file mode 100644 index 0000000..8018611 --- /dev/null +++ b/cli/state/cli.py @@ -0,0 +1,60 @@ +""" +Provide implementation of the command line interface's state commands. +""" +import sys + +import click +from remme import Remme + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_URL_ARGUMENT_HELP_MESSAGE, +) +from cli.state.forms import GetStateForm +from cli.state.help import STATE_ACCOUNT_ADDRESS_ARGUMENT_HELP_MESSAGE +from cli.state.service import State +from cli.utils import ( + default_node_url, + print_errors, + print_result, +) + + +@click.group('state', chain=True) +def state_command(): + """ + Provide commands for working with state. + """ + pass + + +@click.option('--address', required=True, type=str, help=STATE_ACCOUNT_ADDRESS_ARGUMENT_HELP_MESSAGE) +@click.option('--node-url', required=False, type=str, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) +@state_command.command('get') +def get_state(address, node_url): + """ + Get a state by its address. + """ + arguments, errors = GetStateForm().load({ + 'address': address, + 'node_url': node_url, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + address = arguments.get('address') + node_url = arguments.get('node_url') + + remme = Remme(network_config={ + 'node_address': str(node_url) + ':8080', + }) + + result, errors = State(service=remme).get(address=address) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=result) diff --git a/cli/state/forms.py b/cli/state/forms.py new file mode 100644 index 0000000..e13b34d --- /dev/null +++ b/cli/state/forms.py @@ -0,0 +1,18 @@ +""" +Provide forms for command line interface's state commands. +""" +from marshmallow import Schema + +from cli.generic.forms.fields import ( + AccountAddressField, + NodeUrlField, +) + + +class GetStateForm(Schema): + """ + Get a state by its address form. + """ + + address = AccountAddressField(required=True) + node_url = NodeUrlField(required=False) diff --git a/cli/state/help.py b/cli/state/help.py new file mode 100644 index 0000000..272056b --- /dev/null +++ b/cli/state/help.py @@ -0,0 +1,4 @@ +""" +Provide help messages for command line interface's state commands. +""" +STATE_ACCOUNT_ADDRESS_ARGUMENT_HELP_MESSAGE = 'Account address to get a state by.' diff --git a/cli/state/interfaces.py b/cli/state/interfaces.py new file mode 100644 index 0000000..597ec1d --- /dev/null +++ b/cli/state/interfaces.py @@ -0,0 +1,15 @@ +""" +Provide implementation of the state interfaces. +""" + + +class StateInterface: + """ + Implements state interface. + """ + + def get(self, address): + """ + Get a state by its address. + """ + pass diff --git a/cli/state/service.py b/cli/state/service.py new file mode 100644 index 0000000..ef105a1 --- /dev/null +++ b/cli/state/service.py @@ -0,0 +1,46 @@ +""" +Provide implementation of the state. +""" +import asyncio + +from accessify import implements +from aiohttp_json_rpc import RpcGenericServerDefinedError + +from cli.state.interfaces import StateInterface + +loop = asyncio.get_event_loop() + + +@implements(StateInterface) +class State: + """ + Implements state. + """ + + def __init__(self, service): + """ + Constructor. + + Arguments: + service: object to interact with Remme core API. + """ + self.service = service + + def get(self, address): + """ + Get a state by its address. + """ + try: + state = loop.run_until_complete( + self.service.blockchain_info.get_state_by_address(address=address), + ) + + except RpcGenericServerDefinedError as error: + return None, str(error.message) + + except Exception as error: + return None, str(error) + + return { + 'state': state, + }, None diff --git a/cli/transaction/__init__.py b/cli/transaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/transaction/cli.py b/cli/transaction/cli.py new file mode 100644 index 0000000..4dcf0ee --- /dev/null +++ b/cli/transaction/cli.py @@ -0,0 +1,124 @@ +""" +Provide implementation of the command line interface's transaction commands. +""" +import sys + +import click +from remme import Remme + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_URL_ARGUMENT_HELP_MESSAGE, +) +from cli.transaction.forms import ( + GetTransactionForm, + GetTransactionsListForm, +) +from cli.transaction.help import ( + TRANSACTION_FAMILY_NAME_ARGUMENT_HELP_MESSAGE, + TRANSACTION_HEAD_ARGUMENT_HELP_MESSAGE, + TRANSACTION_ID_ARGUMENT_HELP_MESSAGE, + TRANSACTION_IDS_ARGUMENT_HELP_MESSAGE, + TRANSACTION_LIMIT_ARGUMENT_HELP_MESSAGE, + TRANSACTION_REVERSE_ARGUMENT_HELP_MESSAGE, + TRANSACTION_START_ARGUMENT_HELP_MESSAGE, +) +from cli.transaction.service import Transaction +from cli.utils import ( + default_node_url, + print_errors, + print_result, +) + + +@click.group('transaction', chain=True) +def transaction_command(): + """ + Provide commands for working with transaction. + """ + pass + + +@click.option('--ids', required=False, type=str, help=TRANSACTION_IDS_ARGUMENT_HELP_MESSAGE) +@click.option('--start', required=False, type=str, help=TRANSACTION_START_ARGUMENT_HELP_MESSAGE) +@click.option('--limit', required=False, type=int, help=TRANSACTION_LIMIT_ARGUMENT_HELP_MESSAGE) +@click.option('--head', required=False, type=str, help=TRANSACTION_HEAD_ARGUMENT_HELP_MESSAGE) +@click.option('--reverse', required=False, is_flag=True, help=TRANSACTION_REVERSE_ARGUMENT_HELP_MESSAGE) +@click.option('--family-name', required=False, type=str, help=TRANSACTION_FAMILY_NAME_ARGUMENT_HELP_MESSAGE) +@click.option('--node-url', required=False, type=str, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) +@transaction_command.command('get-list') +def get_transactions(ids, start, limit, head, reverse, family_name, node_url): + """ + Get a list of transactions. + """ + arguments, errors = GetTransactionsListForm().load({ + 'ids': ids, + 'start': start, + 'limit': limit, + 'head': head, + 'family_name': family_name, + 'reverse': reverse, + 'node_url': node_url, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + transaction_ids = arguments.get('ids') + start = arguments.get('start') + limit = arguments.get('limit') + head = arguments.get('head') + family_name = arguments.get('family_name') + node_url = arguments.get('node_url') + + remme = Remme(network_config={ + 'node_address': str(node_url) + ':8080', + }) + + transactions, errors = Transaction(service=remme).get_list( + transaction_ids=transaction_ids, + start=start, + limit=limit, + head=head, + family_name=family_name, + reverse=reverse, + ) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=transactions) + + +@click.option('--id', required=True, type=str, help=TRANSACTION_ID_ARGUMENT_HELP_MESSAGE) +@click.option('--node-url', required=False, type=str, help=NODE_URL_ARGUMENT_HELP_MESSAGE, default=default_node_url()) +@transaction_command.command('get') +def get_transaction(id, node_url): + """ + Fetch transaction by its identifier. + """ + arguments, errors = GetTransactionForm().load({ + 'id': id, + 'node_url': node_url, + }) + + if errors: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + transaction_id = arguments.get('id') + node_url = arguments.get('node_url') + + remme = Remme(network_config={ + 'node_address': str(node_url) + ':8080', + }) + + transaction, errors = Transaction(service=remme).get(transaction_id=transaction_id) + + if errors is not None: + print_errors(errors=errors) + sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + + print_result(result=transaction) diff --git a/cli/transaction/forms.py b/cli/transaction/forms.py new file mode 100644 index 0000000..6c1ce3f --- /dev/null +++ b/cli/transaction/forms.py @@ -0,0 +1,44 @@ +""" +Provide forms for command line interface's transaction commands. +""" +from marshmallow import ( + Schema, + fields, + validate, +) + +from cli.generic.forms.fields import ( + FamilyNameField, + NodeUrlField, + TransactionIdentifierField, + TransactionIdentifiersListField, +) + + +class GetTransactionsListForm(Schema): + """ + Get a list of transactions form. + """ + + ids = TransactionIdentifiersListField(allow_none=True, required=False) + start = TransactionIdentifierField(allow_none=True, required=False) + head = TransactionIdentifierField(allow_none=True, required=False) + limit = fields.Integer( + allow_none=True, + strict=True, + required=False, + validate=[ + validate.Range(min=1, error='Limit must be greater than 0.'), + ], + ) + family_name = FamilyNameField(allow_none=True, required=False) + node_url = NodeUrlField(required=False) + + +class GetTransactionForm(Schema): + """ + Get transaction form. + """ + + id = TransactionIdentifierField(allow_none=True, required=True) + node_url = NodeUrlField(required=False) diff --git a/cli/transaction/help.py b/cli/transaction/help.py new file mode 100644 index 0000000..3780807 --- /dev/null +++ b/cli/transaction/help.py @@ -0,0 +1,10 @@ +""" +Provide help messages for command line interface's transaction commands. +""" +TRANSACTION_IDS_ARGUMENT_HELP_MESSAGE = 'Identifiers to get a list of transactions by.' +TRANSACTION_START_ARGUMENT_HELP_MESSAGE = 'Transaction identifier to get a list transaction starting from.' +TRANSACTION_LIMIT_ARGUMENT_HELP_MESSAGE = 'Maximum amount of transactions to return.' +TRANSACTION_HEAD_ARGUMENT_HELP_MESSAGE = 'Block identifier to get a list of transactions from.' +TRANSACTION_REVERSE_ARGUMENT_HELP_MESSAGE = 'Parameter to reverse result.' +TRANSACTION_FAMILY_NAME_ARGUMENT_HELP_MESSAGE = 'List of transactions by its family name.' +TRANSACTION_ID_ARGUMENT_HELP_MESSAGE = 'Identifier to get a transaction by.' diff --git a/cli/transaction/interfaces.py b/cli/transaction/interfaces.py new file mode 100644 index 0000000..50ac1b3 --- /dev/null +++ b/cli/transaction/interfaces.py @@ -0,0 +1,35 @@ +""" +Provide implementation of the transaction interfaces. +""" + + +class TransactionInterface: + """ + Implements transaction interface. + """ + + def get_list(self, transaction_ids, start, limit, head, reverse, family_name): + """ + Get a list of transactions. + + A list of transactions could be filtered by transactions identifiers, start identifier, limit, head identifier, + reverse, family name. + + Arguments: + transaction_ids (list, optional): identifiers to get a list of transactions by. + start (string, optional): transaction identifier to get a list transaction starting from. + limit (int, optional): maximum amount of transactions to return. + head (string, optional): block identifier to get a list of transactions from. + reverse (string, optional): parameter to reverse result. + family_name (string, optional): list of a transactions by its family name. + """ + pass + + def get(self, transaction_id): + """ + Get transaction by its identifier. + + Arguments: + transaction_id (string, required): transaction identifier. + """ + pass diff --git a/cli/transaction/service.py b/cli/transaction/service.py new file mode 100644 index 0000000..e2eab14 --- /dev/null +++ b/cli/transaction/service.py @@ -0,0 +1,81 @@ +""" +Provide implementation of the transaction. +""" +import asyncio + +from accessify import implements +from aiohttp_json_rpc import RpcGenericServerDefinedError + +from cli.transaction.interfaces import TransactionInterface + +loop = asyncio.get_event_loop() + + +@implements(TransactionInterface) +class Transaction: + """ + Implements transaction. + """ + + def __init__(self, service): + """ + Constructor. + + Arguments: + service: object to interact with Remme core API. + """ + self.service = service + + def get_list(self, transaction_ids, start, limit, head, reverse, family_name): + """ + Get a list of transactions. + + Arguments: + transaction_ids (list, optional): identifiers to get a list of transactions by. + start (string, optional): transaction identifier to get a list transaction starting from. + limit (int, optional): maximum amount of transactions to return. + head (string, optional): block identifier to get a list of transactions from. + reverse (string, optional): parameter to reverse result. + family_name (string, optional): list of a transactions by its family name. + """ + reverse = '' if reverse else 'false' + + try: + transactions = loop.run_until_complete( + self.service.blockchain_info.get_transactions(query={ + 'ids': transaction_ids, + 'start': start, + 'limit': limit, + 'head': head, + 'family_name': family_name, + 'reverse': reverse, + }), + ) + + except RpcGenericServerDefinedError as error: + return None, str(error.message) + + except Exception as error: + return None, str(error) + + return transactions, None + + def get(self, transaction_id): + """ + Get a transaction. + + Arguments: + transaction_id (string, required): transaction identifier. + """ + try: + transaction = loop.run_until_complete( + self.service.blockchain_info.get_transaction_by_id(transaction_id=transaction_id), + ) + + except RpcGenericServerDefinedError as error: + return None, str(error.message) + + except Exception as error: + return None, str(error) + + return transaction, None diff --git a/setup.py b/setup.py index 08fdb1b..55d9a4d 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ requirements = f.read().splitlines() setup( - version='0.2.0', + version='0.3.0', name='remme-core-cli', description='The command-line interface (CLI) that provides a set of commands to interact with Remme-core.', long_description=long_description, diff --git a/tests/atomic_swap/test_get_info.py b/tests/atomic_swap/test_get_info.py new file mode 100644 index 0000000..08ce4a3 --- /dev/null +++ b/tests/atomic_swap/test_get_info.py @@ -0,0 +1,208 @@ +""" +Provide tests for command line interface's atomic swap commands. +""" +import json +import re + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + ADDRESS_REGEXP, + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_27_IN_TESTNET_ADDRESS, + PASSED_EXIT_FROM_COMMAND_CODE, + SWAP_IDENTIFIER_REGEXP, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + +SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE = '033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808' + + +def test_get_swap_info(): + """ + Case: get information about atomic swap by its identifier. + Expect: information about the swap is returned. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + swap_info = json.loads(result.output).get('result').get('information') + swap_identifier = swap_info.get('swap_id') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE == swap_identifier + assert re.match(pattern=ADDRESS_REGEXP, string=swap_info.get('sender_address')) is not None + assert re.match(pattern=ADDRESS_REGEXP, string=swap_info.get('receiver_address')) is not None + assert re.match(pattern=SWAP_IDENTIFIER_REGEXP, string=swap_identifier) is not None + assert isinstance(swap_info.get('is_initiator'), bool) + + +def test_get_swap_info_without_node_url(mocker, swap_info): + """ + Case: get information about atomic swap by its identifier without passing node URL. + Expect: information about the swap is returned from node on localhost. + """ + mock_swap_get_info = mocker.patch('cli.atomic_swap.service.loop.run_until_complete') + mock_swap_get_info.return_value = swap_info + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + ]) + + expected_result = { + 'result': { + 'information': swap_info.data, + }, + } + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_result == json.loads(result.output) + + +def test_get_swap_info_invalid_swap_id(): + """ + Case: get information about atomic swap by its invalid identifier. + Expect: the following swap identifier is invalid error message. + """ + invalid_swap_id = '033402fe1346742486b15a3a9966eb524927' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + invalid_swap_id, + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + expected_error = { + 'errors': { + 'id': [ + f'The following swap identifier `{invalid_swap_id}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_swap_info_non_existing_swap_id(): + """ + Case: get information about atomic swap by passing non-existing identifier. + Expect: atomic swap with identifier not found error message. + """ + non_existing_swap_id = '033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6809' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + non_existing_swap_id, + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + expected_error = { + 'errors': f'Atomic swap with id "{non_existing_swap_id}" not found.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_swap_info_non_existing_node_url(): + """ + Case: get information about atomic swap by passing non-existing node URL. + Expect: check if node running at URL error message. + """ + non_existing_node_url = 'non-existing-node.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + non_existing_node_url, + ]) + + expected_error = { + 'errors': f'Please check if your node running at http://{non_existing_node_url}:8080.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_swap_info_invalid_node_url(): + """ + Case: get information about atomic swap by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'domainwithoutextention' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + invalid_node_url, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'The following node URL `{invalid_node_url}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +@pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) +def test_get_swap_info_node_url_with_protocol(node_url_with_protocol): + """ + Case: get information about atomic swap by passing node URL with explicit protocol. + Expect: the following node URL contains protocol error message. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'atomic-swap', + 'get-info', + '--id', + SWAP_IDENTIFIER_PRESENTED_ON_THE_TEST_NODE, + '--node-url', + node_url_with_protocol, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_protocol}` without protocol (http, https, etc.).', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output diff --git a/tests/conftest.py b/tests/conftest.py index 7114926..b9fcb32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -156,6 +156,31 @@ def data(self): } +class AtomicSwapInformation: + """ + Impose atomic swap information data transfer object. + """ + + @property + def data(self): + """ + Get atomic swap information. + """ + return { + 'sender_address': '112007be95c8bb240396446ec359d0d7f04d257b72aeb4ab1ecfe50cf36e400a96ab9c', + 'receiver_address': '112007484def48e1c6b77cf784aeabcac51222e48ae14f3821697f4040247ba01558b1', + 'amount': '10.0000', + 'swap_id': '033402fe1346742486b15a3a9966eb5249271025fc7fb0b37ed3fdb4bcce6808', + 'secret_lock': '0728356568862f9da0825aa45ae9d3642d64a6a732ad70b8857b2823dbf2a0b8', + 'created_at': 1555943451, + 'sender_address_non_local': '0xe6ca0e7c974f06471759e9a05d18b538c5ced11e', + 'state': 'OPENED', + 'email_address_encrypted_optional': '', + 'secret_key': '', + 'is_initiator': False, + } + + class NodeConfigurations: """ Impose node configurations data transfer object. @@ -172,6 +197,22 @@ def data(self): } +class NodeInformation: + """ + Impose node information data transfer object. + """ + + @property + def data(self): + """ + Get node information. + """ + return { + 'is_synced': True, + 'peer_count': 3, + } + + @pytest.fixture() def sent_transaction(): """ @@ -181,16 +222,32 @@ def sent_transaction(): @pytest.fixture() -def public_key_info(): +def public_key_information(): """ Get public key information fixture. """ return PublicKeyInformation() +@pytest.fixture() +def swap_info(): + """ + Get atomic swap information fixture. + """ + return AtomicSwapInformation() + + @pytest.fixture() def node_configurations(): """ Get node configurations fixture. """ return NodeConfigurations() + + +@pytest.fixture() +def node_information(): + """ + Get node information fixture. + """ + return NodeInformation() diff --git a/tests/node/test_get_information.py b/tests/node/test_get_information.py new file mode 100644 index 0000000..2980115 --- /dev/null +++ b/tests/node/test_get_information.py @@ -0,0 +1,138 @@ +""" +Provide tests for command line interface's node get information command. +""" +import json + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_27_IN_TESTNET_ADDRESS, + PASSED_EXIT_FROM_COMMAND_CODE, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + + +def test_get_node_info(): + """ + Case: get information about synchronization and peer count of the node. + Expect: the flag is synced and peer count are returned. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'node', + 'get-info', + '--node-url', + NODE_27_IN_TESTNET_ADDRESS, + ]) + + node_information = json.loads(result.output).get('result').get('information') + + node_is_synced = node_information.get('is_synced') + node_peer_count = node_information.get('peer_count') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(node_is_synced, bool) + assert isinstance(node_peer_count, int) + + +def test_get_node_info_without_node_url(mocker, node_information): + """ + Case: get information about synchronization and peer count of the node without passing node URL. + Expect: the flag is synced and peer count are returned from a node on localhost + """ + mock_node_get_info = mocker.patch('cli.node.service.loop.run_until_complete') + mock_node_get_info.return_value = node_information + + runner = CliRunner() + result = runner.invoke(cli, [ + 'node', + 'get-info', + ]) + + expected_node_information = { + 'result': { + 'information': node_information.data, + }, + } + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_node_information == json.loads(result.output) + + +def test_get_node_info_invalid_node_url(): + """ + Case: get information about synchronization and peer count of the node by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'domainwithoutextention' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'node', + 'get-info', + '--node-url', + invalid_node_url, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'The following node URL `{invalid_node_url}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +@pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) +def test_get_node_info_node_url_with_protocol(node_url_with_protocol): + """ + Case: get information about synchronization and peer count of the node by passing node URL with explicit protocol. + Expect: the following node URL contains protocol error message. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'node', + 'get-info', + '--node-url', + node_url_with_protocol, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_protocol}` without protocol (http, https, etc.).', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output + + +def test_get_node_info_non_existing_node_url(): + """ + Case: get information about synchronization and peer count of the node by passing non-existing node URL. + Expect: check if node running at URL error message. + """ + non_existing_node_url = 'non-existing-node.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'node', + 'get-info', + '--node-url', + non_existing_node_url, + ]) + + expected_error = { + 'errors': f'Please check if your node running at http://{non_existing_node_url}:8080.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output diff --git a/tests/public_key/test_get_public_key_addresses.py b/tests/public_key/test_get_public_key_addresses.py index 0df9d2b..dcca911 100644 --- a/tests/public_key/test_get_public_key_addresses.py +++ b/tests/public_key/test_get_public_key_addresses.py @@ -121,35 +121,6 @@ def test_get_public_keys_invalid_node_url(): assert dict_to_pretty_json(expected_error) in result.output -def test_get_public_keys_node_url_with_http(): - """ - Case: get a list of the addresses of the public keys by passing node URL with explicit HTTP protocol. - Expect: the following node URL contains protocol error message. - """ - node_url_with_http_protocol = 'http://masternode.com' - - runner = CliRunner() - result = runner.invoke(cli, [ - 'public-key', - 'get-list', - '--address', - ADDRESS_PRESENTED_ON_THE_TEST_NODE, - '--node-url', - node_url_with_http_protocol, - ]) - - expected_error = { - 'errors': { - 'node_url': [ - f'Pass the following node URL `{node_url_with_http_protocol}` without protocol (http, https, etc.).', - ], - }, - } - - assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code - assert dict_to_pretty_json(expected_error) in result.output - - @pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) def test_get_public_keys_node_url_with_protocol(node_url_with_protocol): """ diff --git a/tests/public_key/test_get_public_key_info.py b/tests/public_key/test_get_public_key_info.py index 41187e8..b5b282f 100644 --- a/tests/public_key/test_get_public_key_info.py +++ b/tests/public_key/test_get_public_key_info.py @@ -77,13 +77,13 @@ def test_get_public_key_info_invalid_address(): assert dict_to_pretty_json(expected_error) in result.output -def test_get_public_key_info_without_node_url(mocker, public_key_info): +def test_get_public_key_info_without_node_url(mocker, public_key_information): """ Case: get information about public key without passing node URL. Expect: dictionary of public key information is returned from node on localhost. """ mock_public_key_get_info = mocker.patch('cli.public_key.service.loop.run_until_complete') - mock_public_key_get_info.return_value = public_key_info + mock_public_key_get_info.return_value = public_key_information runner = CliRunner() result = runner.invoke(cli, [ @@ -95,7 +95,7 @@ def test_get_public_key_info_without_node_url(mocker, public_key_info): expected_result = { 'result': { - 'information': public_key_info.data, + 'information': public_key_information.data, }, } diff --git a/tests/state/test_get_state.py b/tests/state/test_get_state.py new file mode 100644 index 0000000..fbc04d2 --- /dev/null +++ b/tests/state/test_get_state.py @@ -0,0 +1,175 @@ +""" +Provide tests for command line interface's get state command. +""" +import json +import re + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + HEADER_SIGNATURE_REGEXP, + NODE_IP_ADDRESS_FOR_TESTING, + PASSED_EXIT_FROM_COMMAND_CODE, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + +ADDRESS_WITH_STATE = '0000000000000000000000000000000000000000000000000000000000000000000001' + + +def test_get_state_with_address(): + """ + Case: get a state by its address. + Expect: state is returned. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'state', + 'get', + '--address', + ADDRESS_WITH_STATE, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + result_header_signature = json.loads(result.output).get('result').get('state').get('head') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert re.match(pattern=HEADER_SIGNATURE_REGEXP, string=result_header_signature) is not None + + +def test_get_state_with_invalid_address(): + """ + Case: get a state by its invalid address. + Expect: the following address is invalid error message. + """ + invalid_address = '044c7db163cf2' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'state', + 'get', + '--address', + invalid_address, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': { + 'address': [ + f'The following address `{invalid_address}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_state_with_non_existing_address(): + """ + Case: get a state by its non-existing address. + Expect: block for address not found error message. + """ + non_existing_address = '0000000000000000000000000000000000000000000000000000000000100000000031' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'state', + 'get', + '--address', + non_existing_address, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': f'Block for address `{non_existing_address}` not found.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_state_without_node_url(mocker): + """ + Case: get a state by its address without passing node URL. + Expect: state is returned from node on localhost. + """ + expected_result = { + "data": "CAE=", + "head": "2f7c6645c8e95c42ee229e14c64a80e382e3b3ef4edf73fadbc8f3605f4588ad" + "2e9272264d138278057de2f7961dcc962b4b89713cf69d256299a6635532017b", + } + + mock_get_state_by_address = mocker.patch('cli.state.service.loop.run_until_complete') + mock_get_state_by_address.return_value = expected_result + + runner = CliRunner() + result = runner.invoke(cli, [ + 'state', + 'get', + '--address', + ADDRESS_WITH_STATE, + ]) + + result_output = json.loads(result.output).get('result').get('state') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_result == result_output + + +def test_get_state_with_invalid_node_url(): + """ + Case: get a state by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'my-node-url.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'state', + 'get', + '--address', + ADDRESS_WITH_STATE, + '--node-url', + invalid_node_url, + ]) + + expected_error_message = { + 'errors': f'Please check if your node running at http://{invalid_node_url}:8080.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +@pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) +def test_get_state_node_url_with_protocol(node_url_with_protocol): + """ + Case: get a state by passing node URL with explicit protocol. + Expect: the following node URL contains protocol error message. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'state', + 'get', + '--address', + ADDRESS_WITH_STATE, + '--node-url', + node_url_with_protocol, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_protocol}` without protocol (http, https, etc.).', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output diff --git a/tests/transaction/test_get_list_transactions.py b/tests/transaction/test_get_list_transactions.py new file mode 100644 index 0000000..36770d4 --- /dev/null +++ b/tests/transaction/test_get_list_transactions.py @@ -0,0 +1,436 @@ +""" +Provide tests for command line interface's get list of transactions command. +""" +import json + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_IP_ADDRESS_FOR_TESTING, + PASSED_EXIT_FROM_COMMAND_CODE, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + + +def test_get_list_transactions_with_all_parameters(mocker): + """ + Case: get a list transactions by identifier, identifier starting from, limit, head, family name, reverse. + Expect: transaction are returned from node on localhost. + """ + transaction_id = '79a2780e9f07ca58d97b9de346730ddaba85dbb520778eb3d704cd214f6c580f' \ + '4f7fe4aa0e4fa9238e535f4af7e2dbae4134b4a726b36a5369c1cb4e971a2568' + + head = '95d78133eb98628d5ff17c7d1972b9ab03e50fceeb8e199d98cb52078550f547' \ + '3bb001e57c116238697bdc1958eaf6d5f096f7b66974e1ea46b9c9da694be9d9' + + expected_result = { + 'data': { + "header": { + "batcher_public_key": "02a65796f249091c3087614b4d9c292b00b8eba580d045ac2fd781224b87b6f13e", + "dependencies": [], + "family_name": "account", + "family_version": "0.1", + "inputs": [ + "0000000000000000000000000000000000000000000000000000000000000000000001", + ], + "nonce": "0x1.73309477e6e96p+30", + "outputs": [ + "0000000000000000000000000000000000000000000000000000000000000000000001", + "112007e5116c7f40c9ba679bedd8b50fff0a316b1eb0611a3cc1ceb39c56d206588624", + ], + "payload_sha512": "5bc11b6e912e3d16f90b49e4ef08661f827b8855c9d87deb5bad497a99107b36774b770" + "15cbe9e4c0a8d4763746c1b93e704f5a32d2d4f1c7fdddf5808013961", + "signer_public_key": "02a65796f249091c3087614b4d9c292b00b8eba580d045ac2fd781224b87b6f13e", + }, + "header_signature": "79a2780e9f07ca58d97b9de346730ddaba85dbb520778eb3d704cd214f6c580f" + "4f7fe4aa0e4fa9238e535f4af7e2dbae4134b4a726b36a5369c1cb4e971a2568", + "payload": "CAESBwiAoJSljR0=", + }, + } + + mock_get_transaction_by_ids = mocker.patch('cli.transaction.service.loop.run_until_complete') + mock_get_transaction_by_ids.return_value = expected_result + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--ids', + transaction_id, + '--start', + transaction_id, + '--limit', + 1, + '--head', + head, + '--family-name', + 'account', + '--reverse', + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_result == json.loads(result.output).get('result') + + +def test_get_list_transactions_with_ids(): + """ + Case: get a list transactions by identifiers. + Expect: transactions are returned. + """ + transaction_ids = '044c7db163cf21ab9eafc9b267693e2d732411056c7530e54282946ec47cc180' \ + '201e7be5612a671a7028474ad18e3738e676c17a86b7180fc1aad4c97e38b85b, ' \ + '6601e240044b00db4b7e5eda7800e88236341077879a4a9cf5a1b1f9fb2ece87' \ + '7bc9a43808d429e68f4d65ee8d7231e4e8711e705ad51be7888d1a7f25b57717' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--ids', + transaction_ids, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_header_signature = '6601e240044b00db4b7e5eda7800e88236341077879a4a9cf5a1b1f9fb2ece87' \ + '7bc9a43808d429e68f4d65ee8d7231e4e8711e705ad51be7888d1a7f25b57717' + + result_header_signature = json.loads(result.output).get('result').get('data')[0].get('header_signature') + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + assert expected_header_signature == result_header_signature + + +def test_get_list_transactions_with_invalid_ids(): + """ + Case: get a list transactions by invalid identifiers. + Expect: the following identifier are not valid error message. + """ + invalid_id = '044c7' + transaction_ids = '044c7db163cf21ab9eafc9b267693e2d732411056c7530e54282946ec47cc180' \ + '201e7be5612a671a7028474ad18e3738e676c17a86b7180fc1aad4c97e38b85b, ' \ + f'{invalid_id}' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--ids', + transaction_ids, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': { + 'ids': [ + f'The following identifier `{invalid_id}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_list_transactions_with_start(): + """ + Case: get a list transactions by transaction identifier starting from. + Expect: transactions are returned. + """ + start = 'c13fff007b5059ea0f95fc0dc0bdc897ef185b1e1187e355f3b02fb0aad515eb' \ + '1d679241758805d82fc1b07975cb49ee36e7c9574315fc1df5bae8eb5b2766f4' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--start', + start, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + + +def test_get_list_transactions_with_reverse(): + """ + Case: get a list transactions by reverse. + Expect: reverse list transactions are returned. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + '--reverse', + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + + +def test_get_list_transactions_by_head(): + """ + Case: get a list transactions by block identifier. + Expect: transactions are returned. + """ + head = '152f3be91d8238538a83077ec8cd5d1d937767c0930eea61b59151b0dfa7c5a1' \ + '79a66f176ce23c14a67d8451cec2852c8ff60fe9e8963c3ed115bd6078898da0' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--head', + head, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + + +@pytest.mark.parametrize('command_flag', ('--start', '--head')) +def test_get_list_transactions_with_invalid_start_head(command_flag): + """ + Case: get a list transactions by invalid block identifier and transaction identifier starting from. + Expect: the following identifier is not valid error message. + """ + invalid_id = '044c7db163cf21ab9eafc9b267693e2d732411056c7530e54282946ec47cc180' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + command_flag, + invalid_id, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': { + f'{command_flag[2:]}': [ + f'The following identifier `{invalid_id}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_list_transactions_with_limit(): + """ + Case: get a list transactions by limit. + Expect: transaction is returned. + """ + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--limit', + 1, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + + +def test_get_list_transactions_with_invalid_limit(): + """ + Case: get a list transactions by invalid limit. + Expect: the following limit should be a positive error message. + """ + invalid_limit = -33 + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--limit', + invalid_limit, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': { + 'limit': [ + 'Limit must be greater than 0.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_list_transactions_with_family_name(): + """ + Case: get a list transactions by family name. + Expect: transactions are returned. + """ + family_name = 'account' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--family-name', + family_name, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + + +def test_get_list_transactions_with_invalid_family_name(): + """ + Case: get a list transactions by invalid family name. + Expect: the following family name is not valid error message. + """ + invalid_family_name = 'non-existing family name' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--family-name', + invalid_family_name, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': { + "family_name": [ + f"The following family name `{invalid_family_name}` is invalid.", + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_list_transactions_with_invalid_node_url(): + """ + Case: get a list of transactions by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'my-node-url.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--ids', + '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5', + '--node-url', + invalid_node_url, + ]) + + expected_error_message = { + 'errors': f'Please check if your node running at http://{invalid_node_url}:8080.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_list_transactions_without_node_url(mocker): + """ + Case: get a list transactions by identifier without passing node URL. + Expect: transactions are returned from node on localhost. + """ + transaction_id = '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' \ + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5' + + expected_result = { + 'data': { + 'header': { + 'batcher_public_key': '02a65796f249091c3087614b4d9c292b00b8eba580d045ac2fd781224b87b6f13e', + 'family_name': 'sawtooth_settings', + 'family_version': '1.0', + 'inputs': [ + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c1c0cbf0fbcaf64c0b', + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c12840f169a04216b7', + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c1918142591ba4e8a7', + '000000a87cb5eafdcca6a8f82af32160bc5311783bdad381ea57b4e3b0c44298fc1c14', + ], + 'outputs': [ + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c1c0cbf0fbcaf64c0b', + '000000a87cb5eafdcca6a8f82af32160bc5311783bdad381ea57b4e3b0c44298fc1c14', + ], + 'payload_sha512': '82dd686e5298d24826d68ec2cdfbd1438a1b1d37a88abeacd24e25386d5939fa' + '139c3ab8b33ef594df804281c638887a0b9308c1f0a0922c5240202a4e2d0595', + 'signer_public_key': '02a65796f249091c3087614b4d9c292b00b8eba580d045ac2fd781224b87b6f13e', + 'dependencies': [], + 'nonce': '', + }, + 'header_signature': '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5', + 'payload': 'CAESRAoic2F3dG9vdGgudmFsaWRhdG9yLmJhdGNoX2luamVj' + 'dG9ycxIKYmxvY2tfaW5mbxoSMHhhNGY2YzZhZWMxOWQ1OTBi', + }, + } + + mock_get_transaction_by_ids = mocker.patch('cli.transaction.service.loop.run_until_complete') + mock_get_transaction_by_ids.return_value = expected_result + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get-list', + '--ids', + transaction_id, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_result == json.loads(result.output).get('result') + + +@pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) +def test_get_list_transactions_node_url_with_protocol(node_url_with_protocol): + """ + Case: get list transactions by passing node URL with explicit protocol. + Expect: the following node URL contains protocol error message. + """ + transaction_id = '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' \ + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get', + '--id', + transaction_id, + '--node-url', + node_url_with_protocol, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_protocol}` without protocol (http, https, etc.).', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output diff --git a/tests/transaction/test_get_transaction.py b/tests/transaction/test_get_transaction.py new file mode 100644 index 0000000..27f6bab --- /dev/null +++ b/tests/transaction/test_get_transaction.py @@ -0,0 +1,176 @@ +""" +Provide tests for command line interface's get transaction command. +""" +import json + +import pytest +from click.testing import CliRunner + +from cli.constants import ( + FAILED_EXIT_FROM_COMMAND_CODE, + NODE_IP_ADDRESS_FOR_TESTING, + PASSED_EXIT_FROM_COMMAND_CODE, +) +from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json + + +def test_get_transaction(): + """ + Case: get a transaction by identifier. + Expect: transaction is returned. + """ + transaction_id = 'c13fff007b5059ea0f95fc0dc0bdc897ef185b1e1187e355f3b02fb0aad515eb' \ + '1d679241758805d82fc1b07975cb49ee36e7c9574315fc1df5bae8eb5b2766f4' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get', + '--id', + transaction_id, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert isinstance(json.loads(result.output), dict) + + +def test_get_transaction_with_invalid_id(): + """ + Case: get a transaction by its invalid identifier. + Expect: the following identifier is invalid error message. + """ + invalid_transaction_id = '044c7db163cf21ab9eafc9b267693e2d732411056c7530e54282946ec47cc180dd' \ + '201e7be5612a671a7028474ad18e3738e676c17a86b7180fc1aad4c97e38b85bdd' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get', + '--id', + invalid_transaction_id, + '--node-url', + NODE_IP_ADDRESS_FOR_TESTING, + ]) + + expected_error_message = { + 'errors': { + 'id': [ + f'The following identifier `{invalid_transaction_id}` is invalid.', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +def test_get_transaction_without_node_url(mocker): + """ + Case: get a transaction by identifier without passing node URL. + Expect: transaction is returned from node on localhost. + """ + transaction_id = '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' \ + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5' + + expected_result = { + 'data': { + 'header': { + 'batcher_public_key': '02a65796f249091c3087614b4d9c292b00b8eba580d045ac2fd781224b87b6f13e', + 'family_name': 'sawtooth_settings', + 'family_version': '1.0', + 'inputs': [ + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c1c0cbf0fbcaf64c0b', + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c12840f169a04216b7', + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c1918142591ba4e8a7', + '000000a87cb5eafdcca6a8f82af32160bc5311783bdad381ea57b4e3b0c44298fc1c14', + ], + 'outputs': [ + '000000a87cb5eafdcca6a8cde0fb0dec1400c5ab274474a6aa82c1c0cbf0fbcaf64c0b', + '000000a87cb5eafdcca6a8f82af32160bc5311783bdad381ea57b4e3b0c44298fc1c14', + ], + 'payload_sha512': '82dd686e5298d24826d68ec2cdfbd1438a1b1d37a88abeacd24e25386d5939fa' + '139c3ab8b33ef594df804281c638887a0b9308c1f0a0922c5240202a4e2d0595', + 'signer_public_key': '02a65796f249091c3087614b4d9c292b00b8eba580d045ac2fd781224b87b6f13e', + 'dependencies': [], + 'nonce': '', + }, + 'header_signature': '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5', + 'payload': 'CAESRAoic2F3dG9vdGgudmFsaWRhdG9yLmJhdGNoX2luamVj' + 'dG9ycxIKYmxvY2tfaW5mbxoSMHhhNGY2YzZhZWMxOWQ1OTBi', + }, + } + + mock_get_transaction_by_id = mocker.patch('cli.transaction.service.loop.run_until_complete') + mock_get_transaction_by_id.return_value = expected_result + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get', + '--id', + transaction_id, + ]) + + assert PASSED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert expected_result == json.loads(result.output).get('result') + + +def test_get_transaction_with_invalid_node_url(): + """ + Case: get a transaction by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'my-node-url.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get', + '--id', + '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5', + '--node-url', + invalid_node_url, + ]) + + expected_error_message = { + 'errors': f'Please check if your node running at http://{invalid_node_url}:8080.', + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error_message) in result.output + + +@pytest.mark.parametrize('node_url_with_protocol', ['http://masternode.com', 'https://masternode.com']) +def test_get_transaction_node_url_with_protocol(node_url_with_protocol): + """ + Case: get transaction by passing node URL with explicit protocol. + Expect: the following node URL contains protocol error message. + """ + transaction_id = '8d8cb28c58f7785621b51d220b6a1d39fe5829266495d28eaf0362dc85d7e91c' \ + '205c1c4634604443dc566c56e1a4c0cf2eb122ac42cb482ef1436694634240c5' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'transaction', + 'get', + '--id', + transaction_id, + '--node-url', + node_url_with_protocol, + ]) + + expected_error = { + 'errors': { + 'node_url': [ + f'Pass the following node URL `{node_url_with_protocol}` without protocol (http, https, etc.).', + ], + }, + } + + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code + assert dict_to_pretty_json(expected_error) in result.output