Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[explorer/nodewatch] task: add new route #343

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions explorer/nodewatch/nodewatch/RoutesFacade.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ def html_nodes(self):
def json_nodes(self, **kwargs):
"""Returns all nodes with condition."""

role = kwargs.get('role', None)
exact_match = kwargs.get('exact_match', False)
limit = kwargs.get('limit', None)
ssl = kwargs.get('ssl', None)
order = kwargs.get('order', None)
role = kwargs.get('role')
exact_match = kwargs.get('exact_match')
limit = kwargs.get('limit')
only_ssl = kwargs.get('only_ssl')
order = kwargs.get('order')

def custom_filter(descriptor):
role_condition = True

if role is not None:
role_condition = role == descriptor.roles if exact_match else role == (role & descriptor.roles)

if ssl is not None:
if only_ssl is True:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need "is True" ?

ssl_condition = (descriptor.is_https_enabled and descriptor.is_wss_enabled)
return role_condition and ssl_condition

Expand All @@ -94,17 +94,14 @@ def custom_filter(descriptor):
if order == 'random':
random.shuffle(nodes)

if limit is not None:
nodes = nodes[:limit]

return nodes
return nodes if limit == 0 else nodes[:limit]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not limit. ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def json_node(self, filter_field, public_key):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might want to consider checking if public_key is proper public key and returning 400 if not

"""Returns a node with matching public key."""

matching_items = [item.to_json() for item in self.repository.node_descriptors if item.to_json()[filter_field] == public_key]
matching_items = [item for item in self.repository.node_descriptors if str(getattr(item, filter_field)) == public_key]

return next(iter(matching_items), None)
return next((item.to_json() for item in matching_items), None)

def json_height_chart(self):
"""Builds a JSON height chart."""
Expand Down
60 changes: 33 additions & 27 deletions explorer/nodewatch/nodewatch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pathlib import Path

from apscheduler.schedulers.background import BackgroundScheduler
from flask import Flask, jsonify, redirect, render_template, request, url_for
from flask import Flask, abort, jsonify, redirect, render_template, request, url_for
from symbolchain.CryptoTypes import Hash256
from symbolchain.nem.Network import Network as NemNetwork
from symbolchain.Network import NetworkLocator
Expand All @@ -13,14 +13,8 @@


class Field(Enum):
MAIN_PUBLIC_KEY = 'mainPublicKey'
NODE_PUBLIC_KEY = 'nodePublicKey'


def str_to_bool(value):
if value.lower() == 'true':
return True
return None
MAIN_PUBLIC_KEY = 'main_public_key'
NODE_PUBLIC_KEY = 'node_public_key'


def create_app():
Expand Down Expand Up @@ -103,38 +97,42 @@ def symbol_summary(): # pylint: disable=unused-variable
template_name, context = symbol_routes_facade.html_summary()
return render_template(template_name, **context)

def _get_json_nodes(role, exact_match, ssl, limit, order):
if ssl is not None:
ssl = str_to_bool(ssl)
def _get_json_nodes(role, exact_match, request_args):
only_ssl = None
if 'only_ssl' in request_args:
only_ssl = True

order = request_args.get('order', None)

if limit is not None:
limit = int(limit)
limit = int(request_args.get('limit', 0))

return jsonify(symbol_routes_facade.json_nodes(role=role, exact_match=exact_match, ssl=ssl, limit=limit, order=order))
return jsonify(symbol_routes_facade.json_nodes(role=role, exact_match=exact_match, only_ssl=only_ssl, limit=limit, order=order))

def _get_json_node(result):
if not result:
abort(404)

return jsonify(result)

@app.route('/api/symbol/nodes/api')
def api_symbol_nodes_api(): # pylint: disable=unused-variable
ssl = request.args.get('ssl', None)
limit = request.args.get('limit', None)
order = request.args.get('order', None)

return _get_json_nodes(2, True, ssl, limit, order)
return _get_json_nodes(2, True, request.args)

@app.route('/api/symbol/nodes/peer')
def api_symbol_nodes_peer(): # pylint: disable=unused-variable
ssl = request.args.get('ssl', None)
limit = request.args.get('limit', None)
order = request.args.get('order', None)

return _get_json_nodes(1, False, ssl, limit, order)
return _get_json_nodes(1, False, request.args)

@app.route('/api/symbol/nodes/mainPublicKey/<main_public_key>')
def api_symbol_nodes_get_main_public_key(main_public_key): # pylint: disable=unused-variable
return jsonify(symbol_routes_facade.json_node(filter_field=Field.MAIN_PUBLIC_KEY.value, public_key=main_public_key))
result = symbol_routes_facade.json_node(filter_field=Field.MAIN_PUBLIC_KEY.value, public_key=main_public_key)

return _get_json_node(result)

@app.route('/api/symbol/nodes/nodePublicKey/<node_public_key>')
def api_symbol_nodes_get_node_public_key(node_public_key): # pylint: disable=unused-variable
return jsonify(symbol_routes_facade.json_node(filter_field=Field.NODE_PUBLIC_KEY.value, public_key=node_public_key))
result = symbol_routes_facade.json_node(filter_field=Field.NODE_PUBLIC_KEY.value, public_key=node_public_key)

return _get_json_node(result)

@app.route('/api/symbol/chart/height')
def api_symbol_chart_height(): # pylint: disable=unused-variable
Expand All @@ -152,6 +150,14 @@ def inject_timestamps(): # pylint: disable=unused-variable
'last_refresh_time': routes_facade.last_refresh_time.strftime(TIMESTAMP_FORMAT)
}

@app.errorhandler(404)
def not_found(_):
response = {
'status': 404,
'message': 'Resource not found'
}
return jsonify(response), 404

def reload_all(force=False):
log.debug('reloading all data')
nem_routes_facade.reload_all(resources_path, force)
Expand Down
67 changes: 49 additions & 18 deletions explorer/nodewatch/tests/test_RoutesFacade.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def test_can_map_version_to_css_class(self):
# endregion


class SymbolRoutesFacadeTest(unittest.TestCase):
class SymbolRoutesFacadeTest(unittest.TestCase): # pylint: disable=too-many-public-methods
# region reload / refresh

def test_can_reload_all(self):
Expand Down Expand Up @@ -371,18 +371,28 @@ def test_can_generate_nodes_json(self):
facade.reload_all(Path('tests/resources'), True)

# Act:
node_descriptors = facade.json_nodes(role=1)
node_descriptors = facade.json_nodes()

# Assert: spot check names and roles
self.assertEqual(7, len(node_descriptors))
self.assertEqual(9, len(node_descriptors))
self.assertEqual(
['Apple', 'Shin-Kuma-Node', 'ibone74', 'jaguar', 'symbol.ooo maxUnlockedAccounts:100', 'xym pool', 'yasmine farm'],
[
'Allnodes250',
'Allnodes251',
'Apple',
'Shin-Kuma-Node',
'ibone74',
'jaguar',
'symbol.ooo maxUnlockedAccounts:100',
'xym pool',
'yasmine farm'
],
list(map(lambda descriptor: descriptor['name'], node_descriptors)))
self.assertEqual(
[7, 3, 3, 5, 3, 3, 3],
[2, 2, 7, 3, 3, 5, 3, 3, 3],
list(map(lambda descriptor: descriptor['roles'], node_descriptors)))

def test_can_generate_nodes_json_filtered(self):
def test_can_generate_nodes_json_filtered_by_role(self):
# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)
Expand Down Expand Up @@ -416,13 +426,13 @@ def test_can_generate_nodes_json_filtered_exact_match(self):
[2, 2],
list(map(lambda descriptor: descriptor['roles'], node_descriptors)))

def test_can_generate_nodes_json_filtered_ssl(self):
def test_can_generate_nodes_json_filtered_only_ssl(self):
# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)

# Act: select nodes with only ssl enabled
node_descriptors = facade.json_nodes(ssl=True)
node_descriptors = facade.json_nodes(only_ssl=True)

# Assert: spot check names and roles
self.assertEqual(2, len(node_descriptors))
Expand All @@ -433,26 +443,43 @@ def test_can_generate_nodes_json_filtered_ssl(self):
[2, 3],
list(map(lambda descriptor: descriptor['roles'], node_descriptors)))

def test_can_generate_nodes_json_filtered_order_random_limit_2(self):
def test_can_generate_nodes_json_filtered_order_random_subset(self):
# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)

# Act: select 2 nodes with order random
node_descriptors = facade.json_nodes(limit=2, order='random')

# returns all nodes
# Assert:
all_node_descriptors = facade.json_nodes()

full_node_names = list(map(lambda descriptor: descriptor['name'], all_node_descriptors))
random_node_names = list(map(lambda descriptor: descriptor['name'], node_descriptors))

# Assert: spot check names
self.assertEqual(2, len(node_descriptors))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should check length of set(node_descriptors) to ensure duplicates are not returned

(you should also do this for all "random" tests)

self.assertEqual(2, len(set(random_node_names)))
for name in random_node_names:
self.assertIn(name, full_node_names)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would add test with limit but without random

def test_can_generate_node_json_given_main_public_key(self):
def test_can_generate_nodes_json_filtered_limit_5(self):
# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)

# Act:
node_descriptors = facade.json_nodes(limit=5)

# Assert: spot check names
self.assertEqual(5, len(node_descriptors))
self.assertEqual(
['Allnodes250', 'Allnodes251', 'Apple', 'Shin-Kuma-Node', 'ibone74'],
list(map(lambda descriptor: descriptor['name'], node_descriptors)))
self.assertEqual(
[2, 2, 7, 3, 3],
list(map(lambda descriptor: descriptor['roles'], node_descriptors)))

def can_find_known_node_by_main_public_key(self): # pylint: disable=invalid-name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test need to start with test_ prefix or they will not be detected as tests (x4)

# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)
Expand Down Expand Up @@ -482,7 +509,7 @@ def test_can_generate_node_json_given_main_public_key(self):
# Assert:
self.assertEqual(node_descriptors, expected_node)

def test_can_generate_node_json_given_node_public_key(self):
def can_find_known_node_by_node_public_key(self): # pylint: disable=invalid-name
# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)
Expand Down Expand Up @@ -511,18 +538,22 @@ def test_can_generate_node_json_given_node_public_key(self):
# Assert:
self.assertEqual(node_descriptors, expected_node)

def test_can_generate_node_json_given_public_key_not_found(self):
def _assert_unknown_node_not_found(self, public_key):
# Arrange:
facade = SymbolRoutesFacade(SymbolNetwork.MAINNET, '<symbol_explorer>')
facade.reload_all(Path('tests/resources'), True)

# Act:
main_public_key_descriptors = facade.json_node(filter_field='mainPublicKey', public_key='invalidKey')
node_public_key_descriptors = facade.json_node(filter_field='nodePublicKey', public_key='invalidKey')
node_descriptors = facade.json_node(filter_field=public_key, public_key='invalidKey')

# Assert:
self.assertIsNone(main_public_key_descriptors)
self.assertIsNone(node_public_key_descriptors)
self.assertIsNone(node_descriptors)

def cannot_find_unknown_node_by_main_public_key(self): # pylint: disable=invalid-name
self._assert_unknown_node_not_found('mainPublicKey')

def cannot_find_unknown_node_by_node_public_key(self): # pylint: disable=invalid-name
self._assert_unknown_node_not_found('nodePublicKey')

def test_can_generate_height_chart_json(self):
# Arrange:
Expand Down
23 changes: 12 additions & 11 deletions explorer/nodewatch/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def _assert_symbol_node_response(response, expected_names):
assert 200 == response.status_code
assert 'application/json' == response.headers['Content-Type']
assert len(expected_names) == len(response_json)
assert len(expected_names) == len(set(expected_names))
assert expected_names == list(map(lambda descriptor: descriptor['name'], response_json))


Expand All @@ -181,25 +182,25 @@ def test_get_api_symbol_nodes_api(client): # pylint: disable=redefined-outer-na
_assert_symbol_node_response(response, ['Allnodes250', 'Allnodes251'])


def test_get_api_symbol_nodes_api_order_random_limit_2(client): # pylint: disable=redefined-outer-name
def test_get_api_symbol_nodes_api_order_random_subset(client): # pylint: disable=redefined-outer-name
# Act:
response = client.get('/api/symbol/nodes/api?order=random&limit=2')
response = client.get('/api/symbol/nodes/api?order=random&limit=1')
response_json = json.loads(response.data)

# Assert: spot check names
full_api_node_names = [
'Allnodes250', 'Allnodes251'
]
actual_names = list(map(lambda descriptor: descriptor['name'], response_json))

# Assert:
_assert_symbol_node_response(response, actual_names)
Jaguar0625 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert len(actual_names) == 1

for name in actual_names:
assert name in full_api_node_names


def test_get_api_symbol_nodes_api_ssl_true(client): # pylint: disable=redefined-outer-name
def test_get_api_symbol_nodes_api_only_ssl(client): # pylint: disable=redefined-outer-name
# Act:
response = client.get('/api/symbol/nodes/api?ssl=true')
response = client.get('/api/symbol/nodes/api?only_ssl')

# Assert:
_assert_symbol_node_response(response, ['Allnodes251'])
Expand All @@ -216,7 +217,7 @@ def test_get_api_symbol_nodes_peer(client): # pylint: disable=redefined-outer-n
)


def test_get_api_symbol_nodes_peer_order_random_limit_2(client): # pylint: disable=redefined-outer-name
def test_get_api_symbol_nodes_peer_order_random_subset(client): # pylint: disable=redefined-outer-name
# Act:
response = client.get('/api/symbol/nodes/peer?order=random&limit=2')
response_json = json.loads(response.data)
Expand All @@ -232,9 +233,9 @@ def test_get_api_symbol_nodes_peer_order_random_limit_2(client): # pylint: disa
assert name in full_node_names
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert length of actual names is 2



def test_get_api_symbol_nodes_peer_ssl_true(client): # pylint: disable=redefined-outer-name
def test_get_api_symbol_nodes_peer_only_ssl(client): # pylint: disable=redefined-outer-name
# Act:
response = client.get('/api/symbol/nodes/peer?ssl=true')
response = client.get('/api/symbol/nodes/peer?only_ssl')

# Assert:
_assert_symbol_node_response(response, ['xym pool'])
Expand All @@ -244,10 +245,10 @@ def _assert_api_symbol_node_with_public_key_not_found(response):
# Act:
response_json = json.loads(response.data)

# Assert: spot check names
assert 200 == response.status_code
# Assert:
assert 404 == response.status_code
assert 'application/json' == response.headers['Content-Type']
assert response_json is None
assert response_json == {'message': 'Resource not found', 'status': 404}


def _assert_api_symbol_node_with_public_key_found(response, expected_name):
Expand Down