diff --git a/explorer/rest/rest/__init__.py b/explorer/rest/rest/__init__.py index 988df6a25d..e74e63feec 100644 --- a/explorer/rest/rest/__init__.py +++ b/explorer/rest/rest/__init__.py @@ -154,6 +154,37 @@ def api_get_nem_accounts(): return jsonify(results) + @app.route('/api/nem/namespace/') + def api_get_nem_namespace_by_name(name): + result = nem_api_facade.get_namespace_by_name(name) + + if not result: + abort(404) + + return jsonify(result) + + @app.route('/api/nem/namespaces') + def api_get_nem_namespaces(): + try: + limit = int(request.args.get('limit', 10)) + offset = int(request.args.get('offset', 0)) + sort = request.args.get('sort', 'DESC') + + if limit < 0 or offset < 0: + raise ValueError('Limit and offset must be greater than or equal to 0') + if sort.upper() not in ['ASC', 'DESC']: + raise ValueError('Sort must be either ASC or DESC') + + except ValueError as error: + abort(400, error) + + results = nem_api_facade.get_namespaces( + pagination=Pagination(limit, offset), + sort=sort + ) + + return jsonify(results) + def setup_error_handlers(app): @app.errorhandler(404) diff --git a/explorer/rest/rest/db/NemDatabase.py b/explorer/rest/rest/db/NemDatabase.py index 57cf966345..05c495fac1 100644 --- a/explorer/rest/rest/db/NemDatabase.py +++ b/explorer/rest/rest/db/NemDatabase.py @@ -5,6 +5,7 @@ from rest.model.Account import AccountView from rest.model.Block import BlockView +from rest.model.Namespace import NamespaceView from .DatabaseConnection import DatabaseConnectionPool @@ -51,7 +52,8 @@ def _create_block_view(self, result): size=size ) - def _create_account_view(self, result): # pylint: disable=no-self-use,too-many-locals + @staticmethod + def _create_account_view(result): # pylint: disable=too-many-locals ( address, public_key, @@ -91,7 +93,28 @@ def _create_account_view(self, result): # pylint: disable=no-self-use,too-many- cosignatories=[str(Address(address)) for address in cosignatories] if cosignatories else None ) - def _generate_account_query(self, where_condition, order_condition='', limit_condition=''): # pylint: disable=no-self-use + @staticmethod + def _create_namespace_view(result): + ( + root_namespace, + owner, + registered_height, + registered_timestamp, + expiration_height, + sub_namespaces + ) = result + + return NamespaceView( + root_namespace=root_namespace, + owner=str(PublicKey(owner)), + registered_height=registered_height, + registered_timestamp=str(registered_timestamp), + expiration_height=expiration_height, + sub_namespaces=sub_namespaces + ) + + @staticmethod + def _generate_account_query(where_condition, order_condition='', limit_condition=''): """Base account query.""" return f''' @@ -117,7 +140,8 @@ def _generate_account_query(self, where_condition, order_condition='', limit_con {limit_condition} ''' - def _generate_block_query(self, where_condition, order_condition='', limit_condition=''): # pylint: disable=no-self-use + @staticmethod + def _generate_block_query(where_condition, order_condition='', limit_condition=''): """Base block query.""" return f''' @@ -138,6 +162,26 @@ def _generate_block_query(self, where_condition, order_condition='', limit_condi {limit_condition} ''' + @staticmethod + def _generate_namespace_query(where_condition='', order_condition='', limit_condition=''): + """Base namespace query.""" + + return f''' + SELECT + root_namespace, + owner, + registered_height, + b.timestamp AS registered_timestamp, + expiration_height , + sub_namespaces + FROM namespaces n + left join blocks b + on n.registered_height = b.height + {where_condition} + {order_condition} + {limit_condition} + ''' + def _get_account(self, where_condition, query_bytes): """Gets account by where clause.""" @@ -217,3 +261,37 @@ def get_accounts(self, pagination, sorting, is_harvesting): results = cursor.fetchall() return [self._create_account_view(result) for result in results] + + def get_namespace_by_name(self, name): + """Gets namespace by root namespace or sub namespace name.""" + + where_condition = 'WHERE n.root_namespace = %s or %s = ANY(n.sub_namespaces)' + + sql = self._generate_namespace_query(where_condition) + + with self.connection() as connection: + cursor = connection.cursor() + cursor.execute(sql, (name, name)) + result = cursor.fetchone() + + return self._create_namespace_view(result) if result else None + + def get_namespaces(self, pagination, sort): + """Gets namespaces pagination in database.""" + + order_condition = f' ORDER BY registered_height {sort}' + limit_condition = ' LIMIT %s OFFSET %s' + + sql = self._generate_namespace_query( + order_condition=order_condition, + limit_condition=limit_condition + ) + + params = [pagination.limit, pagination.offset] + + with self.connection() as connection: + cursor = connection.cursor() + cursor.execute(sql, params) + results = cursor.fetchall() + + return [self._create_namespace_view(result) for result in results] diff --git a/explorer/rest/rest/facade/NemRestFacade.py b/explorer/rest/rest/facade/NemRestFacade.py index b220046001..0a8670666d 100644 --- a/explorer/rest/rest/facade/NemRestFacade.py +++ b/explorer/rest/rest/facade/NemRestFacade.py @@ -103,3 +103,17 @@ async def get_health(self): 'lastDBHeight': last_db_height, 'errors': errors } + + def get_namespace_by_name(self, name): + """Gets namespace by root namespace or sub namespace name.""" + + namespace = self.nem_db.get_namespace_by_name(name) + + return namespace.to_dict() if namespace else None + + def get_namespaces(self, pagination, sort): + """Gets namespaces pagination.""" + + namespaces = self.nem_db.get_namespaces(pagination, sort) + + return [namespace.to_dict() for namespace in namespaces] diff --git a/explorer/rest/rest/model/Namespace.py b/explorer/rest/rest/model/Namespace.py new file mode 100644 index 0000000000..2b23b4f746 --- /dev/null +++ b/explorer/rest/rest/model/Namespace.py @@ -0,0 +1,34 @@ +class NamespaceView: + def __init__(self, root_namespace, owner, registered_height, registered_timestamp, expiration_height, sub_namespaces): + """Create Namespace view.""" + + # pylint: disable=too-many-arguments,too-many-positional-arguments + + self.root_namespace = root_namespace + self.owner = owner + self.registered_height = registered_height + self.registered_timestamp = registered_timestamp + self.expiration_height = expiration_height + self.sub_namespaces = sub_namespaces + + def __eq__(self, other): + return isinstance(other, NamespaceView) and all([ + self.root_namespace == other.root_namespace, + self.owner == other.owner, + self.registered_height == other.registered_height, + self.registered_timestamp == other.registered_timestamp, + self.expiration_height == other.expiration_height, + self.sub_namespaces == other.sub_namespaces + ]) + + def to_dict(self): + """Formats the namespace info as a dictionary.""" + + return { + 'rootNamespace': self.root_namespace, + 'owner': self.owner, + 'registeredHeight': self.registered_height, + 'registeredTimestamp': self.registered_timestamp, + 'expirationHeight': self.expiration_height, + 'subNamespaces': self.sub_namespaces + } diff --git a/explorer/rest/tests/db/test_NemDatabase.py b/explorer/rest/tests/db/test_NemDatabase.py index d9d911d0bc..dadfb35fe3 100644 --- a/explorer/rest/tests/db/test_NemDatabase.py +++ b/explorer/rest/tests/db/test_NemDatabase.py @@ -1,7 +1,7 @@ from rest import Pagination, Sorting from rest.db.NemDatabase import NemDatabase -from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, ACCOUNTS, BLOCK_VIEWS, DatabaseTestBase +from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, ACCOUNTS, BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase # region test data @@ -13,10 +13,14 @@ EXPECTED_ACCOUNT_VIEW_2 = ACCOUNT_VIEWS[1] +EXPECTED_NAMESPACE_VIEW_1 = NAMESPACE_VIEWS[0] + +EXPECTED_NAMESPACE_VIEW_2 = NAMESPACE_VIEWS[1] + # endregion -class NemDatabaseTest(DatabaseTestBase): +class NemDatabaseTest(DatabaseTestBase): # pylint: disable=too-many-public-methods def setUp(self): super().setUp() @@ -109,3 +113,46 @@ def test_can_query_accounts_sorted_by_balance_desc(self): self._assert_can_query_accounts(Pagination(10, 0), Sorting('BALANCE', 'desc'), [EXPECTED_ACCOUNT_VIEW_2, EXPECTED_ACCOUNT_VIEW_1]) # endregion + + # region namespace + + def _assert_can_query_namespace_by_name(self, namespace, expected_namespace): + # Act: + namespace_view = self.nem_db.get_namespace_by_name(namespace) + + # Assert: + self.assertEqual(expected_namespace, namespace_view) + + def test_can_query_namespace_by_root_namespace(self): + self._assert_can_query_namespace_by_name('root', EXPECTED_NAMESPACE_VIEW_1) + + def test_can_query_namespace_by_sub_namespace(self): + self._assert_can_query_namespace_by_name('root_sub.sub_1', EXPECTED_NAMESPACE_VIEW_2) + + def test_cannot_query_nonexistent_namespace(self): + self._assert_can_query_namespace_by_name('nonexistent', None) + + # endregion + + # region namespaces + + def _assert_can_query_namespaces_with_filter(self, pagination, sort, expected_namespaces): + # Act: + namespaces_view = self.nem_db.get_namespaces(pagination, sort) + + # Assert: + self.assertEqual(expected_namespaces, namespaces_view) + + def test_can_query_namespaces_filtered_limit_offset_0(self): + self._assert_can_query_namespaces_with_filter(Pagination(1, 0), 'desc', [EXPECTED_NAMESPACE_VIEW_2]) + + def test_can_query_namespaces_filtered_offset_1(self): + self._assert_can_query_namespaces_with_filter(Pagination(1, 1), 'desc', [EXPECTED_NAMESPACE_VIEW_1]) + + def test_can_query_namespaces_sorted_by_registered_height_asc(self): + self._assert_can_query_namespaces_with_filter(Pagination(10, 0), 'asc', [EXPECTED_NAMESPACE_VIEW_1, EXPECTED_NAMESPACE_VIEW_2]) + + def test_can_query_namespaces_sorted_by_registered_height_desc(self): + self._assert_can_query_namespaces_with_filter(Pagination(10, 0), 'desc', [EXPECTED_NAMESPACE_VIEW_2, EXPECTED_NAMESPACE_VIEW_1]) + + # endregion diff --git a/explorer/rest/tests/facade/test_NemRestFacade.py b/explorer/rest/tests/facade/test_NemRestFacade.py index a623b08b64..7a9b7c653e 100644 --- a/explorer/rest/tests/facade/test_NemRestFacade.py +++ b/explorer/rest/tests/facade/test_NemRestFacade.py @@ -6,7 +6,7 @@ from rest.facade.NemRestFacade import NemRestFacade from rest.model.common import Pagination, RestConfig, Sorting -from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, BLOCK_VIEWS, DatabaseTestBase +from ..test.DatabaseTestUtils import ACCOUNT_VIEWS, BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase # region test data @@ -18,11 +18,14 @@ EXPECTED_ACCOUNT_2 = ACCOUNT_VIEWS[1].to_dict() +EXPECTED_NAMESPACE_1 = NAMESPACE_VIEWS[0].to_dict() + +EXPECTED_NAMESPACE_2 = NAMESPACE_VIEWS[1].to_dict() + # endregion class TestNemRestFacade(DatabaseTestBase): # pylint: disable=too-many-public-methods - def setUp(self): super().setUp() self.nem_rest_facade = NemRestFacade(self.db_config, RestConfig( @@ -210,3 +213,62 @@ def test_can_retrieve_health_with_node_exception(self, mock_chain_height): self.assertEqual([{'type': 'synchronization', 'message': 'Connection refused'}], result['errors']) # endregion + + # region namespace + + def _assert_can_retrieve_namespace_by_name(self, name, expected_namespace): + # Act: + namespace = self.nem_rest_facade.get_namespace_by_name(name) + + # Assert: + self.assertEqual(expected_namespace, namespace) + + def test_can_retrieve_namespace_by_root_namespace_name(self): + self._assert_can_retrieve_namespace_by_name(name='root', expected_namespace=EXPECTED_NAMESPACE_1) + + def test_can_retrieve_namespace_by_sub_namespace_name(self): + self._assert_can_retrieve_namespace_by_name(name='root_sub.sub_1', expected_namespace=EXPECTED_NAMESPACE_2) + + def test_returns_none_for_nonexistent_namespace_name(self): + self._assert_can_retrieve_namespace_by_name(name='nonexistent', expected_namespace=None) + + # endregion + + # region namespaces + + def _assert_can_retrieve_namespaces(self, pagination, sort, expected_namespaces): + # Act: + namespaces = self.nem_rest_facade.get_namespaces(pagination, sort) + + # Assert: + self.assertEqual(expected_namespaces, namespaces) + + def test_can_retrieve_namespaces_filtered_by_limit(self): + self._assert_can_retrieve_namespaces( + pagination=Pagination(1, 0), + sort='DESC', + expected_namespaces=[EXPECTED_NAMESPACE_2] + ) + + def test_can_retrieve_namespaces_filtered_by_offset(self): + self._assert_can_retrieve_namespaces( + pagination=Pagination(1, 1), + sort='DESC', + expected_namespaces=[EXPECTED_NAMESPACE_1] + ) + + def test_can_retrieve_namespaces_sorted_by_registered_height_asc(self): + self._assert_can_retrieve_namespaces( + pagination=Pagination(10, 0), + sort='ASC', + expected_namespaces=[EXPECTED_NAMESPACE_1, EXPECTED_NAMESPACE_2] + ) + + def test_can_retrieve_namespaces_sorted_by_registered_height_desc(self): + self._assert_can_retrieve_namespaces( + pagination=Pagination(10, 0), + sort='DESC', + expected_namespaces=[EXPECTED_NAMESPACE_2, EXPECTED_NAMESPACE_1] + ) + + # endregion diff --git a/explorer/rest/tests/model/test_Namespace.py b/explorer/rest/tests/model/test_Namespace.py new file mode 100644 index 0000000000..e8a90b1c5b --- /dev/null +++ b/explorer/rest/tests/model/test_Namespace.py @@ -0,0 +1,65 @@ +import unittest + +from rest.model.Namespace import NamespaceView + + +class NamespaceTest(unittest.TestCase): + @staticmethod + def _create_default_namespace_view(override=None): + namespace_view = NamespaceView( + root_namespace='namespace', + owner='107051C28A2C009A83AE0861CDBFF7C1CBAB387C964CC433F7D191D9C3115ED7', + registered_height=100, + registered_timestamp='2015-03-29 20:39:21', + expiration_height=525700, + sub_namespaces=['namespace.sub_1', 'namespace.sub_2'] + ) + + if override: + setattr(namespace_view, override[0], override[1]) + + return namespace_view + + def test_can_create_namespace_view(self): + # Act: + namespace_view = self._create_default_namespace_view() + + # Assert: + self.assertEqual('namespace', namespace_view.root_namespace) + self.assertEqual('107051C28A2C009A83AE0861CDBFF7C1CBAB387C964CC433F7D191D9C3115ED7', namespace_view.owner) + self.assertEqual(100, namespace_view.registered_height) + self.assertEqual('2015-03-29 20:39:21', namespace_view.registered_timestamp) + self.assertEqual(525700, namespace_view.expiration_height) + self.assertEqual(['namespace.sub_1', 'namespace.sub_2'], namespace_view.sub_namespaces) + + def test_can_convert_to_simple_dict(self): + # Arrange: + namespace_view = self._create_default_namespace_view() + + # Act: + namespace_view_dict = namespace_view.to_dict() + + # Assert: + self.assertEqual({ + 'rootNamespace': 'namespace', + 'owner': '107051C28A2C009A83AE0861CDBFF7C1CBAB387C964CC433F7D191D9C3115ED7', + 'registeredHeight': 100, + 'registeredTimestamp': '2015-03-29 20:39:21', + 'expirationHeight': 525700, + 'subNamespaces': ['namespace.sub_1', 'namespace.sub_2'] + }, namespace_view_dict) + + def test_eq_is_supported(self): + # Arrange: + namespace_view = self._create_default_namespace_view() + + # Assert: + self.assertEqual(namespace_view, self._create_default_namespace_view()) + self.assertNotEqual(namespace_view, None) + self.assertNotEqual(namespace_view, 'namespace_view') + self.assertNotEqual(namespace_view, self._create_default_namespace_view(('root_namespace', 'different'))) + self.assertNotEqual(namespace_view, self._create_default_namespace_view(('owner', 'PUBLIC_KEY'))) + self.assertNotEqual(namespace_view, self._create_default_namespace_view(('registered_height', 101))) + self.assertNotEqual(namespace_view, self._create_default_namespace_view(('registered_timestamp', 'timestamp'))) + self.assertNotEqual(namespace_view, self._create_default_namespace_view(('expiration_height', 525701))) + self.assertNotEqual(namespace_view, self._create_default_namespace_view(('sub_namespaces', ['namespace.sub_3']))) diff --git a/explorer/rest/tests/test/DatabaseTestUtils.py b/explorer/rest/tests/test/DatabaseTestUtils.py index ab0f12a84d..5fb4503af4 100644 --- a/explorer/rest/tests/test/DatabaseTestUtils.py +++ b/explorer/rest/tests/test/DatabaseTestUtils.py @@ -10,6 +10,7 @@ from rest.db.NemDatabase import NemDatabase from rest.model.Account import AccountView from rest.model.Block import BlockView +from rest.model.Namespace import NamespaceView Block = namedtuple( 'Block', @@ -41,6 +42,13 @@ 'cosignatory_of', 'cosignatories' ]) +Namespace = namedtuple('Namespace', [ + 'root_namespace', + 'owner', + 'registered_height', + 'expiration_height', + 'sub_namespaces' +]) DatabaseConfig = namedtuple('DatabaseConfig', ['database', 'user', 'password', 'host', 'port']) # region test data @@ -102,6 +110,23 @@ None, None) ] +NAMESPACES = [ + Namespace( + 'root', + PublicKey('a5f06d59b97aa40c82afb941a61fb6483bdb7491805cdb9dc47d92136983b9a5'), + 1, + 525700, + [] + ), + Namespace( + 'root_sub', + PublicKey('a5f06d59b97aa40c82afb941a61fb6483bdb7491805cdb9dc47d92136983b9a5'), + 2, + 525800, + ['root_sub.sub_1', 'root_sub.sub_2'] + ), +] + BLOCK_VIEWS = [ BlockView(*BLOCKS[0]._replace( @@ -159,6 +184,25 @@ ) ] +NAMESPACE_VIEWS = [ + NamespaceView( + root_namespace=NAMESPACES[0].root_namespace, + owner=str(NAMESPACES[0].owner), + registered_height=NAMESPACES[0].registered_height, + registered_timestamp=BLOCKS[0].timestamp, + expiration_height=NAMESPACES[0].expiration_height, + sub_namespaces=NAMESPACES[0].sub_namespaces + ), + NamespaceView( + root_namespace=NAMESPACES[1].root_namespace, + owner=str(NAMESPACES[1].owner), + registered_height=NAMESPACES[1].registered_height, + registered_timestamp=BLOCKS[1].timestamp, + expiration_height=NAMESPACES[1].expiration_height, + sub_namespaces=NAMESPACES[1].sub_namespaces + ) +] + # endregion @@ -209,6 +253,19 @@ def initialize_database(db_config, network_name): ) ''') + cursor.execute( + ''' + CREATE TABLE IF NOT EXISTS namespaces ( + id serial PRIMARY KEY, + root_namespace varchar(16) NOT NULL UNIQUE, + owner bytea NOT NULL, + registered_height bigint NOT NULL, + expiration_height bigint NOT NULL, + sub_namespaces VARCHAR(146)[] DEFAULT '{}' + ) + ''' + ) + # Insert data for block in BLOCKS: cursor.execute( @@ -265,6 +322,26 @@ def initialize_database(db_config, network_name): ) ) + for namespace in NAMESPACES: + cursor.execute( + ''' + INSERT INTO namespaces ( + root_namespace, + owner, + registered_height, + expiration_height, + sub_namespaces + ) + VALUES (%s, %s, %s, %s, %s) + ''', ( + namespace.root_namespace, + namespace.owner.bytes, + namespace.registered_height, + namespace.expiration_height, + namespace.sub_namespaces + ) + ) + connection.commit() diff --git a/explorer/rest/tests/test_rest.py b/explorer/rest/tests/test_rest.py index e507b25716..498b22f3e9 100644 --- a/explorer/rest/tests/test_rest.py +++ b/explorer/rest/tests/test_rest.py @@ -9,7 +9,7 @@ from rest import create_app -from .test.DatabaseTestUtils import ACCOUNT_VIEWS, BLOCK_VIEWS, DatabaseConfig, initialize_database +from .test.DatabaseTestUtils import ACCOUNT_VIEWS, BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseConfig, initialize_database DATABASE_CONFIG_INI = 'db_config.ini' @@ -19,6 +19,7 @@ EXPECTED_BLOCK_VIEW_2 = BLOCK_VIEWS[1] + # endregion # region fixtures @@ -75,6 +76,57 @@ def _assert_status_code_and_headers(response, expected_status_code): assert response.headers['Access-Control-Allow-Origin'] == '*' +def _assert_status_code_400(response, expected_message): + _assert_status_code_and_headers(response, 400) + assert { + 'message': expected_message, + 'status': 400 + } == response.json + + +def _get_api(client, endpoint, **query_params): # pylint: disable=redefined-outer-name + query_string = '&'.join(f'{key}={val}' for key, val in query_params.items()) + return client.get(f'/api/nem/{endpoint}?{query_string}') + + +def test_invalid_pagination_params(client): # pylint: disable=redefined-outer-name + + for module in ['blocks', 'accounts', 'namespaces']: + # Act: + response = client.get(f'/api/nem/{module}', query_string={'limit': -1}) + + _assert_status_code_400(response, 'Limit and offset must be greater than or equal to 0') + + # Act: + response = client.get(f'/api/nem/{module}', query_string={'offset': -1}) + + _assert_status_code_400(response, 'Limit and offset must be greater than or equal to 0') + + +def test_invalid_sort_params(client): # pylint: disable=redefined-outer-name + + for module in ['blocks', 'namespaces']: + # Act: + response = client.get(f'/api/nem/{module}', query_string={'sort': 'INVALID'}) + + # Assert: + _assert_status_code_400(response, 'Sort must be either ASC or DESC') + + +def test_data_not_found(client): # pylint: disable=redefined-outer-name + + for module, params in [('block', '/3'), ('account', '?address=NANEMOABLAGR72AZ2RV3V4ZHDCXW25XQ73O7OBT5'), ('namespace', '/nonexistent')]: + # Act: + response = client.get(f'/api/nem/{module}{params}') + + # Assert: + _assert_status_code_and_headers(response, 404) + assert { + 'message': 'Resource not found', + 'status': 404 + } == response.json + + # region /block/ def _assert_get_api_nem_block_by_height(client, height, expected_status_code, expected_result): # pylint: disable=redefined-outer-name @@ -90,13 +142,6 @@ def test_api_nem_block_by_height(client): # pylint: disable=redefined-outer-nam _assert_get_api_nem_block_by_height(client, 1, 200, EXPECTED_BLOCK_VIEW_1.to_dict()) -def test_api_nem_block_non_exist(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_block_by_height(client, 3, 404, { - 'message': 'Resource not found', - 'status': 404 - }) - - def test_api_nem_block_by_invalid_height(client): # pylint: disable=redefined-outer-name _assert_get_api_nem_block_by_height(client, 0, 400, { 'message': 'Height must be greater than or equal to 1', @@ -108,32 +153,15 @@ def test_api_nem_block_by_invalid_height(client): # pylint: disable=redefined-o # region /blocks -def _get_api_nem_blocks(client, **query_params): # pylint: disable=redefined-outer-name - query_string = '&'.join(f'{key}={val}' for key, val in query_params.items()) - return client.get(f'/api/nem/blocks?{query_string}') - - def _assert_get_api_nem_blocks(client, expected_status_code, expected_result, **query_params): # pylint: disable=redefined-outer-name # Act: - response = _get_api_nem_blocks(client, **query_params) + response = _get_api(client, 'blocks', **query_params) # Assert: _assert_status_code_and_headers(response, expected_status_code) assert expected_result == response.json -def _assert_get_api_nem_blocks_fail(client, expected_message, **query_params): # pylint: disable=redefined-outer-name - # Act: - response = _get_api_nem_blocks(client, **query_params) - - # Assert: - _assert_status_code_and_headers(response, 400) - assert { - 'message': expected_message, - 'status': 400 - } == response.json - - def test_api_nem_blocks_without_params(client): # pylint: disable=redefined-outer-name # Act: response = client.get('/api/nem/blocks') @@ -168,19 +196,15 @@ def test_api_nem_blocks_with_all_params(client): # pylint: disable=redefined-ou def test_api_nem_blocks_invalid_min_height(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_blocks_fail(client, 'Minimum height must be greater than or equal to 1', min_height=0) - - -def test_api_nem_blocks_invalid_limit(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_blocks_fail(client, 'Limit and offset must be greater than or equal to 0', limit=-1) - - -def test_api_nem_blocks_invalid_offset(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_blocks_fail(client, 'Limit and offset must be greater than or equal to 0', offset=-1) - + # Act: + response = _get_api(client, 'blocks', min_height=0) -def test_api_nem_blocks_invalid_sort(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_blocks_fail(client, 'Sort must be either ASC or DESC', sort='invalid') + # Assert: + _assert_status_code_and_headers(response, 400) + assert { + 'message': 'Minimum height must be greater than or equal to 1', + 'status': 400 + } == response.json # endregion @@ -242,17 +266,6 @@ def test_api_nem_account_invalid_public_key(client): # pylint: disable=redefine _assert_get_nem_account_bad_request(client, 'Invalid public key format', publicKey='INVALIDPUBLICKEY') -def test_api_nem_account_not_found(client): # pylint: disable=redefined-outer-name - # Act: - response = client.get('/api/nem/account?address=NANEMOABLAGR72AZ2RV3V4ZHDCXW25XQ73O7OBT5') - - # Assert: - _assert_status_code_and_headers(response, 404) - assert { - 'message': 'Resource not found', - 'status': 404 - } == response.json - # endregion # region /accounts @@ -260,7 +273,7 @@ def test_api_nem_account_not_found(client): # pylint: disable=redefined-outer-n def _assert_get_nem_accounts_bad_request(client, expected_message, **query_params): # pylint: disable=redefined-outer-name # Act: - response = client.get('/api/nem/accounts', query_string=query_params) + response = _get_api(client, 'accounts', **query_params) # Assert: _assert_status_code_and_headers(response, 400) @@ -271,12 +284,8 @@ def _assert_get_nem_accounts_bad_request(client, expected_message, **query_param def _assert_get_api_nem_accounts(client, expected_status_code, expected_result, **query_params): # pylint: disable=redefined-outer-name - # Arrange: - query_string = '&'.join(f'{key}={val}' for key, val in query_params.items()) - client.get(f'/api/nem/accounts?{query_string}') - # Act: - response = client.get(f'/api/nem/accounts?{query_string}') + response = _get_api(client, 'accounts', **query_params) # Assert: _assert_status_code_and_headers(response, expected_status_code) @@ -309,14 +318,6 @@ def test_api_nem_accounts_applies_sorted_by_balance_asc(client): # pylint: disa _assert_get_api_nem_accounts(client, 200, [ACCOUNT_VIEWS[0].to_dict(), ACCOUNT_VIEWS[1].to_dict()], sort_field='BALANCE', sort_order='ASC') -def test_api_nem_accounts_invalid_limit(client): # pylint: disable=redefined-outer-name - _assert_get_nem_accounts_bad_request(client, 'Limit and offset must be greater than or equal to 0', limit=-1) - - -def test_api_nem_accounts_invalid_offset(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_nem_accounts_bad_request(client, 'Limit and offset must be greater than or equal to 0', offset=-1) - - def test_api_nem_accounts_invalid_sort_field(client): # pylint: disable=redefined-outer-name, invalid-name _assert_get_nem_accounts_bad_request(client, 'Sort field must be BALANCE', sort_field='INVALID') @@ -395,3 +396,76 @@ def test_api_nem_health_node_fails(mock_chain_height, client): # pylint: disabl } == response.json # endregion + +# region /namespace/ + + +def _assert_get_nem_namespace_by_name(client, name, expected_status_code, expected_result): # pylint: disable=redefined-outer-name + # Act: + response = client.get(f'/api/nem/namespace/{name}') + + # Assert: + _assert_status_code_and_headers(response, expected_status_code) + assert expected_result == response.json + + +def test_api_namespace_by_root_namespace(client): # pylint: disable=redefined-outer-name + _assert_get_nem_namespace_by_name(client, 'root', 200, NAMESPACE_VIEWS[0].to_dict()) + + +def test_api_namespace_by_sub_namespace(client): # pylint: disable=redefined-outer-name + _assert_get_nem_namespace_by_name(client, 'root_sub.sub_1', 200, NAMESPACE_VIEWS[1].to_dict()) + + +def test_api_namespace_by_name_not_found(client): # pylint: disable=redefined-outer-name + _assert_get_nem_namespace_by_name(client, 'nonexistent', 404, { + 'message': 'Resource not found', + 'status': 404 + }) + + +# endregion + +# region /namespaces + +def _assert_get_api_nem_namespaces(client, expected_status_code, expected_result, **query_params): # pylint: disable=redefined-outer-name + # Act: + response = _get_api(client, 'namespaces', **query_params) + + # Assert: + _assert_status_code_and_headers(response, expected_status_code) + assert expected_result == response.json + + +def test_api_namespaces_without_params(client): # pylint: disable=redefined-outer-name + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[1].to_dict(), NAMESPACE_VIEWS[0].to_dict()]) + + +def test_api_namespaces_applies_limit(client): # pylint: disable=redefined-outer-name + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[1].to_dict()], limit=1) + + +def test_api_namespaces_applies_offset(client): # pylint: disable=redefined-outer-name + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[0].to_dict()], offset=1) + + +def test_api_namespaces_applies_sorted_by_registered_height_asc(client): # pylint: disable=redefined-outer-name, invalid-name + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[0].to_dict(), NAMESPACE_VIEWS[1].to_dict()], sort='ASC') + + +def test_api_namespaces_applies_sorted_by_registered_height_desc(client): # pylint: disable=redefined-outer-name, invalid-name + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[1].to_dict(), NAMESPACE_VIEWS[0].to_dict()], sort='DESC') + + +def test_api_namespaces_with_all_params(client): # pylint: disable=redefined-outer-name + _assert_get_api_nem_namespaces( + client, + 200, + [NAMESPACE_VIEWS[0].to_dict()], + limit=1, + offset=1, + sort='DESC' + ) + + +# endregion