diff --git a/explorer/rest/rest/__init__.py b/explorer/rest/rest/__init__.py index ef2071032..eb47263f7 100644 --- a/explorer/rest/rest/__init__.py +++ b/explorer/rest/rest/__init__.py @@ -102,6 +102,13 @@ def api_get_nem_namespaces(): return jsonify(nem_api_facade.get_namespaces(limit=limit, offset=offset, sort=sort)) + @app.route('/api/nem/mosaic/') + def api_get_nem_mosaic_by_name(name): + result = nem_api_facade.get_mosaic(name) + if not result: + abort(404) + return jsonify(result) + 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 6dec7bf21..874ae32ff 100644 --- a/explorer/rest/rest/db/NemDatabase.py +++ b/explorer/rest/rest/db/NemDatabase.py @@ -1,15 +1,20 @@ -from binascii import hexlify +from binascii import hexlify, unhexlify from symbolchain.CryptoTypes import PublicKey -from symbolchain.nem.Network import Network +from symbolchain.nem.Network import Address, Network from symbolchain.Network import NetworkLocator from rest.model.Block import BlockView +from rest.model.Mosaic import MosaicView from rest.model.Namespace import NamespaceView from .DatabaseConnection import DatabaseConnectionPool +def _format_address_bytes(buffer): + return unhexlify(_format_bytes(buffer)) + + def _format_bytes(buffer): return hexlify(buffer).decode('utf8').upper() @@ -18,6 +23,10 @@ def _format_xem_relative(amount): return amount / (10 ** 6) +def _format_relative(amount, divisibility): + return amount / (10 ** divisibility) + + class NemDatabase(DatabaseConnectionPool): """Database containing Nem blockchain data.""" @@ -112,6 +121,41 @@ def _create_namespace_view(self, result): mosaics=mosaics ) + def _create_mosaic_view(self, result): + levy_types = { + 1: 'absolute fee', + 2: 'percentile' + } + + creator_public_key = PublicKey(_format_bytes(result[2])) + levy_type = levy_types.get(result[10], None) + levy_fee = _format_relative(result[13], result[12]) if levy_type else None + + namespace_mosaic_name = result[0].split('.') + namespace_name = '.'.join(namespace_mosaic_name[:-1]) + mosaic_name = namespace_mosaic_name[-1] + + return MosaicView( + mosaic_name=mosaic_name, + namespace_name=namespace_name, + description=result[1], + creator=self.network.public_key_to_address(creator_public_key), + registered_height=result[3], + registered_timestamp=str(result[4]), + initial_supply=result[5], + total_supply=result[6], + divisibility=result[7], + supply_mutable=result[8], + transferable=result[9], + levy_type=levy_type, + levy_namespace=result[11], + levy_fee=levy_fee, + levy_recipient=Address(_format_address_bytes(result[14])) if result[14] else None, + root_namespace_registered_height=result[15], + root_namespace_registered_timestamp=str(result[16]), + root_namespace_expiration_height=result[17], + ) + def get_block(self, height): """Gets block by height in database.""" @@ -173,3 +217,47 @@ def get_namespaces(self, limit, offset, sort): results = cursor.fetchall() return [self._create_namespace_view(result) for result in results] + + def get_mosaic(self, namespace_name): + """Gets mosaic by namespace name in database.""" + + with self.connection() as connection: + cursor = connection.cursor() + cursor.execute(''' + SELECT + m1.namespace_name, + m1.description, + m1.creator, + m1.registered_height as mosaic_registered_height, + b2.timestamp as mosaic_registered_timestamp, + m1.initial_supply, + m1.total_supply, + m1.divisibility, + m1.supply_mutable, + m1.transferable, + m1.levy_type, + m1.levy_namespace_name, + CASE + WHEN m1.levy_namespace_name = 'nem.xem' THEN 6 + WHEN m1.levy_namespace_name IS NULL THEN NULL + ELSE m2.divisibility + END AS levy_namespace_divisibility, + m1.levy_fee, + m1.levy_recipient, + n.registered_height AS root_namespace_registered_height, + b1.timestamp AS root_namespace_registered_timestamp, + n.expiration_height + FROM mosaics m1 + LEFT JOIN mosaics m2 + ON m1.levy_namespace_name = m2.namespace_name AND m1.levy_namespace_name IS NOT NULL + LEFT JOIN namespaces n + ON m1.root_namespace = n.root_namespace + LEFT JOIN blocks b1 + ON b1.height = n.registered_height + LEFT JOIN blocks b2 + ON b2.height = m1.registered_height + WHERE m1.namespace_name = %s + ''', (namespace_name,)) + result = cursor.fetchone() + + return self._create_mosaic_view(result) if result else None diff --git a/explorer/rest/rest/facade/NemRestFacade.py b/explorer/rest/rest/facade/NemRestFacade.py index 085e26a77..ce93ee168 100644 --- a/explorer/rest/rest/facade/NemRestFacade.py +++ b/explorer/rest/rest/facade/NemRestFacade.py @@ -36,3 +36,10 @@ def get_namespaces(self, limit, offset, sort): namespaces = self.nem_db.get_namespaces(limit, offset, sort) return [namespace.to_dict() for namespace in namespaces] + + def get_mosaic(self, name): + """Gets mosaic by namespace name.""" + + mosaic = self.nem_db.get_mosaic(name) + + return mosaic.to_dict() if mosaic else None diff --git a/explorer/rest/rest/model/Mosaic.py b/explorer/rest/rest/model/Mosaic.py new file mode 100644 index 000000000..9d4d3d1aa --- /dev/null +++ b/explorer/rest/rest/model/Mosaic.py @@ -0,0 +1,90 @@ +class MosaicView: # pylint: disable=too-many-instance-attributes, too-many-locals + def __init__( + self, + mosaic_name, + namespace_name, + description, + creator, + registered_height, + registered_timestamp, + initial_supply, + total_supply, + divisibility, + supply_mutable, + transferable, + levy_type, + levy_namespace, + levy_fee, + levy_recipient, + root_namespace_registered_height, + root_namespace_registered_timestamp, + root_namespace_expiration_height, + ): + """Create mosaic view.""" + + # pylint: disable=too-many-arguments + + self.mosaic_name = mosaic_name + self.namespace_name = namespace_name + self.description = description + self.creator = creator + self.registered_height = registered_height + self.registered_timestamp = registered_timestamp + self.initial_supply = initial_supply + self.total_supply = total_supply + self.divisibility = divisibility + self.supply_mutable = supply_mutable + self.transferable = transferable + self.levy_type = levy_type + self.levy_namespace = levy_namespace + self.levy_fee = levy_fee + self.levy_recipient = levy_recipient + self.root_namespace_registered_height = root_namespace_registered_height + self.root_namespace_registered_timestamp = root_namespace_registered_timestamp + self.root_namespace_expiration_height = root_namespace_expiration_height + + def __eq__(self, other): + return isinstance(other, MosaicView) and all([ + self.mosaic_name == other.mosaic_name, + self.namespace_name == other.namespace_name, + self.description == other.description, + self.creator == other.creator, + self.registered_height == other.registered_height, + self.registered_timestamp == other.registered_timestamp, + self.initial_supply == other.initial_supply, + self.total_supply == other.total_supply, + self.divisibility == other.divisibility, + self.supply_mutable == other.supply_mutable, + self.transferable == other.transferable, + self.levy_type == other.levy_type, + self.levy_namespace == other.levy_namespace, + self.levy_fee == other.levy_fee, + self.levy_recipient == other.levy_recipient, + self.root_namespace_registered_height == other.root_namespace_registered_height, + self.root_namespace_registered_timestamp == other.root_namespace_registered_timestamp, + self.root_namespace_expiration_height == other.root_namespace_expiration_height + ]) + + def to_dict(self): + """Formats the mosaic info as a dictionary.""" + + return { + 'mosaicName': self.mosaic_name, + 'namespaceName': self.namespace_name, + 'description': self.description, + 'creator': str(self.creator), + 'registeredHeight': self.registered_height, + 'registeredTimestamp': str(self.registered_timestamp), + 'initialSupply': self.initial_supply, + 'totalSupply': self.total_supply, + 'divisibility': self.divisibility, + 'supplyMutable': self.supply_mutable, + 'transferable': self.transferable, + 'levyType': self.levy_type, + 'levyNamespace': self.levy_namespace, + 'levyFee': self.levy_fee, + 'levyRecipient': str(self.levy_recipient) if self.levy_recipient else None, + 'rootNamespaceRegisteredHeight': self.root_namespace_registered_height, + 'rootNamespaceRegisteredTimestamp': str(self.root_namespace_registered_timestamp), + 'rootNamespaceExpirationHeight': self.root_namespace_expiration_height + } diff --git a/explorer/rest/tests/db/test_NemDatabase.py b/explorer/rest/tests/db/test_NemDatabase.py index e313d949e..dbf3c3543 100644 --- a/explorer/rest/tests/db/test_NemDatabase.py +++ b/explorer/rest/tests/db/test_NemDatabase.py @@ -2,23 +2,11 @@ from rest.db.NemDatabase import NemDatabase -from ..test.DatabaseTestUtils import BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase +from ..test.DatabaseTestUtils import BLOCK_VIEWS, MOSAIC_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase BlockQueryParams = namedtuple('BlockQueryParams', ['limit', 'offset', 'min_height', 'sort']) PaginationQueryParams = namedtuple('PaginationQueryParams', ['limit', 'offset', 'sort']) -# region test data - -EXPECTED_BLOCK_VIEW_1 = BLOCK_VIEWS[0] - -EXPECTED_BLOCK_VIEW_2 = BLOCK_VIEWS[1] - -EXPECTED_NAMESPACE_VIEW_1 = NAMESPACE_VIEWS[0] - -EXPECTED_NAMESPACE_VIEW_2 = NAMESPACE_VIEWS[1] - -# endregion - class NemDatabaseTest(DatabaseTestBase): @@ -45,44 +33,42 @@ def _assert_can_query_blocks_with_filter(self, query_params, expected_blocks): self.assertEqual(expected_blocks, blocks_view) def test_can_query_block_by_height_1(self): - self._assert_can_query_block_by_height(1, EXPECTED_BLOCK_VIEW_1) + self._assert_can_query_block_by_height(1, BLOCK_VIEWS[0]) def test_cannot_query_nonexistent_block(self): self._assert_can_query_block_by_height(3, None) def test_can_query_blocks_filtered_limit(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(1, 0, 1, 'desc'), [EXPECTED_BLOCK_VIEW_2]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(1, 0, 1, 'desc'), [BLOCK_VIEWS[1]]) def test_can_query_blocks_filtered_offset_0(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(1, 0, 0, 'desc'), [EXPECTED_BLOCK_VIEW_2]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(1, 0, 0, 'desc'), [BLOCK_VIEWS[1]]) def test_can_query_blocks_filtered_offset_1(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(1, 1, 0, 'desc'), [EXPECTED_BLOCK_VIEW_1]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(1, 1, 0, 'desc'), [BLOCK_VIEWS[0]]) def test_can_query_blocks_filtered_min_height_1(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 1, 'desc'), [EXPECTED_BLOCK_VIEW_2, EXPECTED_BLOCK_VIEW_1]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 1, 'desc'), [BLOCK_VIEWS[1], BLOCK_VIEWS[0]]) def test_can_query_blocks_filtered_min_height_2(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 2, 'desc'), [EXPECTED_BLOCK_VIEW_2]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 2, 'desc'), [BLOCK_VIEWS[1]]) def test_can_query_blocks_filtered_min_height_3(self): self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 3, 'desc'), []) def test_can_query_blocks_sorted_by_height_asc(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 0, 'asc'), [EXPECTED_BLOCK_VIEW_1, EXPECTED_BLOCK_VIEW_2]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 0, 'asc'), [BLOCK_VIEWS[0], BLOCK_VIEWS[1]]) def test_can_query_blocks_sorted_by_height_desc(self): - self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 0, 'desc'), [EXPECTED_BLOCK_VIEW_2, EXPECTED_BLOCK_VIEW_1]) + self._assert_can_query_blocks_with_filter(BlockQueryParams(10, 0, 0, 'desc'), [BLOCK_VIEWS[1], BLOCK_VIEWS[0]]) # endregion # region namespace tests def _assert_can_query_namespace_by_name(self, name, expected_namespace): - # Arrange: nem_db = NemDatabase(self.db_config, self.network_name) - # Act: namespace_view = nem_db.get_namespace(name) # Assert: @@ -99,27 +85,47 @@ def _assert_can_query_namespaces_with_filter(self, query_params, expected_namesp self.assertEqual(expected_namespaces, namespaces_view) def test_can_query_namespace_by_name(self): - self._assert_can_query_namespace_by_name('oxford', EXPECTED_NAMESPACE_VIEW_1) + self._assert_can_query_namespace_by_name('oxford', NAMESPACE_VIEWS[0]) def test_cannot_query_nonexistent_namespace(self): self._assert_can_query_namespace_by_name('non_exist', None) def test_can_query_namespaces_filtered_limit(self): - self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 0, 'desc'), [EXPECTED_NAMESPACE_VIEW_2]) + self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 0, 'desc'), [NAMESPACE_VIEWS[1]]) def test_can_query_namespaces_filtered_offset_0(self): - self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 0, 'desc'), [EXPECTED_NAMESPACE_VIEW_2]) + self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 0, 'desc'), [NAMESPACE_VIEWS[1]]) def test_can_query_namespaces_filtered_offset_1(self): - self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 1, 'desc'), [EXPECTED_NAMESPACE_VIEW_1]) + self._assert_can_query_namespaces_with_filter(PaginationQueryParams(1, 1, 'desc'), [NAMESPACE_VIEWS[0]]) def test_can_query_namespaces_sorted_by_id_asc(self): - self._assert_can_query_namespaces_with_filter(PaginationQueryParams(10, 0, 'asc'), [EXPECTED_NAMESPACE_VIEW_1, EXPECTED_NAMESPACE_VIEW_2]) + self._assert_can_query_namespaces_with_filter(PaginationQueryParams(10, 0, 'asc'), [NAMESPACE_VIEWS[0], NAMESPACE_VIEWS[1]]) def test_can_query_namespaces_sorted_by_id_desc(self): self._assert_can_query_namespaces_with_filter( PaginationQueryParams(10, 0, 'desc'), - [EXPECTED_NAMESPACE_VIEW_2, EXPECTED_NAMESPACE_VIEW_1] + [NAMESPACE_VIEWS[1], NAMESPACE_VIEWS[0]] ) # endregion + + # region mosaic tests + + def _assert_can_query_mosaic_by_name(self, namespace_name, expected_mosaic): + # Arrange: + nem_db = NemDatabase(self.db_config, self.network_name) + + # Act: + mosaic_view = nem_db.get_mosaic(namespace_name) + + # Assert: + self.assertEqual(expected_mosaic, mosaic_view) + + def test_can_query_mosaic_by_name(self): + self._assert_can_query_mosaic_by_name('dragon.dragonfly', MOSAIC_VIEWS[0]) + + def test_cannot_query_nonexistent_mosaic(self): + self._assert_can_query_mosaic_by_name('non-exist-mosaic', None) + + # endregion diff --git a/explorer/rest/tests/facade/test_NemRestFacade.py b/explorer/rest/tests/facade/test_NemRestFacade.py index 014f35dd3..313d6f1f9 100644 --- a/explorer/rest/tests/facade/test_NemRestFacade.py +++ b/explorer/rest/tests/facade/test_NemRestFacade.py @@ -1,7 +1,7 @@ from rest.facade.NemRestFacade import NemRestFacade from ..db.test_NemDatabase import BlockQueryParams, PaginationQueryParams -from ..test.DatabaseTestUtils import BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase +from ..test.DatabaseTestUtils import BLOCK_VIEWS, MOSAIC_VIEWS, NAMESPACE_VIEWS, DatabaseTestBase # region test data @@ -13,6 +13,8 @@ EXPECTED_NAMESPACE_2 = NAMESPACE_VIEWS[1].to_dict() +EXPECTED_MOSAIC_1 = MOSAIC_VIEWS[0].to_dict() + # endregion @@ -107,3 +109,23 @@ def test_namespaces_sorted_by_id_desc(self): self._assert_can_retrieve_namespaces(PaginationQueryParams(10, 0, 'desc'), [EXPECTED_NAMESPACE_2, EXPECTED_NAMESPACE_1]) # endregion + + # region mosaic tests + + def _assert_can_retrieve_mosaic(self, name, expected_mosaic): + # Arrange: + nem_rest_facade = NemRestFacade(self.db_config, self.network_name) + + # Act: + mosaic = nem_rest_facade.get_mosaic(name) + + # Assert: + self.assertEqual(expected_mosaic, mosaic) + + def test_retrieve_mosaic_by_name(self): + self._assert_can_retrieve_mosaic('dragon.dragonfly', EXPECTED_MOSAIC_1) + + def test_returns_none_for_nonexistent_mosaic(self): + self._assert_can_retrieve_mosaic('non_existing_mosaic', None) + + # endregion diff --git a/explorer/rest/tests/model/test_Mosaic.py b/explorer/rest/tests/model/test_Mosaic.py new file mode 100644 index 000000000..69bcfb007 --- /dev/null +++ b/explorer/rest/tests/model/test_Mosaic.py @@ -0,0 +1,115 @@ +import unittest + +from symbolchain.nem.Network import Address + +from rest.model.Mosaic import MosaicView + + +class MosaicTest(unittest.TestCase): + @staticmethod + def _create_default_mosaic_view(override=None): + mosaic_view = MosaicView( + 'dragonfly', + 'dragon', + 'sample information', + Address('NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'), + 2, + '2015-03-29 20:34:19', + 100, + 1000, + 0, + False, + True, + 'percentile', + 'nem.xem', + 15, + Address('NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'), + 2, + '2015-03-29 20:34:19', + 525602 + ) + + if override: + setattr(mosaic_view, override[0], override[1]) + + return mosaic_view + + def test_can_create_mosaic_view(self): + # Act: + mosaic_view = self._create_default_mosaic_view() + + # Assert: + self.assertEqual('dragonfly', mosaic_view.mosaic_name) + self.assertEqual('dragon', mosaic_view.namespace_name) + self.assertEqual('sample information', mosaic_view.description) + self.assertEqual(Address('NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'), mosaic_view.creator) + self.assertEqual(2, mosaic_view.registered_height) + self.assertEqual('2015-03-29 20:34:19', mosaic_view.registered_timestamp) + self.assertEqual(100, mosaic_view.initial_supply) + self.assertEqual(1000, mosaic_view.total_supply) + self.assertEqual(0, mosaic_view.divisibility) + self.assertEqual(False, mosaic_view.supply_mutable) + self.assertEqual(True, mosaic_view.transferable) + self.assertEqual('percentile', mosaic_view.levy_type) + self.assertEqual('nem.xem', mosaic_view.levy_namespace) + self.assertEqual(15, mosaic_view.levy_fee) + self.assertEqual(Address('NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'), mosaic_view.levy_recipient) + self.assertEqual(2, mosaic_view.root_namespace_registered_height) + self.assertEqual('2015-03-29 20:34:19', mosaic_view.root_namespace_registered_timestamp) + self.assertEqual(525602, mosaic_view.root_namespace_expiration_height) + + def test_can_convert_to_simple_dict(self): + # Arrange: + mosaic_view = self._create_default_mosaic_view() + + # Act: + mosaic_view_dict = mosaic_view.to_dict() + + # Assert: + self.assertEqual({ + 'mosaicName': 'dragonfly', + 'namespaceName': 'dragon', + 'description': 'sample information', + 'creator': 'NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3', + 'registeredHeight': 2, + 'registeredTimestamp': '2015-03-29 20:34:19', + 'initialSupply': 100, + 'totalSupply': 1000, + 'divisibility': 0, + 'supplyMutable': False, + 'transferable': True, + 'levyType': 'percentile', + 'levyNamespace': 'nem.xem', + 'levyFee': 15, + 'levyRecipient': 'NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3', + 'rootNamespaceRegisteredHeight': 2, + 'rootNamespaceRegisteredTimestamp': '2015-03-29 20:34:19', + 'rootNamespaceExpirationHeight': 525602 + }, mosaic_view_dict) + + def test_eq_is_supported(self): + # Arrange: + mosaic_view = self._create_default_mosaic_view() + + # Assert: + self.assertEqual(mosaic_view, self._create_default_mosaic_view()) + self.assertNotEqual(mosaic_view, None) + self.assertNotEqual(mosaic_view, 'mosaic_view') + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('mosaic_name', 'xem'))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('namespace_name', 'nem'))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('description', 'no description'))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('creator', 'random creator'))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('registered_height', 10))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('registered_timestamp', 'random timestamp'))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('initial_supply', 99))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('total_supply', 99))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('divisibility', 6))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('supply_mutable', True))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('transferable', False))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('levy_type', None))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('levy_namespace', None))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('levy_fee', None))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('levy_recipient', None))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('root_namespace_registered_height', 4))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('root_namespace_registered_timestamp', 'random timestamp'))) + self.assertNotEqual(mosaic_view, self._create_default_mosaic_view(('root_namespace_expiration_height', 10))) diff --git a/explorer/rest/tests/test/DatabaseTestUtils.py b/explorer/rest/tests/test/DatabaseTestUtils.py index 6a7b7fd19..c29807b13 100644 --- a/explorer/rest/tests/test/DatabaseTestUtils.py +++ b/explorer/rest/tests/test/DatabaseTestUtils.py @@ -7,6 +7,7 @@ from rest.db.NemDatabase import NemDatabase from rest.model.Block import BlockView +from rest.model.Mosaic import MosaicView from rest.model.Namespace import NamespaceView Block = namedtuple( @@ -52,6 +53,7 @@ 'levy_recipient' ] ) + DatabaseConfig = namedtuple('DatabaseConfig', ['database', 'user', 'password', 'host', 'port']) # region test data @@ -110,10 +112,10 @@ 0, False, True, - None, - None, - None, - None + 2, + 'nem.xem', + 15000, + 'NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3' ) ] @@ -147,7 +149,30 @@ 'registeredHeight': 2, 'registeredTimestamp': '2015-03-29 20:34:19' }] - ), + ) +] + +MOSAIC_VIEWS = [ + MosaicView( + 'dragonfly', + 'dragon', + MOSAICS[0].description, + Address('NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'), + MOSAICS[0].registered_height, + '2015-03-29 20:34:19', + MOSAICS[0].initial_supply, + MOSAICS[0].total_supply, + MOSAICS[0].divisibility, + MOSAICS[0].supply_mutable, + MOSAICS[0].transferable, + 'percentile', + 'nem.xem', + 0.015, + Address('NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3'), + 2, + '2015-03-29 20:34:19', + 525602 + ) ] # endregion diff --git a/explorer/rest/tests/test_rest.py b/explorer/rest/tests/test_rest.py index 1c29ed874..67f3540d8 100644 --- a/explorer/rest/tests/test_rest.py +++ b/explorer/rest/tests/test_rest.py @@ -7,23 +7,10 @@ from rest import create_app -from .test.DatabaseTestUtils import BLOCK_VIEWS, NAMESPACE_VIEWS, DatabaseConfig, initialize_database +from .test.DatabaseTestUtils import BLOCK_VIEWS, MOSAIC_VIEWS, NAMESPACE_VIEWS, DatabaseConfig, initialize_database DATABASE_CONFIG_INI = 'db_config.ini' -# region test data - -EXPECTED_BLOCK_VIEW_1 = BLOCK_VIEWS[0] - -EXPECTED_BLOCK_VIEW_2 = BLOCK_VIEWS[1] - -EXPECTED_NAMESPACE_VIEW_1 = NAMESPACE_VIEWS[0] - -EXPECTED_NAMESPACE_VIEW_2 = NAMESPACE_VIEWS[1] - - -# endregion - # region fixtures @@ -90,7 +77,7 @@ def _assert_get_api_nem_block_by_height(client, height, expected_status_code, ex def test_api_nem_block_by_height(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_block_by_height(client, 1, 200, EXPECTED_BLOCK_VIEW_1.to_dict()) + _assert_get_api_nem_block_by_height(client, 1, 200, BLOCK_VIEWS[0].to_dict()) def test_api_nem_block_non_exist(client): # pylint: disable=redefined-outer-name @@ -143,15 +130,15 @@ def test_api_nem_blocks_without_params(client): # pylint: disable=redefined-out # Assert: _assert_status_code_and_headers(response, 200) - assert [EXPECTED_BLOCK_VIEW_2.to_dict(), EXPECTED_BLOCK_VIEW_1.to_dict()] == response.json + assert [BLOCK_VIEWS[1].to_dict(), BLOCK_VIEWS[0].to_dict()] == response.json def test_api_nem_blocks_applies_limit(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_blocks(client, 200, [EXPECTED_BLOCK_VIEW_2.to_dict()], limit=1) + _assert_get_api_nem_blocks(client, 200, [BLOCK_VIEWS[1].to_dict()], limit=1) def test_api_nem_blocks_applies_offset(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_blocks(client, 200, [EXPECTED_BLOCK_VIEW_1.to_dict()], offset=1) + _assert_get_api_nem_blocks(client, 200, [BLOCK_VIEWS[0].to_dict()], offset=1) def test_api_nem_blocks_applies_min_height(client): # pylint: disable=redefined-outer-name, invalid-name @@ -159,15 +146,15 @@ def test_api_nem_blocks_applies_min_height(client): # pylint: disable=redefined def test_api_nem_blocks_applies_sorted_by_height_desc(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_blocks(client, 200, [EXPECTED_BLOCK_VIEW_2.to_dict(), EXPECTED_BLOCK_VIEW_1.to_dict()], sort='desc') + _assert_get_api_nem_blocks(client, 200, [BLOCK_VIEWS[1].to_dict(), BLOCK_VIEWS[0].to_dict()], sort='desc') def test_api_nem_blocks_applies_sorted_by_height_asc(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_blocks(client, 200, [EXPECTED_BLOCK_VIEW_1.to_dict(), EXPECTED_BLOCK_VIEW_2.to_dict()], sort='asc') + _assert_get_api_nem_blocks(client, 200, [BLOCK_VIEWS[0].to_dict(), BLOCK_VIEWS[1].to_dict()], sort='asc') def test_api_nem_blocks_with_all_params(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_blocks(client, 200, [EXPECTED_BLOCK_VIEW_2.to_dict()], limit=1, offset=1, min_height=1, sort='asc') + _assert_get_api_nem_blocks(client, 200, [BLOCK_VIEWS[1].to_dict()], limit=1, offset=1, min_height=1, sort='asc') def test_api_nem_blocks_invalid_min_height(client): # pylint: disable=redefined-outer-name, invalid-name @@ -205,7 +192,7 @@ def _assert_get_api_nem_namespace_by_name(client, name, expected_status_code, ex def test_api_nem_namespace_by_name(client): # pylint: disable=redefined-outer-name - _assert_get_api_nem_namespace_by_name(client, 'oxford', 200, EXPECTED_NAMESPACE_VIEW_1.to_dict()) + _assert_get_api_nem_namespace_by_name(client, 'oxford', 200, NAMESPACE_VIEWS[0].to_dict()) def test_api_nem_namespace_non_exist(client): # pylint: disable=redefined-outer-name @@ -239,27 +226,27 @@ def test_api_nem_namespaces_without_params(client): # pylint: disable=redefined # Assert: _assert_status_code_and_headers(response, 200) - assert [EXPECTED_NAMESPACE_VIEW_2.to_dict(), EXPECTED_NAMESPACE_VIEW_1.to_dict()] == response.json + assert [NAMESPACE_VIEWS[1].to_dict(), NAMESPACE_VIEWS[0].to_dict()] == response.json def test_api_nem_namespaces_applies_limit(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_namespaces(client, 200, [EXPECTED_NAMESPACE_VIEW_2.to_dict()], limit=1) + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[1].to_dict()], limit=1) def test_api_nem_namespaces_applies_offset(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_namespaces(client, 200, [EXPECTED_NAMESPACE_VIEW_1.to_dict()], offset=1) + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[0].to_dict()], offset=1) def test_api_nem_namespaces_applies_sorted_by_id_desc(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_namespaces(client, 200, [EXPECTED_NAMESPACE_VIEW_2.to_dict(), EXPECTED_NAMESPACE_VIEW_1.to_dict()], sort='desc') + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[1].to_dict(), NAMESPACE_VIEWS[0].to_dict()], sort='desc') def test_api_nem_namespaces_applies_sorted_by_id_asc(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_namespaces(client, 200, [EXPECTED_NAMESPACE_VIEW_1.to_dict(), EXPECTED_NAMESPACE_VIEW_2.to_dict()], sort='asc') + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[0].to_dict(), NAMESPACE_VIEWS[1].to_dict()], sort='asc') def test_api_nem_namespaces_with_all_params(client): # pylint: disable=redefined-outer-name, invalid-name - _assert_get_api_nem_namespaces(client, 200, [EXPECTED_NAMESPACE_VIEW_2.to_dict()], limit=1, offset=1, sort='asc') + _assert_get_api_nem_namespaces(client, 200, [NAMESPACE_VIEWS[1].to_dict()], limit=1, offset=1, sort='asc') def _assert_get_api_nem_namespaces_fail(client, expected_status_code, **query_params): # pylint: disable=redefined-outer-name @@ -290,3 +277,27 @@ def test_api_nem_namespaces_invalid_sort(client): # pylint: disable=redefined-o # endregion + + +# region /mosaic/ + +def _assert_get_api_nem_mosaic_by_name(client, name, expected_status_code, expected_result): # pylint: disable=redefined-outer-name + # Act: + response = client.get(f'/api/nem/mosaic/{name}') + + # Assert: + _assert_status_code_and_headers(response, expected_status_code) + assert expected_result == response.json + + +def test_api_nem_mosaic_by_name(client): # pylint: disable=redefined-outer-name + _assert_get_api_nem_mosaic_by_name(client, 'dragon.dragonfly', 200, MOSAIC_VIEWS[0].to_dict()) + + +def test_api_nem_mosaic_non_exist(client): # pylint: disable=redefined-outer-name + _assert_get_api_nem_mosaic_by_name(client, 'non_exist_mosaic', 404, { + 'message': 'Resource not found', + 'status': 404 + }) + +# endregion