From 42dbb7423cd5d650755176897d24952408d63619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 24 Nov 2021 14:06:01 -0300 Subject: [PATCH 1/3] feat: node api proxy (#92) * feat: node api proxy * feat: force query to be an empty dict when no parameters are passed * chore: add checks for invalid parameters * chore: do not retry on failure * test: patch bucket configuration * chore: docstrings --- gateways/clients/hathor_core_client.py | 9 +- gateways/node_api_gateway.py | 81 +++++- handlers/node_api.py | 251 +++++++++++++++++-- serverless.yml | 157 ++++++++++++ tests/fixtures/node_api_factory.py | 13 + tests/unit/common/__init__.py | 0 tests/unit/common/test_configuration.py | 16 ++ tests/unit/gateways/test_metadata_gateway.py | 10 + tests/unit/gateways/test_node_api_gateway.py | 146 ++++++++++- tests/unit/usecases/test_node_api.py | 111 +++++++- usecases/node_api.py | 36 +++ utils/wrappers/aws/api_gateway.py | 6 +- 12 files changed, 811 insertions(+), 25 deletions(-) create mode 100644 tests/unit/common/__init__.py create mode 100644 tests/unit/common/test_configuration.py diff --git a/gateways/clients/hathor_core_client.py b/gateways/clients/hathor_core_client.py index a188066d..8458420f 100644 --- a/gateways/clients/hathor_core_client.py +++ b/gateways/clients/hathor_core_client.py @@ -10,10 +10,15 @@ logger = get_logger() -STATUS_ENDPOINT = '/v1a/status' -TOKEN_ENDPOINT = '/v1a/thin_wallet/token' ADDRESS_BALANCE_ENDPOINT = '/v1a/thin_wallet/address_balance' ADDRESS_SEARCH_ENDPOINT = '/v1a/thin_wallet/address_search' +DASHBOARD_TX_ENDPOINT = '/v1a/dashboard_tx' +STATUS_ENDPOINT = '/v1a/status' +TOKEN_ENDPOINT = '/v1a/thin_wallet/token' +TOKEN_HISTORY_ENDPOINT = '/v1a/thin_wallet/token_history' +TRANSACTION_ENDPOINT = '/v1a/transaction' +TX_ACC_WEIGHT_ENDPOINT = '/v1a/transaction_acc_weight' +VERSION_ENDPOINT = '/v1a/version' class HathorCoreAsyncClient: diff --git a/gateways/node_api_gateway.py b/gateways/node_api_gateway.py index e7d4bfa0..d77a2d87 100644 --- a/gateways/node_api_gateway.py +++ b/gateways/node_api_gateway.py @@ -1,7 +1,17 @@ from typing import Optional from gateways.clients.cache_client import ADDRESS_BLACKLIST_COLLECTION_NAME, CacheClient -from gateways.clients.hathor_core_client import ADDRESS_BALANCE_ENDPOINT, ADDRESS_SEARCH_ENDPOINT, HathorCoreClient +from gateways.clients.hathor_core_client import ( + ADDRESS_BALANCE_ENDPOINT, + ADDRESS_SEARCH_ENDPOINT, + DASHBOARD_TX_ENDPOINT, + TOKEN_ENDPOINT, + TOKEN_HISTORY_ENDPOINT, + TRANSACTION_ENDPOINT, + TX_ACC_WEIGHT_ENDPOINT, + VERSION_ENDPOINT, + HathorCoreClient, +) class NodeApiGateway: @@ -74,3 +84,72 @@ def get_address_search( params=params, timeout=10, ) + + def get_version(self) -> Optional[dict]: + """Retrieve version information + """ + + return self.hathor_core_client.get(VERSION_ENDPOINT) + + def get_dashboard_tx(self, block: int, tx: int) -> Optional[dict]: + """Retrieve info on blocks and transaction to show on dashboard + """ + + return self.hathor_core_client.get(DASHBOARD_TX_ENDPOINT, params={'tx': tx, 'block': block}) + + def get_transaction_acc_weight(self, id: str) -> Optional[dict]: + """Retrieve acc weight for a tx + """ + + return self.hathor_core_client.get(TX_ACC_WEIGHT_ENDPOINT, params={'id': id}) + + def get_token_history( + self, + id: str, + count: int, + hash: Optional[str] = None, + page: Optional[str] = None, + timestamp: Optional[int] = None) -> Optional[dict]: + """Retrieve token history + """ + + params = {'id': id, 'count': count} + if hash: + params['hash'] = hash + params['page'] = page + params['timestamp'] = timestamp + + return self.hathor_core_client.get(TOKEN_HISTORY_ENDPOINT, params=params) + + def get_transaction(self, id: str) -> Optional[dict]: + """Retrieve transaction by id + """ + return self.hathor_core_client.get(TRANSACTION_ENDPOINT, params={'id': id}) + + def list_transactions( + self, + type: str, + count: int, + hash: Optional[str] = None, + page: Optional[str] = None, + timestamp: Optional[int] = None) -> Optional[dict]: + """Retrieve list of transactions + """ + + params = {'type': type, 'count': count} + if hash: + params['hash'] = hash + params['page'] = page + params['timestamp'] = timestamp + + return self.hathor_core_client.get(TRANSACTION_ENDPOINT, params=params) + + def get_token(self, id: str) -> Optional[dict]: + """Retrieve token by id + """ + return self.hathor_core_client.get(TOKEN_ENDPOINT, params={'id': id}) + + def list_tokens(self) -> Optional[dict]: + """Retrieve list of tokens + """ + return self.hathor_core_client.get(TOKEN_ENDPOINT) diff --git a/handlers/node_api.py b/handlers/node_api.py index c15515c7..b2fa1a74 100644 --- a/handlers/node_api.py +++ b/handlers/node_api.py @@ -7,6 +7,8 @@ from usecases.node_api import NodeApi from utils.wrappers.aws.api_gateway import ApiGateway, ApiGatewayEvent +UNKNOWN_ERROR_MSG = {"error": "unknown_error"} + @ApiGateway() def get_address_balance( @@ -14,24 +16,20 @@ def get_address_balance( _context: LambdaContext, node_api: Optional[NodeApi] = None ) -> dict: + """Get the token balance of a given address. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ node_api = node_api or NodeApi() address = event.query.get("address") if address is None: raise ApiError("invalid_parameters") response = node_api.get_address_balance(address) - if response is None: - return { - "statusCode": 503, - "body": json.dumps({"error": "unknown_error"}), - "headers": { - "Content-Type": "application/json" - } - } return { "statusCode": 200, - "body": json.dumps(response), + "body": json.dumps(response or UNKNOWN_ERROR_MSG), "headers": { "Content-Type": "application/json" } @@ -44,6 +42,10 @@ def get_address_search( _context: LambdaContext, node_api: Optional[NodeApi] = None ) -> dict: + """Get a paginated list of transactions for a given address. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ node_api = node_api or NodeApi() address = event.query.get("address") @@ -60,18 +62,233 @@ def get_address_search( response = node_api.get_address_search(address, count, page, hash, token) - if response is None: - return { - "statusCode": 503, - "body": json.dumps({"error": "unknown_error"}), - "headers": { - "Content-Type": "application/json" - } + return { + "statusCode": 200, + "body": json.dumps(response or UNKNOWN_ERROR_MSG), + "headers": { + "Content-Type": "application/json" } + } + +@ApiGateway() +def get_version( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get the node version settings. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + + response = node_api.get_version() + + return { + "statusCode": 200, + "body": json.dumps(response or UNKNOWN_ERROR_MSG), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def get_dashboard_tx( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get the txs and blocks to be shown on the dashboard. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + + block = event.query.get("block") + tx = event.query.get("tx") + + if block is None or tx is None: + raise ApiError("invalid_parameters") + + response = node_api.get_dashboard_tx(block, tx) + + return { + "statusCode": 200, + "body": json.dumps(response or UNKNOWN_ERROR_MSG), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def get_transaction_acc_weight( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get a tx accumulated weight data. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + id = event.query.get("id") + + if id is None: + raise ApiError("invalid_parameters") + + response = node_api.get_transaction_acc_weight(id) + + return { + "statusCode": 200, + "body": json.dumps(response or UNKNOWN_ERROR_MSG), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def get_token_history( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get a paginated history of transactions for a given token id. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + + id = event.query.get("id") + count = event.query.get("count") + hash = event.query.get("hash") + page = event.query.get("page") + timestamp = event.query.get("timestamp") + + if id is None or count is None: + raise ApiError("invalid_parameters") + + if hash is not None and (page is None or timestamp is None): + # If hash exists, it"s a paginated request and page is required + raise ApiError("invalid_parameters") + + response = node_api.get_token_history(id, count, hash, page, timestamp) + + return { + "statusCode": 200, + "body": json.dumps(response or {}), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def get_transaction( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get transaction details given a tx_id. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + id = event.query.get("id") + + if id is None: + raise ApiError("invalid_parameters") + response = node_api.get_transaction(id) + + return { + "statusCode": 200, + "body": json.dumps(response or UNKNOWN_ERROR_MSG), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def list_transactions( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get a pagination on blocks or transactions with details. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + + type = event.query.get("type") + count = event.query.get("count") + hash = event.query.get("hash") + page = event.query.get("page") + timestamp = event.query.get("timestamp") + + if type is None or count is None: + raise ApiError("invalid_parameters") + + if hash is not None and (page is None or timestamp is None): + # If hash exists, it"s a paginated request and page is required + raise ApiError("invalid_parameters") + + response = node_api.list_transactions(type, count, hash, page, timestamp) + + return { + "statusCode": 200, + "body": json.dumps(response or {}), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def get_token( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get token details given a token uid. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + id = event.query.get("id") + + if id is None: + raise ApiError("invalid_parameters") + response = node_api.get_token(id) + + return { + "statusCode": 200, + "body": json.dumps(response or UNKNOWN_ERROR_MSG), + "headers": { + "Content-Type": "application/json" + } + } + + +@ApiGateway() +def list_tokens( + event: ApiGatewayEvent, + _context: LambdaContext, + node_api: Optional[NodeApi] = None +) -> dict: + """Get a list of tokens with details. + + *IMPORTANT: Any changes on the parameters should be reflected on the `cacheKeyParameters` for this method. + """ + node_api = node_api or NodeApi() + response = node_api.list_tokens() return { "statusCode": 200, - "body": json.dumps(response), + "body": json.dumps(response or {}), "headers": { "Content-Type": "application/json" } diff --git a/serverless.yml b/serverless.yml index 68cf7c87..0319c6b3 100644 --- a/serverless.yml +++ b/serverless.yml @@ -195,3 +195,160 @@ functions: - name: request.querystring.token - name: request.querystring.page - name: request.querystring.hash + + node_api_get_version: + handler: handlers/node_api.get_version + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/version + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 3600 + + node_api_get_dashboard_tx: + handler: handlers/node_api.get_dashboard_tx + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/dashboard_tx + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 10 + cacheKeyParameters: + - name: request.querystring.block + - name: request.querystring.tx + + node_api_get_transaction_acc_weight: + handler: handlers/node_api.get_transaction_acc_weight + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/transaction_acc_weight + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 5 + cacheKeyParameters: + - name: request.querystring.id + + node_api_get_token_history: + handler: handlers/node_api.get_token_history + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/token_history + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 10 + cacheKeyParameters: + - name: request.querystring.id + - name: request.querystring.count + - name: request.querystring.hash + - name: request.querystring.page + - name: request.querystring.timestamp + + node_api_get_transaction: + handler: handlers/node_api.get_transaction + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/transaction + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 10 + cacheKeyParameters: + - name: request.querystring.id + + node_api_list_transactions: + handler: handlers/node_api.list_transactions + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/transactions + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 10 + cacheKeyParameters: + - name: request.querystring.type + - name: request.querystring.count + - name: request.querystring.hash + - name: request.querystring.page + - name: request.querystring.timestamp + + node_api_get_token: + handler: handlers/node_api.get_token + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/token + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 10 + cacheKeyParameters: + - name: request.querystring.id + + node_api_list_tokens: + handler: handlers/node_api.list_tokens + maximumRetryAttempts: 0 + package: + patterns: + - 'handlers/node_api.py' + layers: + - { Ref: PythonRequirementsLambdaLayer } + events: + - http: + path: node_api/tokens + method: get + cors: true + caching: + enabled: true + ttlInSeconds: 3600 diff --git a/tests/fixtures/node_api_factory.py b/tests/fixtures/node_api_factory.py index e88be1d4..a17241bc 100644 --- a/tests/fixtures/node_api_factory.py +++ b/tests/fixtures/node_api_factory.py @@ -97,3 +97,16 @@ class Params: has_more = factory.Faker('boolean') total = factory.Faker('random_int', min=1, max=999999) transactions = factory.LazyFunction(lambda: [TransactionFactory() for _ in range(fake.random_int(min=1, max=20))]) + + +class VersionResourceFactory(factory.DictFactory): + version = factory.Faker('ipv4') # not actuallt ipv4, but it generated some dot separated numbers + network = factory.Faker('word') + min_weight = factory.Faker('random_int', max=100) + min_tx_weight = factory.Faker('random_int', max=100) + min_tx_weight_coefficient = factory.Faker('pyfloat', positive=True) + token_deposit_percentage = factory.Faker('pyfloat', positive=True, max_value=1) + min_tx_weight_k = factory.Faker('random_int', max=999) + reward_spend_min_blocks = factory.Faker('random_int') + max_number_inputs = factory.Faker('random_int') + max_number_outputs = factory.Faker('random_int') diff --git a/tests/unit/common/__init__.py b/tests/unit/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/common/test_configuration.py b/tests/unit/common/test_configuration.py new file mode 100644 index 00000000..a7b8c8b7 --- /dev/null +++ b/tests/unit/common/test_configuration.py @@ -0,0 +1,16 @@ +from common.configuration import Environment + + +class TestConfiguration: + + def test_environment_props(self): + assert Environment.PROD.is_prod is True + assert Environment.PROD.is_dev is False + assert Environment.PROD.is_test is False + assert Environment.DEV.is_prod is False + assert Environment.DEV.is_dev is True + assert Environment.DEV.is_test is False + assert Environment.TEST.is_prod is False + assert Environment.TEST.is_dev is False + assert Environment.TEST.is_test is True + assert Environment.default() is Environment.DEV diff --git a/tests/unit/gateways/test_metadata_gateway.py b/tests/unit/gateways/test_metadata_gateway.py index c38f5771..ede53170 100644 --- a/tests/unit/gateways/test_metadata_gateway.py +++ b/tests/unit/gateways/test_metadata_gateway.py @@ -85,3 +85,13 @@ def test_get_dag_metadata_raises_exception(self): with raises(ConfigError, match=r'No bucket name in config'): gateway.get_dag_metadata('token-id') + + @patch('gateways.metadata_gateway.json.loads', side_effect=ValueError('mock-error')) + @patch('gateways.metadata_gateway.METADATA_BUCKET', 'metadata') + def test_get_dag_metadata_return_non_json(self, s3_client): + s3_client.load_file = MagicMock(return_value='{"foo":"bar"}') + + gateway = MetadataGateway(s3_client=s3_client) + + result = gateway._get_metadata('obj_name') + assert result is None diff --git a/tests/unit/gateways/test_node_api_gateway.py b/tests/unit/gateways/test_node_api_gateway.py index 6badc923..b1f10ee3 100644 --- a/tests/unit/gateways/test_node_api_gateway.py +++ b/tests/unit/gateways/test_node_api_gateway.py @@ -3,7 +3,7 @@ from pytest import fixture from gateways.node_api_gateway import NodeApiGateway -from tests.fixtures.node_api_factory import AddressBalanceFactory, AddressSearchFactory +from tests.fixtures.node_api_factory import AddressBalanceFactory, AddressSearchFactory, VersionResourceFactory class TestNodeApiGateway: @@ -42,6 +42,13 @@ def test_get_address_balance(self, hathor_client): assert result assert sorted(result) == sorted(obj) + @patch('gateways.node_api_gateway.ADDRESS_BALANCE_ENDPOINT', 'mock-endpoint') + def test_get_address_balance_fail(self, hathor_client): + hathor_client.get = MagicMock(return_value=None) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_address_balance('mock-address') + assert result is None + @patch('gateways.node_api_gateway.ADDRESS_SEARCH_ENDPOINT', 'mock-endpoint') def test_get_address_search(self, hathor_client): obj = AddressSearchFactory() @@ -70,3 +77,140 @@ def test_get_address_search(self, hathor_client): timeout=10) assert result assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.ADDRESS_SEARCH_ENDPOINT', 'mock-endpoint') + def test_get_address_search_fail(self, hathor_client): + hathor_client.get = MagicMock(return_value=None) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_address_search('mock-address', 99) + assert result is None + + @patch('gateways.node_api_gateway.VERSION_ENDPOINT', 'mock-endpoint') + def test_get_version(self, hathor_client): + obj = VersionResourceFactory() + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_version() + hathor_client.get.assert_called_once_with('mock-endpoint') + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.DASHBOARD_TX_ENDPOINT', 'mock-endpoint') + def test_get_dashboard_tx(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_dashboard_tx(15, 5) + hathor_client.get.assert_called_once_with('mock-endpoint', params={'tx': 5, 'block': 15}) + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.TX_ACC_WEIGHT_ENDPOINT', 'mock-endpoint') + def test_get_transaction_acc_weight(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_transaction_acc_weight('mock-txid') + hathor_client.get.assert_called_once_with('mock-endpoint', params={'id': 'mock-txid'}) + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.TOKEN_HISTORY_ENDPOINT', 'mock-endpoint') + def test_get_token_history(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_token_history('mock-id-1', 1) + hathor_client.get.assert_called_once_with( + 'mock-endpoint', params={'id': 'mock-id-1', 'count': 1}) + assert result + assert sorted(result) == sorted(obj) + + result = gateway.get_token_history('mock-id-2', 2, '123') + hathor_client.get.assert_called_with( + 'mock-endpoint', params={ + 'id': 'mock-id-2', + 'count': 2, + 'hash': '123', + 'page': None, + 'timestamp': None, + }) + assert result + assert sorted(result) == sorted(obj) + + result = gateway.get_token_history('mock-id-3', 15, 'a-hash', 'next', 123) + hathor_client.get.assert_called_with( + 'mock-endpoint', params={ + 'id': 'mock-id-3', + 'count': 15, + 'hash': 'a-hash', + 'page': 'next', + 'timestamp': 123, + }) + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.TRANSACTION_ENDPOINT', 'mock-endpoint') + def test_get_transaction(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_transaction('mock-txid') + hathor_client.get.assert_called_once_with('mock-endpoint', params={'id': 'mock-txid'}) + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.TRANSACTION_ENDPOINT', 'mock-endpoint') + def test_list_transactions(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.list_transactions('type-1', 1) + hathor_client.get.assert_called_once_with( + 'mock-endpoint', params={'type': 'type-1', 'count': 1}) + assert result + assert sorted(result) == sorted(obj) + + result = gateway.list_transactions('type-2', 2, '123') + hathor_client.get.assert_called_with( + 'mock-endpoint', params={ + 'type': 'type-2', + 'count': 2, + 'hash': '123', + 'page': None, + 'timestamp': None, + }) + assert result + assert sorted(result) == sorted(obj) + + result = gateway.list_transactions('type-3', 15, 'a-hash', 'next', 123) + hathor_client.get.assert_called_with( + 'mock-endpoint', params={ + 'type': 'type-3', + 'count': 15, + 'hash': 'a-hash', + 'page': 'next', + 'timestamp': 123, + }) + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.TOKEN_ENDPOINT', 'mock-endpoint') + def test_get_token(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.get_token('mock-token-uid') + hathor_client.get.assert_called_once_with('mock-endpoint', params={'id': 'mock-token-uid'}) + assert result + assert sorted(result) == sorted(obj) + + @patch('gateways.node_api_gateway.TOKEN_ENDPOINT', 'mock-endpoint') + def test_list_tokens(self, hathor_client): + obj = {'foo': 'bar'} + hathor_client.get = MagicMock(return_value=obj) + gateway = NodeApiGateway(hathor_core_client=hathor_client) + result = gateway.list_tokens() + hathor_client.get.assert_called_once_with('mock-endpoint') + assert result + assert sorted(result) == sorted(obj) diff --git a/tests/unit/usecases/test_node_api.py b/tests/unit/usecases/test_node_api.py index 435d31c2..0fe27a7b 100644 --- a/tests/unit/usecases/test_node_api.py +++ b/tests/unit/usecases/test_node_api.py @@ -3,7 +3,7 @@ from pytest import fixture, raises from common.errors import HathorCoreTimeout -from tests.fixtures.node_api_factory import AddressBalanceFactory, AddressSearchFactory +from tests.fixtures.node_api_factory import AddressBalanceFactory, AddressSearchFactory, VersionResourceFactory from usecases.node_api import NodeApi @@ -26,6 +26,16 @@ def test_address_balance_ok(self, node_api_gateway): assert result assert sorted(result) == sorted(obj) + def test_address_balance_none(self, node_api_gateway): + node_api_gateway.is_blacklisted_address = MagicMock(return_value=False) + node_api_gateway.get_address_balance = MagicMock(return_value=None) + node_api = NodeApi(node_api_gateway) + result = node_api.get_address_balance('fake-address') + node_api_gateway.is_blacklisted_address.assert_called_once_with('fake-address') + node_api_gateway.get_address_balance.assert_called_once_with('fake-address') + node_api_gateway.blacklist_address.assert_not_called() + assert result is None + def test_address_balance_fail(self, node_api_gateway): obj = AddressBalanceFactory(fail=True) node_api_gateway.is_blacklisted_address = MagicMock(return_value=False) @@ -96,6 +106,16 @@ def test_address_search_ok(self, node_api_gateway): assert result assert sorted(result) == sorted(obj) + def test_address_search_none(self, node_api_gateway): + node_api_gateway.is_blacklisted_address = MagicMock(return_value=False) + node_api_gateway.get_address_search = MagicMock(return_value=None) + node_api = NodeApi(node_api_gateway) + result = node_api.get_address_search('fake-address', 50) + node_api_gateway.is_blacklisted_address.assert_called_once_with('fake-address') + node_api_gateway.get_address_search.assert_called_once_with('fake-address', 50, None, None, None) + node_api_gateway.blacklist_address.assert_not_called() + assert result is None + def test_address_search_fail(self, node_api_gateway): obj = AddressSearchFactory(fail=True) node_api_gateway.is_blacklisted_address = MagicMock(return_value=False) @@ -144,3 +164,92 @@ def test_address_search_reraise(self, node_api_gateway): node_api.get_address_search('fake-address', 15) node_api_gateway.is_blacklisted_address.assert_called_once_with('fake-address') + + +class TestNodeApiCommon: + + @fixture + def node_api_gateway(self): + return MagicMock() + + def test_version(self, node_api_gateway): + obj = VersionResourceFactory() + node_api_gateway.get_version = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.get_version() + node_api_gateway.get_version.assert_called_once() + assert result + assert sorted(result) == sorted(obj) + + def test_dashboard_tx(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.get_dashboard_tx = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.get_dashboard_tx(50, 4) + node_api_gateway.get_dashboard_tx.assert_called_once_with(50, 4) + assert result + assert sorted(result) == sorted(obj) + + def test_get_transaction_acc_weight(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.get_transaction_acc_weight = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.get_transaction_acc_weight('mock-txid') + node_api_gateway.get_transaction_acc_weight.assert_called_once_with('mock-txid') + assert result + assert sorted(result) == sorted(obj) + + def test_get_token_history(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.get_token_history = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.get_token_history('mock-token-uid', 1) + node_api_gateway.get_token_history.assert_called_once_with('mock-token-uid', 1, None, None, None) + assert result + assert sorted(result) == sorted(obj) + + result = node_api.get_token_history('mock-token-uid-2', 10, 'a-hash', 'a-page', 8765) + node_api_gateway.get_token_history.assert_called_with('mock-token-uid-2', 10, 'a-hash', 'a-page', 8765) + assert result + assert sorted(result) == sorted(obj) + + def test_get_transaction(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.get_transaction = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.get_transaction('mock-tx-id') + node_api_gateway.get_transaction.assert_called_once_with('mock-tx-id') + assert result + assert sorted(result) == sorted(obj) + + def test_list_transactions(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.list_transactions = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.list_transactions('mock-tx-id', 27) + node_api_gateway.list_transactions.assert_called_once_with('mock-tx-id', 27, None, None, None) + assert result + assert sorted(result) == sorted(obj) + + result = node_api.list_transactions('mock-tx-id-2', 81, 'a-hash', 'a-page', 8765) + node_api_gateway.list_transactions.assert_called_with('mock-tx-id-2', 81, 'a-hash', 'a-page', 8765) + assert result + assert sorted(result) == sorted(obj) + + def test_get_token(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.get_token = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.get_token('mock-tx-id') + node_api_gateway.get_token.assert_called_once_with('mock-tx-id') + assert result + assert sorted(result) == sorted(obj) + + def test_list_tokens(self, node_api_gateway): + obj = {"foo": "bar"} + node_api_gateway.list_tokens = MagicMock(return_value=obj) + node_api = NodeApi(node_api_gateway) + result = node_api.list_tokens() + node_api_gateway.list_tokens.assert_called_once() + assert result + assert sorted(result) == sorted(obj) diff --git a/usecases/node_api.py b/usecases/node_api.py index cffbcc91..ae427cd7 100644 --- a/usecases/node_api.py +++ b/usecases/node_api.py @@ -41,3 +41,39 @@ def get_address_search( except HathorCoreTimeout: self.node_api_gateway.blacklist_address(address) return ADDRESS_BLACKLIST_RESPONSE + + def get_version(self) -> Optional[dict]: + return self.node_api_gateway.get_version() + + def get_dashboard_tx(self, block: int, tx: int) -> Optional[dict]: + return self.node_api_gateway.get_dashboard_tx(block, tx) + + def get_transaction_acc_weight(self, id: str) -> Optional[dict]: + return self.node_api_gateway.get_transaction_acc_weight(id) + + def get_token_history( + self, + id: str, + count: int, + hash: Optional[str] = None, + page: Optional[str] = None, + timestamp: Optional[int] = None) -> Optional[dict]: + return self.node_api_gateway.get_token_history(id, count, hash, page, timestamp) + + def get_transaction(self, id: str) -> Optional[dict]: + return self.node_api_gateway.get_transaction(id) + + def list_transactions( + self, + type: str, + count: int, + hash: Optional[str] = None, + page: Optional[str] = None, + timestamp: Optional[int] = None) -> Optional[dict]: + return self.node_api_gateway.list_transactions(type, count, hash, page, timestamp) + + def get_token(self, id: str) -> Optional[dict]: + return self.node_api_gateway.get_token(id) + + def list_tokens(self) -> Optional[dict]: + return self.node_api_gateway.list_tokens() diff --git a/utils/wrappers/aws/api_gateway.py b/utils/wrappers/aws/api_gateway.py index 0bf942a3..164cdcfb 100644 --- a/utils/wrappers/aws/api_gateway.py +++ b/utils/wrappers/aws/api_gateway.py @@ -21,10 +21,10 @@ def parse_body(event: dict) -> dict: class ApiGatewayEvent: def __init__(self, event: dict, context: LambdaContext) -> None: - self.query = event.get('queryStringParameters', {}) - self.path = event.get('pathParameters', {}) + self.query = event.get('queryStringParameters') or {} + self.path = event.get('pathParameters') or {} self.body = parse_body(event) - self.headers = event.get('headers', {}) + self.headers = event.get('headers') or {} class ApiGateway: From 923aedbdbca70359b1166c05a952dfcb3346fafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 25 Nov 2021 09:45:48 -0300 Subject: [PATCH 2/3] chore: define cache ttl for metadata api (#101) --- serverless.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/serverless.yml b/serverless.yml index 0319c6b3..4665ad08 100644 --- a/serverless.yml +++ b/serverless.yml @@ -149,6 +149,7 @@ functions: cors: true caching: enabled: true + ttlInSeconds: 10 cacheKeyParameters: - name: request.querystring.id From 894a764cd387d8cc2809d30de262bb80dfc6196b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 25 Nov 2021 16:30:13 -0300 Subject: [PATCH 3/3] chore: bump version 0.1.12 (#103) --- package-lock.json | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12d7e29f..b7c34886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-explorer-service", - "version": "0.1.11", + "version": "0.1.12", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2c801850..b0345018 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hathor-explorer-service", - "version": "0.1.11", + "version": "0.1.12", "description": "Hathor Explorer Service Serverless deps", "dependencies": { "serverless": "^2.44.0", diff --git a/pyproject.toml b/pyproject.toml index 47fc17de..1811063d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hathor-explorer-service" -version = "0.1.11" +version = "0.1.12" description = "" authors = ["Hathor Labs "] license = "MIT"