Repository: bitcoin/bitcoin
File: test/functional/interface_rpc.py (Python)

Code before:
#!/usr/bin/env python3
# Copyright (c) 2018-2022 The Bitcoin 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 BitcoinTestFramework
from test_framework.util import assert_equal, assert_greater_than_or_equal
from threading import Thread
import subprocess


def expect_http_status(expected_http_status, expected_rpc_code,
                       fcn, *args):
    try:
        fcn(*args)
        raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    except JSONRPCException as exc:
        assert_equal(exc.error["code"], expected_rpc_code)
        assert_equal(exc.http_status, expected_http_status)


def test_work_queue_getblock(node, got_exceeded_error):
    while not got_exceeded_error:
        try:
            node.cli("waitfornewblock", "500").send_cli()
        except subprocess.CalledProcessError as e:
            assert_equal(e.output, 'error: Server response: Work queue depth exceeded\n')
            got_exceeded_error.append(True)


class RPCInterfaceTest(BitcoinTestFramework):
    def set_test_params(self):
        self.num_nodes = 1
        self.setup_clean_chain = True
        self.supports_cli = False

    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)
        assert_equal(info['logpath'], os.path.join(self.nodes[0].chain_path, 'debug.log'))

    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": "getblockhash", "id": 3, "params": [0]},
        ])

        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 test_work_queue_exceeded(self):
        self.log.info("Testing work queue exceeded...")
        self.restart_node(0, ['-rpcworkqueue=1', '-rpcthreads=1'])
        got_exceeded_error = []
        threads = []
        for _ in range(3):
            t = Thread(target=test_work_queue_getblock, args=(self.nodes[0], got_exceeded_error))
            t.start()
            threads.append(t)
        for t in threads:
            t.join()

    def run_test(self):
        self.test_getrpcinfo()
        self.test_batch_request()
        self.test_http_status_codes()
        self.test_work_queue_exceeded()


if __name__ == '__main__':
    RPCInterfaceTest().main()


Review comment (suggestion):
Note that we do actually test json-rpc error code `-32602` in a different test, which might be worth documenting here?

https://github.com/bitcoin/bitcoin/blob/1cc1c7210696e3ea758ea5e390eafe824cd06a9e/test/functional/p2p_disconnect_ban.py#L111

Similarly `-32603` is tested here:

https://github.com/bitcoin/bitcoin/blob/1cc1c7210696e3ea758ea5e390eafe824cd06a9e/test/functional/feature_fee_estimation.py#L335

Code after:
        ]

        request = []
        response = []
        for idx, (call, result) in enumerate(zip(calls, results), 1):
            options = call_options(idx)
            if options is None:
                continue
            request.append(format_request(options, idx, call))
            r = format_response(options, idx, result)
            if r is not None:
                response.append(r)

        rpc_response, http_status = send_json_rpc(self.nodes[0], request)
        if len(response) == 0 and len(request) > 0:
            assert_equal(http_status, 204)
            assert_equal(rpc_response, None)
        else:
            assert_equal(http_status, 200)
            assert_equal(rpc_response, response)

    def test_batch_requests(self):
        self.log.info("Testing empty batch request...")
        self.test_batch_request(lambda idx: None)

        self.log.info("Testing basic JSON-RPC 2.0 batch request...")
        self.test_batch_request(lambda idx: BatchOptions(version=2))

        self.log.info("Testing JSON-RPC 2.0 batch with notifications...")
        self.test_batch_request(lambda idx: BatchOptions(version=2, notification=idx < 2))

        self.log.info("Testing JSON-RPC 2.0 batch of ALL notifications...")
        self.test_batch_request(lambda idx: BatchOptions(version=2, notification=True))

        # JSONRPC 1.1 does not support batch requests, but test them for backwards compatibility.
        self.log.info("Testing nonstandard JSON-RPC 1.1 batch request...")
        self.test_batch_request(lambda idx: BatchOptions(version=1))

        self.log.info("Testing nonstandard mixed JSON-RPC 1.1/2.0 batch request...")
        self.test_batch_request(lambda idx: BatchOptions(version=2 if idx % 2 else 1))

        self.log.info("Testing nonstandard batch request without version numbers...")
        self.test_batch_request(lambda idx: BatchOptions())

        self.log.info("Testing nonstandard batch request without version numbers or ids...")
        self.test_batch_request(lambda idx: BatchOptions(notification=True))

        self.log.info("Testing nonstandard jsonrpc 1.0 version number is accepted...")
        self.test_batch_request(lambda idx: BatchOptions(request_fields={"jsonrpc": "1.0"}))

        self.log.info("Testing unrecognized jsonrpc version number is rejected...")