# -*- 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}, )