Skip to content

Commit d4afa59

Browse files
committed
test(rpcserver): Integration tests for JSON-RPC request parsing
- Add comprehensive tests covering positional and named parameters, null/omitted params, optional defaults, error codes, and response structure validation
1 parent be3936e commit d4afa59

File tree

3 files changed

+397
-30
lines changed

3 files changed

+397
-30
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
"""
2+
Tests for JSON-RPC request parsing in florestad.
3+
4+
Validates that the RPC server correctly handles:
5+
- Positional (array) parameters
6+
- Named (object) parameters
7+
- Null / omitted parameters
8+
- Default values for optional parameters
9+
- Proper JSON-RPC error codes per the spec (-32700, -32600, -32601, -32602, -32603)
10+
- HTTP status codes (400, 404, 500, 503)
11+
- Methods that require no params vs methods that require params
12+
"""
13+
14+
from test_framework import FlorestaTestFramework
15+
from test_framework.node import NodeType
16+
17+
# JSON-RPC spec error code constants
18+
PARSE_ERROR = -32700
19+
INVALID_REQUEST = -32600
20+
METHOD_NOT_FOUND = -32601
21+
INVALID_PARAMS = -32602
22+
INTERNAL_ERROR = -32603
23+
24+
25+
class RpcServerRequestParsingTest(FlorestaTestFramework):
26+
"""
27+
Test JSON-RPC request parsing, parameter extraction (positional and named),
28+
error codes, and edge cases on the florestad RPC server.
29+
"""
30+
31+
def assert_success(self, resp):
32+
"""Assert that a JSON-RPC response indicates success (HTTP 200, no error)."""
33+
self.assertEqual(resp["status_code"], 200)
34+
self.assertIsNone(resp["body"].get("error"))
35+
36+
def assert_error(
37+
self, resp, expected_status_code=None, expected_rpcerror_code=None
38+
):
39+
"""
40+
Assert that a JSON-RPC response indicates an error (non-200, error present)."""
41+
self.assertIsSome(resp["body"].get("error"))
42+
43+
if expected_status_code is None:
44+
self.assertNotEqual(resp["status_code"], 200)
45+
else:
46+
self.assertEqual(resp["status_code"], expected_status_code)
47+
48+
if expected_rpcerror_code is not None:
49+
self.assertEqual(resp["body"]["error"]["code"], expected_rpcerror_code)
50+
51+
def set_test_params(self):
52+
"""Configure test parameters with a single florestad node."""
53+
self.node = self.add_node_default_args(NodeType.FLORESTAD)
54+
55+
def run_test(self):
56+
"""Run all JSON-RPC request parsing tests."""
57+
self.run_node(self.node)
58+
59+
self.test_noparammethods_omittedparams_succeeds()
60+
self.test_noparammethods_nullparams_succeeds()
61+
self.test_noparammethods_emptyarray_succeeds()
62+
self.test_positionalparams_validargs_succeeds()
63+
self.test_namedparams_validargs_succeeds()
64+
self.test_optionalparams_omitted_usesdefaults()
65+
self.test_unknownmethod_anyparams_returnsmethodnotfound()
66+
self.test_requiredparams_missing_returnsinvalidparams()
67+
self.test_paramtypes_wrongtype_returnsinvalidparams()
68+
self.test_jsonrpcversion_invalid_returnsrejection()
69+
self.test_parammethods_omittedparams_returnserror()
70+
self.test_responsestructure_success_matchesjsonrpcspec()
71+
self.test_responsestructure_error_matchesjsonrpcspec()
72+
73+
def test_noparammethods_omittedparams_succeeds(self):
74+
"""Verify no-param methods succeed when the params field is omitted."""
75+
self.log("Test: no-param methods without params field")
76+
77+
no_param_methods = [
78+
"getbestblockhash",
79+
"getblockchaininfo",
80+
"getblockcount",
81+
"getroots",
82+
"getrpcinfo",
83+
"uptime",
84+
"getpeerinfo",
85+
"listdescriptors",
86+
]
87+
88+
for method in no_param_methods:
89+
resp = self.node.rpc.noraise_request(method)
90+
self.assert_success(resp)
91+
92+
def test_noparammethods_nullparams_succeeds(self):
93+
"""Verify no-param methods succeed when params is explicitly null."""
94+
self.log("Test: no-param methods with params: null")
95+
96+
resp = self.node.rpc.noraise_request("getblockcount", params=None)
97+
self.assert_success(resp)
98+
99+
def test_noparammethods_emptyarray_succeeds(self):
100+
"""Verify no-param methods succeed when params is an empty array."""
101+
self.log("Test: no-param methods with empty array params")
102+
103+
resp = self.node.rpc.noraise_request("getblockcount", params=[])
104+
self.assert_success(resp)
105+
106+
def test_positionalparams_validargs_succeeds(self):
107+
"""Verify methods accept valid positional (array) parameters."""
108+
self.log("Test: positional params")
109+
110+
# getblockhash with positional param: height 0
111+
resp = self.node.rpc.noraise_request("getblockhash", params=[0])
112+
self.assert_success(resp)
113+
114+
genesis_hash = resp["body"]["result"]
115+
116+
# getblockheader with positional param: genesis hash
117+
resp = self.node.rpc.noraise_request("getblockheader", params=[genesis_hash])
118+
self.assert_success(resp)
119+
120+
# getblock with positional params: hash, verbosity
121+
resp = self.node.rpc.noraise_request("getblock", params=[genesis_hash, 1])
122+
self.assert_success(resp)
123+
124+
def test_namedparams_validargs_succeeds(self):
125+
"""Verify methods accept valid named (object) parameters."""
126+
self.log("Test: named params")
127+
128+
resp = self.node.rpc.noraise_request("getblockhash", params={"block_height": 0})
129+
self.assert_success(resp)
130+
131+
genesis_hash = resp["body"]["result"]
132+
133+
resp = self.node.rpc.noraise_request(
134+
"getblockheader", params={"block_hash": genesis_hash}
135+
)
136+
self.assert_success(resp)
137+
138+
resp = self.node.rpc.noraise_request(
139+
"getblock", params={"block_hash": genesis_hash, "verbosity": 0}
140+
)
141+
self.assert_success(resp)
142+
143+
def test_optionalparams_omitted_usesdefaults(self):
144+
"""Verify omitted optional parameters fall back to their defaults."""
145+
self.log("Test: optional defaults")
146+
147+
genesis_hash = self.node.rpc.get_bestblockhash()
148+
149+
# getblock with only the required param (verbosity defaults to 1)
150+
resp_default = self.node.rpc.noraise_request("getblock", params=[genesis_hash])
151+
self.assert_success(resp_default)
152+
153+
# Result should be verbose (verbosity=1): an object, not a hex string
154+
result = resp_default["body"]["result"]
155+
self.assertIn("hash", result)
156+
self.assertIn("tx", result)
157+
158+
# Explicit verbosity=1 should match the default
159+
resp_explicit = self.node.rpc.noraise_request(
160+
"getblock", params=[genesis_hash, 1]
161+
)
162+
self.assert_success(resp_explicit)
163+
self.assertEqual(
164+
resp_default["body"]["result"], resp_explicit["body"]["result"]
165+
)
166+
167+
# getmemoryinfo with omitted default
168+
resp = self.node.rpc.noraise_request("getmemoryinfo")
169+
self.assert_success(resp)
170+
171+
# Named params: only required field, optional uses default
172+
resp = self.node.rpc.noraise_request(
173+
"getblock", params={"block_hash": genesis_hash}
174+
)
175+
self.assert_success(resp)
176+
self.assertEqual(
177+
resp_default["body"]["result"], resp_explicit["body"]["result"]
178+
)
179+
self.assertIn("hash", resp["body"]["result"])
180+
181+
def test_unknownmethod_anyparams_returnsmethodnotfound(self):
182+
"""Verify unknown methods return METHOD_NOT_FOUND (-32601)."""
183+
self.log("Test: method not found")
184+
185+
resp = self.node.rpc.noraise_request("nonexistent_method", params=[])
186+
self.assert_error(
187+
resp, expected_status_code=404, expected_rpcerror_code=METHOD_NOT_FOUND
188+
)
189+
190+
def test_requiredparams_missing_returnsinvalidparams(self):
191+
"""Verify missing required parameters return INVALID_PARAMS (-32602)."""
192+
self.log("Test: missing required params")
193+
194+
# getblockhash requires a height parameter
195+
resp = self.node.rpc.noraise_request("getblockhash", params=[])
196+
self.assert_error(
197+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
198+
)
199+
200+
# getblockheader requires a block_hash, not an int
201+
resp = self.node.rpc.noraise_request("getblockheader", params=[1])
202+
self.assert_error(
203+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
204+
)
205+
206+
# Named params: empty object means missing required fields
207+
resp = self.node.rpc.noraise_request("getblockhash", params={})
208+
self.assert_error(
209+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
210+
)
211+
212+
def test_paramtypes_wrongtype_returnsinvalidparams(self):
213+
"""Verify wrong parameter types return INVALID_PARAMS (-32602)."""
214+
self.log("Test: wrong param types")
215+
216+
# getblockhash expects a number, not a string
217+
resp = self.node.rpc.noraise_request("getblockhash", params=["not_a_number"])
218+
self.assert_error(
219+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
220+
)
221+
222+
# getblock expects a valid block hash string, not a number
223+
resp = self.node.rpc.noraise_request("getblock", params=[12345])
224+
self.assert_error(
225+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
226+
)
227+
228+
# getblock verbosity expects a number, not a string
229+
genesis_hash = self.node.rpc.get_bestblockhash()
230+
resp = self.node.rpc.noraise_request(
231+
"getblock", params=[genesis_hash, "invalid_verbosity"]
232+
)
233+
self.assert_error(
234+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
235+
)
236+
237+
def test_jsonrpcversion_invalid_returnsrejection(self):
238+
"""Verify invalid jsonrpc versions are rejected and valid ones accepted."""
239+
self.log("Test: invalid jsonrpc version")
240+
241+
resp = self.node.rpc.noraise_raw_request(
242+
{
243+
"jsonrpc": "3.0",
244+
"id": "test",
245+
"method": "getblockcount",
246+
"params": [],
247+
}
248+
)
249+
self.assert_error(
250+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_REQUEST
251+
)
252+
253+
# Valid versions ("1.0" and "2.0") should work
254+
for version in ["1.0", "2.0"]:
255+
resp = self.node.rpc.noraise_raw_request(
256+
{
257+
"jsonrpc": version,
258+
"id": "test",
259+
"method": "getblockcount",
260+
"params": [],
261+
}
262+
)
263+
self.assert_success(resp)
264+
265+
# Omitted jsonrpc field should work (pre-2.0 compat)
266+
resp = self.node.rpc.noraise_raw_request(
267+
{
268+
"id": "test",
269+
"method": "getblockcount",
270+
}
271+
)
272+
self.assert_success(resp)
273+
274+
def test_parammethods_omittedparams_returnserror(self):
275+
"""Verify methods that require params fail when params are omitted."""
276+
self.log("Test: param methods fail without params")
277+
278+
methods_needing_params = [
279+
"getblock",
280+
"getblockhash",
281+
"getblockheader",
282+
"getblockfrompeer",
283+
"getrawtransaction",
284+
"gettxout",
285+
"gettxoutproof",
286+
"findtxout",
287+
"addnode",
288+
"disconnectnode",
289+
"loaddescriptor",
290+
"sendrawtransaction",
291+
]
292+
293+
for method in methods_needing_params:
294+
resp = self.node.rpc.noraise_request(method)
295+
self.assert_error(
296+
resp, expected_status_code=400, expected_rpcerror_code=INVALID_PARAMS
297+
)
298+
299+
def test_responsestructure_success_matchesjsonrpcspec(self):
300+
"""Verify successful responses match the JSON-RPC 2.0 spec structure."""
301+
self.log("Test: success response structure")
302+
303+
resp = self.node.rpc.noraise_raw_request(
304+
{
305+
"jsonrpc": "2.0",
306+
"id": "struct_test",
307+
"method": "getblockcount",
308+
}
309+
)
310+
311+
body = resp["body"]
312+
self.assertIn("result", body)
313+
self.assertIn("id", body)
314+
self.assertEqual(body["id"], "struct_test")
315+
self.assertIsSome(body.get("result"))
316+
317+
def test_responsestructure_error_matchesjsonrpcspec(self):
318+
"""Verify error responses match the JSON-RPC 2.0 spec structure."""
319+
self.log("Test: error response structure")
320+
321+
resp = self.node.rpc.noraise_raw_request(
322+
{
323+
"jsonrpc": "2.0",
324+
"id": "struct_err",
325+
"method": "nonexistent",
326+
"params": [],
327+
}
328+
)
329+
330+
body = resp["body"]
331+
self.assertIn("error", body)
332+
self.assertIn("id", body)
333+
self.assertEqual(body["id"], "struct_err")
334+
335+
err = body["error"]
336+
self.assertIn("code", err)
337+
self.assertIn("message", err)
338+
self.assertTrue(isinstance(err["code"], int))
339+
self.assertEqual(body["id"], "struct_err")
340+
341+
342+
if __name__ == "__main__":
343+
RpcServerRequestParsingTest().main()

0 commit comments

Comments
 (0)