777 lines
20 KiB
Python
777 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from bthlabs_jsonrpc_core import exceptions, executor
|
|
from bthlabs_jsonrpc_core.registry import MethodRegistry
|
|
from bthlabs_jsonrpc_core.serializer import JSONRPCSerializer
|
|
|
|
|
|
@pytest.fixture
|
|
def single_call():
|
|
return {
|
|
'jsonrpc': '2.0',
|
|
'id': 'test',
|
|
'method': 'test',
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def batch_calls():
|
|
return [
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'id': 'test',
|
|
'method': 'test',
|
|
},
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'id': 'test2',
|
|
'method': 'test',
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def jsonrpc_error():
|
|
return exceptions.BaseJSONRPCError('I HAZ FIAL')
|
|
|
|
|
|
@pytest.fixture
|
|
def execute_context():
|
|
return executor.Executor.ExecuteContext([])
|
|
|
|
|
|
def test_CallContext_invalid_context():
|
|
# When
|
|
result = executor.Executor.CallContext.invalid_context()
|
|
|
|
# Then
|
|
assert result.method is None
|
|
assert result.handler is None
|
|
assert result.args is None
|
|
assert result.kwargs is None
|
|
|
|
|
|
def test_CallContext_is_valid_method_none(fake_handler):
|
|
# When
|
|
call_context = executor.Executor.CallContext(None, fake_handler, [], {})
|
|
|
|
# Then
|
|
assert call_context.is_valid is False
|
|
|
|
|
|
def test_CallContext_is_valid_handler_none():
|
|
# When
|
|
call_context = executor.Executor.CallContext('test', None, [], {})
|
|
|
|
# Then
|
|
assert call_context.is_valid is False
|
|
|
|
|
|
def test_CallContext_is_valid_args_none(fake_handler):
|
|
# When
|
|
call_context = executor.Executor.CallContext(
|
|
'test', fake_handler, None, {},
|
|
)
|
|
|
|
# Then
|
|
assert call_context.is_valid is False
|
|
|
|
|
|
def test_CallContext_is_valid_kwargs_none(fake_handler):
|
|
# When
|
|
call_context = executor.Executor.CallContext(
|
|
'test', fake_handler, [], None,
|
|
)
|
|
|
|
# Then
|
|
assert call_context.is_valid is False
|
|
|
|
|
|
def test_CallContext_is_valid(fake_handler):
|
|
# When
|
|
call_context = executor.Executor.CallContext('test', fake_handler, [], {})
|
|
|
|
# Then
|
|
assert call_context.is_valid is True
|
|
|
|
|
|
def test_init_default_namespace():
|
|
# When
|
|
result = executor.Executor()
|
|
|
|
# Then
|
|
assert result.namespace == MethodRegistry.DEFAULT_NAMESPACE
|
|
|
|
|
|
def test_init_custom_namespace():
|
|
# When
|
|
result = executor.Executor(namespace='testing')
|
|
|
|
# Then
|
|
assert result.namespace == 'testing'
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_list_methods(mock_shared_registry, fake_method_registry):
|
|
# Given
|
|
fake_method_registry.get_methods.return_value = ['test']
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.list_methods()
|
|
|
|
# Then
|
|
assert result == ['system.list_methods', 'test']
|
|
|
|
assert mock_shared_registry.called is True
|
|
|
|
fake_method_registry.get_methods.assert_called_with(the_executor.namespace)
|
|
|
|
|
|
def test_get_internal_handler_list_methods():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.get_internal_handler('system.list_methods')
|
|
|
|
# Then
|
|
assert result == the_executor.list_methods
|
|
|
|
|
|
def test_get_internal_handler_method_not_found():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_internal_handler('test')
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCMethodNotFoundError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
def test_deserialize_data():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.deserialize_data('"spam"')
|
|
|
|
# Then
|
|
assert result == 'spam'
|
|
|
|
|
|
def test_deserialize_data_error():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.deserialize_data(None)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCParseError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
def test_get_calls_batch(batch_calls):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.get_calls(batch_calls)
|
|
|
|
# Then
|
|
assert result == batch_calls
|
|
|
|
|
|
def test_get_calls_single(single_call):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.get_calls(single_call)
|
|
|
|
# Then
|
|
assert result == [single_call]
|
|
|
|
|
|
def test_get_calls_empty():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_calls([])
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
def test_get_call_spec_not_dict():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_call_spec(None)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
def test_get_call_spec_wihtout_jsonrpc(single_call):
|
|
# Given
|
|
single_call.pop('jsonrpc')
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_call_spec(single_call)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
def test_get_call_spec_invalid_jsonrpc(single_call):
|
|
# Given
|
|
single_call['jsonrpc'] = 'test'
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_call_spec(single_call)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
def test_get_call_spec_wihtout_method(single_call):
|
|
# Given
|
|
single_call.pop('method')
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_call_spec(single_call)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_get_call_spec_internal_method(mock_shared_registry,
|
|
single_call,
|
|
fake_handler):
|
|
# Given
|
|
single_call['method'] = 'system.list_methods'
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'get_internal_handler') as mock_get_internal_handler:
|
|
mock_get_internal_handler.return_value = fake_handler
|
|
|
|
# When
|
|
result = the_executor.get_call_spec(single_call)
|
|
|
|
# Then
|
|
assert result[0] == 'system.list_methods'
|
|
assert result[1] is fake_handler
|
|
|
|
mock_get_internal_handler.assert_called_with('system.list_methods')
|
|
|
|
assert mock_shared_registry.called is False
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_get_call_spec_registry_method(mock_shared_registry,
|
|
single_call,
|
|
fake_method_registry,
|
|
fake_handler):
|
|
# Given
|
|
fake_method_registry.get_handler.return_value = fake_handler
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'get_internal_handler') as mock_get_internal_handler:
|
|
# When
|
|
result = the_executor.get_call_spec(single_call)
|
|
|
|
# Then
|
|
assert result[0] == 'test'
|
|
assert result[1] is fake_handler
|
|
|
|
assert mock_get_internal_handler.called is False
|
|
|
|
fake_method_registry.get_handler.assert_called_with(
|
|
the_executor.namespace, 'test',
|
|
)
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_get_call_spec_method_not_found(mock_shared_registry,
|
|
single_call,
|
|
fake_method_registry):
|
|
# Given
|
|
fake_method_registry.get_handler.return_value = None
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_call_spec(single_call)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCMethodNotFoundError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_get_call_spec_invalid_params(mock_shared_registry,
|
|
single_call,
|
|
fake_method_registry,
|
|
fake_handler):
|
|
# Given
|
|
single_call['params'] = 'spam'
|
|
|
|
fake_method_registry.get_handler.return_value = fake_handler
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
_ = the_executor.get_call_spec(single_call)
|
|
except Exception as exception:
|
|
# Then
|
|
assert isinstance(exception, exceptions.JSONRPCInvalidParamsError)
|
|
else:
|
|
assert False, 'No exception raised?'
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_get_call_spec_with_args(mock_shared_registry,
|
|
single_call,
|
|
fake_method_registry,
|
|
fake_handler):
|
|
# Given
|
|
single_call['params'] = ['spam']
|
|
|
|
fake_method_registry.get_handler.return_value = fake_handler
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'enrich_args') as mock_enrich_args:
|
|
mock_enrich_args.return_value = ['spam', 'eggs']
|
|
|
|
# When
|
|
result = the_executor.get_call_spec(single_call)
|
|
|
|
# Then
|
|
assert result[2] == ['spam', 'eggs']
|
|
assert result[3] == {}
|
|
|
|
mock_enrich_args.assert_called_with(['spam'])
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_get_call_spec_with_kwargs(mock_shared_registry,
|
|
single_call,
|
|
fake_method_registry,
|
|
fake_handler):
|
|
# Given
|
|
single_call['params'] = {'spam': True}
|
|
|
|
fake_method_registry.get_handler.return_value = fake_handler
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'enrich_kwargs') as mock_enrich_kwargs:
|
|
mock_enrich_kwargs.return_value = {'spam': True, 'eggs': False}
|
|
|
|
# When
|
|
result = the_executor.get_call_spec(single_call)
|
|
|
|
# Then
|
|
assert result[2] == []
|
|
assert result[3] == {'spam': True, 'eggs': False}
|
|
|
|
mock_enrich_kwargs.assert_called_with({'spam': True})
|
|
|
|
|
|
def test_process_results(batch_calls, single_call, jsonrpc_error):
|
|
# Given
|
|
call_without_id = {**single_call}
|
|
call_without_id.pop('id')
|
|
|
|
call_results = [
|
|
(batch_calls[0], 'OK'),
|
|
(batch_calls[1], jsonrpc_error),
|
|
(call_without_id, '???'),
|
|
(call_without_id, jsonrpc_error),
|
|
]
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.process_results(call_results)
|
|
|
|
# Then
|
|
assert isinstance(result, list)
|
|
assert len(result) == 2
|
|
|
|
first_response, second_response = result
|
|
|
|
expected_first_response = {
|
|
'jsonrpc': '2.0',
|
|
'id': batch_calls[0]['id'],
|
|
'result': 'OK',
|
|
}
|
|
assert first_response == expected_first_response
|
|
|
|
expected_second_response = {
|
|
'jsonrpc': '2.0',
|
|
'id': batch_calls[1]['id'],
|
|
'error': jsonrpc_error,
|
|
}
|
|
assert second_response == expected_second_response
|
|
|
|
|
|
def test_process_results_single_call(single_call):
|
|
# Given
|
|
call_results = [
|
|
(single_call, 'OK'),
|
|
]
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.process_results(call_results)
|
|
|
|
# Then
|
|
expected_result = {
|
|
'jsonrpc': '2.0',
|
|
'id': single_call['id'],
|
|
'result': 'OK',
|
|
}
|
|
assert result == expected_result
|
|
|
|
|
|
def test_process_results_top_level_error(jsonrpc_error):
|
|
# Given
|
|
call_results = [
|
|
(None, jsonrpc_error),
|
|
]
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.process_results(call_results)
|
|
|
|
# Then
|
|
expected_result = {
|
|
'jsonrpc': '2.0',
|
|
'id': None,
|
|
'error': jsonrpc_error,
|
|
}
|
|
assert result == expected_result
|
|
|
|
|
|
def test_process_results_empty(single_call):
|
|
# Given
|
|
single_call.pop('id')
|
|
|
|
call_results = [
|
|
(single_call, 'OK'),
|
|
]
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.process_results(call_results)
|
|
|
|
# Then
|
|
assert result is None
|
|
|
|
|
|
def test_call_context_invalid_context(jsonrpc_error,
|
|
execute_context,
|
|
single_call):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
|
|
mock_get_call_spec.side_effect = jsonrpc_error
|
|
|
|
# When
|
|
with the_executor.call_context(execute_context, single_call) as result:
|
|
pass
|
|
|
|
# Then
|
|
assert result.is_valid is False
|
|
|
|
|
|
def test_call_context_handle_jsonrpc_error(fake_handler,
|
|
jsonrpc_error,
|
|
execute_context,
|
|
single_call):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
|
|
mock_get_call_spec.return_value = (
|
|
'test', fake_handler, [], {},
|
|
)
|
|
|
|
# When
|
|
with the_executor.call_context(execute_context, single_call) as _:
|
|
raise jsonrpc_error
|
|
|
|
# Then
|
|
assert len(execute_context.results) == 1
|
|
|
|
call_result = execute_context.results[0]
|
|
assert call_result[1] == jsonrpc_error
|
|
|
|
|
|
def test_call_context_handle_exception(fake_handler,
|
|
execute_context,
|
|
single_call):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
|
|
mock_get_call_spec.return_value = (
|
|
'test', fake_handler, [], {},
|
|
)
|
|
|
|
# When
|
|
with the_executor.call_context(execute_context, single_call) as _:
|
|
raise RuntimeError('I HAZ FIAL')
|
|
|
|
# Then
|
|
assert len(execute_context.results) == 1
|
|
|
|
call_result = execute_context.results[0]
|
|
assert isinstance(call_result[1], exceptions.JSONRPCInternalError)
|
|
assert call_result[1].data == 'I HAZ FIAL'
|
|
|
|
|
|
def test_call_context(fake_handler, execute_context, single_call):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
|
|
mock_get_call_spec.return_value = (
|
|
'test', fake_handler, [], {},
|
|
)
|
|
|
|
# When
|
|
with the_executor.call_context(execute_context, single_call) as result:
|
|
result.result = 'OK'
|
|
|
|
# Then
|
|
assert result.method == 'test'
|
|
assert result.handler is fake_handler
|
|
assert result.args == []
|
|
assert result.kwargs == {}
|
|
|
|
assert len(execute_context.results) == 1
|
|
|
|
expected_call_result = (single_call, 'OK')
|
|
assert execute_context.results[0] == expected_call_result
|
|
|
|
|
|
def test_execute_context_handle_jsonrpc_error(jsonrpc_error):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
with the_executor.execute_context() as result:
|
|
raise jsonrpc_error
|
|
|
|
# Then
|
|
assert result.results == [(None, jsonrpc_error)]
|
|
|
|
|
|
def test_execute_context_handle_exception():
|
|
# Given
|
|
error = RuntimeError('I HAZ FIAL')
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
try:
|
|
with the_executor.execute_context() as result:
|
|
raise error
|
|
except Exception as exception:
|
|
assert exception is error
|
|
|
|
assert result.serializer is None
|
|
|
|
|
|
def test_execute_context_handle_empty_results(single_call):
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'process_results') as mock_process_results:
|
|
with mock.patch.object(the_executor, 'serializer') as mock_serializer:
|
|
mock_process_results.return_value = None
|
|
|
|
# When
|
|
with the_executor.execute_context() as result:
|
|
result.results.append((single_call, 'OK'))
|
|
|
|
# Then
|
|
assert result.serializer is None
|
|
|
|
assert mock_serializer.called is False
|
|
|
|
|
|
def test_execute_context(fake_rpc_serializer, single_call):
|
|
# Given
|
|
fake_responses = {
|
|
'jsonrpc': '2.0',
|
|
'id': 'test',
|
|
'result': 'OK',
|
|
}
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'process_results') as mock_process_results:
|
|
with mock.patch.object(the_executor, 'serializer') as mock_serializer:
|
|
mock_process_results.return_value = fake_responses
|
|
|
|
mock_serializer.return_value = fake_rpc_serializer
|
|
|
|
# When
|
|
with the_executor.execute_context() as result:
|
|
result.results.append((single_call, 'OK'))
|
|
|
|
# Then
|
|
assert result.serializer is fake_rpc_serializer
|
|
|
|
mock_process_results.assert_called_with([(single_call, 'OK')])
|
|
|
|
mock_serializer.assert_called_with(fake_responses)
|
|
|
|
|
|
def test_enrich_args():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.enrich_args(['spam', 'eggs'])
|
|
|
|
# Then
|
|
assert result == ['spam', 'eggs']
|
|
|
|
|
|
def test_enrich_kwargs():
|
|
# Given
|
|
the_executor = executor.Executor()
|
|
|
|
# When
|
|
result = the_executor.enrich_kwargs({'spam': True, 'eggs': False})
|
|
|
|
# Then
|
|
assert result == {'spam': True, 'eggs': False}
|
|
|
|
|
|
@pytest.mark.skip('NOOP')
|
|
def test_before_call():
|
|
pass
|
|
|
|
|
|
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
|
|
def test_execute(mock_shared_registry, fake_method_registry):
|
|
# Given
|
|
fake_method_registry.get_handler.return_value = None
|
|
fake_method_registry.get_methods.return_value = []
|
|
mock_shared_registry.return_value = fake_method_registry
|
|
|
|
batch = [
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'id': 'call_1',
|
|
'method': 'system.list_methods',
|
|
'params': ['spam'],
|
|
},
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'id': 'call_2',
|
|
'method': 'idontexist',
|
|
},
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'method': 'system.list_methods',
|
|
'params': {'spam': True},
|
|
},
|
|
]
|
|
|
|
payload = json.dumps(batch)
|
|
|
|
the_executor = executor.Executor()
|
|
|
|
with mock.patch.object(the_executor, 'before_call') as mock_before_call:
|
|
# When
|
|
result = the_executor.execute(payload)
|
|
|
|
# Then
|
|
assert isinstance(result, JSONRPCSerializer)
|
|
|
|
expected_result_data = [
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'id': 'call_1',
|
|
'result': ['system.list_methods'],
|
|
},
|
|
{
|
|
'jsonrpc': '2.0',
|
|
'id': 'call_2',
|
|
'error': {
|
|
'code': exceptions.JSONRPCMethodNotFoundError.ERROR_CODE,
|
|
'message': exceptions.JSONRPCMethodNotFoundError.ERROR_MESSAGE,
|
|
},
|
|
},
|
|
]
|
|
assert result.data == expected_result_data
|
|
|
|
fake_method_registry.get_handler.assert_called_with(
|
|
'jsonrpc', 'idontexist',
|
|
)
|
|
|
|
assert mock_before_call.call_count == 2
|
|
mock_before_call.assert_any_call('system.list_methods', ['spam'], {})
|
|
mock_before_call.assert_any_call(
|
|
'system.list_methods', [], {'spam': True},
|
|
)
|