diff --git a/test/functional/interface_rpc.py b/test/functional/interface_rpc.py new file mode 100755 index 0000000000..f1506aa579 --- /dev/null +++ b/test/functional/interface_rpc.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018-2019 The Bitcoin Core developers +# Copyright (c) 2017-2018 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Tests some generic aspects of the RPC interface.""" + +import os +from test_framework.authproxy import JSONRPCException +from test_framework.test_framework import RavenTestFramework +from test_framework.util import assert_equal, assert_greater_than_or_equal + + +def expect_http_status(expected_http_status, expected_rpc_code, fcn, *args): + try: + fcn(*args) + raise AssertionError("Expected RPC error %d, got none" % expected_rpc_code) + except JSONRPCException as exc: + assert_equal(exc.error["code"], expected_rpc_code) + assert_equal(exc.http_status, expected_http_status) + + +class RPCInterfaceTest(RavenTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + + def test_getrpcinfo(self): + self.log.info("Testing getrpcinfo...") + + info = self.nodes[0].getrpcinfo() + assert_equal(len(info['active_commands']), 1) + + command = info['active_commands'][0] + assert_equal(command['method'], 'getrpcinfo') + assert_greater_than_or_equal(command['duration'], 0) + + + def test_batch_request(self): + self.log.info("Testing basic JSON-RPC batch request...") + + results = self.nodes[0].batch([ + # A basic request that will work fine. + {"method": "getblockcount", "id": 1}, + # Request that will fail. The whole batch request should still + # work fine. + {"method": "invalidmethod", "id": 2}, + # Another call that should succeed. + {"method": "getbestblockhash", "id": 3}, + ]) + + result_by_id = {} + for res in results: + result_by_id[res["id"]] = res + + assert_equal(result_by_id[1]['error'], None) + assert_equal(result_by_id[1]['result'], 0) + + assert_equal(result_by_id[2]['error']['code'], -32601) + assert_equal(result_by_id[2]['result'], None) + + assert_equal(result_by_id[3]['error'], None) + assert result_by_id[3]['result'] is not None + + def test_http_status_codes(self): + self.log.info("Testing HTTP status codes for JSON-RPC requests...") + + expect_http_status(404, -32601, self.nodes[0].invalidmethod) + expect_http_status(500, -8, self.nodes[0].getblockhash, 42) + + def run_test(self): + self.test_getrpcinfo() + self.test_batch_request() + self.test_http_status_codes() + + +if __name__ == '__main__': + RPCInterfaceTest().main() diff --git a/test/functional/test_framework/authproxy.py b/test/functional/test_framework/authproxy.py index 1af996d12b..2a42b540dd 100644 --- a/test/functional/test_framework/authproxy.py +++ b/test/functional/test_framework/authproxy.py @@ -37,6 +37,7 @@ import base64 import decimal +from http import HTTPStatus import http.client import json import logging @@ -51,13 +52,14 @@ class JSONRPCException(Exception): - def __init__(self, rpc_error): + def __init__(self, rpc_error, http_status=None): try: errmsg = '%(message)s (%(code)i)' % rpc_error except (KeyError, TypeError): errmsg = '' super().__init__(errmsg) self.error = rpc_error + self.http_status = http_status def encode_decimal(o): @@ -147,23 +149,30 @@ def get_request(self, *args, **argsn): def __call__(self, *args, **argsn): post_data = json.dumps(self.get_request(*args, **argsn), default=encode_decimal, ensure_ascii=self.ensure_ascii) - response = self._request('POST', self.__url.path, post_data.encode('utf-8')) + response, status = self._request('POST', self.__url.path, post_data.encode('utf-8')) if response['error'] is not None: - log.debug("--------") - log.debug("Call failed. postdata:") + log.debug("------------------------------------------------------") + log.debug("Call failed! postdata:") log.debug(post_data) - log.debug("-------") - raise JSONRPCException(response['error']) + log.debug("-----------------------------------------------------") + raise JSONRPCException(response['error'], status) elif 'result' not in response: raise JSONRPCException({ - 'code': -343, 'message': 'missing JSON-RPC result'}) + 'code': -343, 'message': 'missing JSON-RPC result'}, status) + elif status != HTTPStatus.OK: + raise JSONRPCException({ + 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) else: return response['result'] def batch(self, rpc_call_list): postdata = json.dumps(list(rpc_call_list), default=encode_decimal, ensure_ascii=self.ensure_ascii) log.debug("--> " + postdata) - return self._request('POST', self.__url.path, postdata.encode('utf-8')) + response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) + if status != HTTPStatus.OK: + raise JSONRPCException({ + 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) + return response def _get_response(self): req_start_time = time.time() @@ -188,8 +197,8 @@ def _get_response(self): content_type = http_response.getheader('Content-Type') if content_type != 'application/json': - raise JSONRPCException({ - 'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}) + raise JSONRPCException({'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}, + http_response.status) response_data = http_response.read().decode('utf8') response = json.loads(response_data, parse_float=decimal.Decimal) @@ -198,7 +207,7 @@ def _get_response(self): log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=encode_decimal, ensure_ascii=self.ensure_ascii))) else: log.debug("<-- [%.6f] %s" % (elapsed, response_data)) - return response + return response, http_response.status def __truediv__(self, relative_uri): return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index e949b22721..7ec2c4c228 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -154,6 +154,7 @@ 'rpc_signrawtransaction.py', 'wallet_resendtransactions.py', 'wallet_txn_clone.py --mineblock', + 'interface_rpc.py', 'rpc_signmessage.py', 'rpc_deprecated.py', 'wallet_coinbase_category.py',