diff --git a/cli/account/cli.py b/cli/account/cli.py index 97a9325..fe43c58 100644 --- a/cli/account/cli.py +++ b/cli/account/cli.py @@ -2,19 +2,19 @@ Provide implementation of the command line interface's account commands. """ import asyncio -import re import sys import click from remme import Remme +from cli.account.forms import GetAccountBalanceForm from cli.account.help import GET_ACCOUNT_BALANCE_ADDRESS_ARGUMENT_HELP_MESSAGE from cli.account.service import Account from cli.constants import ( - ADDRESS_REGEXP, FAILED_EXIT_FROM_COMMAND_CODE, NODE_URL_ARGUMENT_HELP_MESSAGE, ) +from cli.utils import dict_to_pretty_json loop = asyncio.get_event_loop() @@ -34,14 +34,22 @@ def get_balance(address, node_url): """ Get balance of the account by its address. """ - if re.match(pattern=ADDRESS_REGEXP, string=address) is None: - click.echo(f'The following address `{address}` is invalid.') + arguments, errors = GetAccountBalanceForm().load({ + 'address': address, + 'node_url': node_url, + }) + + if errors: + click.echo(dict_to_pretty_json(errors)) sys.exit(FAILED_EXIT_FROM_COMMAND_CODE) + address = arguments.get('address') + node_url = arguments.get('node_url') + if node_url is None: node_url = 'localhost' - remme = Remme(private_key_hex=None, network_config={ + remme = Remme(network_config={ 'node_address': str(node_url) + ':8080', }) diff --git a/cli/account/forms.py b/cli/account/forms.py new file mode 100644 index 0000000..7edca73 --- /dev/null +++ b/cli/account/forms.py @@ -0,0 +1,44 @@ +""" +Provide forms for command line interface's account commands. +""" +import re + +from marshmallow import ( + Schema, + ValidationError, + fields, + validates, +) + +from cli.constants import ( + ADDRESS_REGEXP, + DOMAIN_NAME_REGEXP, +) + + +class GetAccountBalanceForm(Schema): + """ + Get balance of the account. + """ + + address = fields.String() + node_url = fields.String() + + @validates('address') + def validate_address(self, address): + """ + Validate account address. + """ + if re.match(pattern=ADDRESS_REGEXP, string=address) is None: + raise ValidationError(f'The following address `{address}` is invalid.') + + @validates('node_url') + def validate_node_url(self, node_url): + """ + Validate node URL. + """ + if 'http' in node_url or 'https' in node_url: + raise ValidationError(f'Pass the following node URL `{node_url}` without protocol (http, https, etc.).') + + if re.match(pattern=DOMAIN_NAME_REGEXP, string=node_url) is None: + raise ValidationError(f'The following node URL `{node_url}` is invalid.') diff --git a/cli/constants.py b/cli/constants.py index edf96b1..918d339 100644 --- a/cli/constants.py +++ b/cli/constants.py @@ -2,6 +2,7 @@ Provide constants for command line interface. """ ADDRESS_REGEXP = '^[0-9a-f]{70}$' +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 FAILED_EXIT_FROM_COMMAND_CODE = -1 diff --git a/requirements.txt b/requirements.txt index 44223d4..7e36fe6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ accessify==0.3.1 asyncio==3.4.3 click==7.0 remme==1.0.0 +marshmallow==2.19.2 diff --git a/tests/test_account.py b/tests/test_account.py index 20f93c4..9f9b878 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -11,6 +11,7 @@ PASSED_EXIT_FROM_COMMAND_CODE, ) from cli.entrypoint import cli +from cli.utils import dict_to_pretty_json def test_get_balance(): @@ -49,5 +50,92 @@ def test_get_balance_invalid_address(): NODE_IP_ADDRESS_FOR_TESTING, ]) + expected_error = { + '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) in result.output + + +def test_get_balance_invalid_node_url(): + """ + Case: get a balance of an account by passing invalid node URL. + Expect: the following node URL is invalid error message. + """ + invalid_node_url = 'domainwithoutextention' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'account', + 'get-balance', + '--address', + '1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf', + '--node-url', + invalid_node_url, + ]) + + expected_error = { + '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 + + +def test_get_balance_node_url_with_http(): + """ + Case: get a balance of an account 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, [ + 'account', + 'get-balance', + '--address', + '1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf', + '--node-url', + node_url_with_http_protocol, + ]) + + expected_error = { + '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 + + +def test_get_balance_node_url_with_https(): + """ + Case: get a balance of an account by passing node URL with explicit HTTPS protocol. + Expect: the following node URL contains protocol error message. + """ + node_url_with_https_protocol = 'https://masternode.com' + + runner = CliRunner() + result = runner.invoke(cli, [ + 'account', + 'get-balance', + '--address', + '1120076ecf036e857f42129b58303bcf1e03723764a1702cbe98529802aad8514ee3cf', + '--node-url', + node_url_with_https_protocol, + ]) + + expected_error = { + 'node_url': [ + f'Pass the following node URL `{node_url_with_https_protocol}` without protocol (http, https, etc.).', + ], + } + assert FAILED_EXIT_FROM_COMMAND_CODE == result.exit_code - assert f'The following address `{invalid_address}` is invalid.' in result.output + assert dict_to_pretty_json(expected_error) in result.output