Skip to content

Commit 248817b

Browse files
test: cover xdrFormat param and JSON-RPC batch requests
Encode the expected behavior for the RPC plumbing work: - getTransaction and sendTransaction accept an optional xdrFormat param (per the stellar-rpc protocol structs): 'base64' behaves as the default, 'json' is rejected as unsupported with -32602, and any other value (or a non-string) is -32602. Rejection must happen before the transaction executes. - JSON-RPC 2.0 batch requests: an array of requests yields an array of responses matched by id; an empty array is a single Invalid Request error; invalid batch elements each get a -32600 response with id null; notifications get no response, and an all-notifications batch yields an empty body. The non-object-frame test now uses a JSON string body, since an array body is a batch and no longer an Invalid Request. The new tests fail until the feature lands; the existing suite still passes.
1 parent 8f392c5 commit 248817b

1 file changed

Lines changed: 166 additions & 3 deletions

File tree

src/tests/integration/test_server.py

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,18 @@ def _rpc(port: int, method: str, params: dict[str, Any]) -> dict[str, Any]:
4848
return _post(port, body)
4949

5050

51-
def _post(port: int, body: bytes) -> dict[str, Any]:
51+
def _post(port: int, body: bytes) -> Any:
52+
return json.loads(_post_raw(port, body))
53+
54+
55+
def _post_raw(port: int, body: bytes) -> bytes:
5256
req = urllib.request.Request(
5357
f'http://localhost:{port}',
5458
data=body,
5559
headers={'Content-Type': 'application/json'},
5660
)
5761
with urllib.request.urlopen(req) as resp:
58-
return json.loads(resp.read())
62+
return resp.read()
5963

6064

6165
@pytest.fixture
@@ -138,7 +142,7 @@ def test_malformed_body_returns_parse_error(server: StellarRpcServer) -> None:
138142

139143

140144
def test_non_object_frame_returns_invalid_request(server: StellarRpcServer) -> None:
141-
result = _post(server.port(), b'[1, 2, 3]')
145+
result = _post(server.port(), b'"just a string"')
142146
assert result['error']['code'] == -32600
143147

144148

@@ -498,3 +502,162 @@ def builder() -> TransactionBuilder:
498502

499503
# All four transactions, including the non-Void invocation, advanced the ledger.
500504
assert _rpc(server.port(), 'getLatestLedger', {})['result']['sequence'] == 4
505+
506+
507+
# ----------------------------------------------------------------------
508+
# xdrFormat parameter (getTransaction / sendTransaction)
509+
#
510+
# Real stellar-rpc accepts an optional `xdrFormat` param on both methods
511+
# (protocols/rpc: GetTransactionRequest.Format, SendTransactionRequest.Format)
512+
# and rejects invalid values with InvalidParams (-32602). komet-node supports
513+
# only 'base64' (the default); 'json' is rejected with a clear -32602 error.
514+
# ----------------------------------------------------------------------
515+
516+
517+
def _create_account_xdr() -> str:
518+
"""A freshly signed CreateAccount transaction envelope, base64 XDR."""
519+
keypair = Keypair.random()
520+
account = Account(keypair.public_key, sequence=0)
521+
envelope = (
522+
TransactionBuilder(account, Network.TESTNET_NETWORK_PASSPHRASE)
523+
.append_create_account_op(destination=keypair.public_key, starting_balance='1000')
524+
.set_timeout(30)
525+
.build()
526+
)
527+
envelope.sign(keypair)
528+
return envelope.to_xdr()
529+
530+
531+
def test_xdr_format_base64_behaves_as_default(server: StellarRpcServer) -> None:
532+
"""xdrFormat 'base64' is the explicit spelling of the default on both methods."""
533+
send_result = _rpc(server.port(), 'sendTransaction', {'transaction': _create_account_xdr(), 'xdrFormat': 'base64'})
534+
assert send_result['result']['status'] == 'PENDING'
535+
tx_hash = send_result['result']['hash']
536+
537+
get_result = _rpc(server.port(), 'getTransaction', {'hash': tx_hash, 'xdrFormat': 'base64'})
538+
assert get_result['result']['status'] == 'SUCCESS'
539+
540+
541+
def test_get_transaction_xdr_format_json_returns_invalid_params(server: StellarRpcServer) -> None:
542+
"""komet-node does not support the JSON XDR format: reject with a clear -32602 error."""
543+
result = _rpc(server.port(), 'getTransaction', {'hash': '0' * 64, 'xdrFormat': 'json'})
544+
assert result['error']['code'] == -32602
545+
assert 'json' in result['error']['message'].lower()
546+
547+
548+
def test_get_transaction_xdr_format_invalid_value_returns_invalid_params(server: StellarRpcServer) -> None:
549+
result = _rpc(server.port(), 'getTransaction', {'hash': '0' * 64, 'xdrFormat': 'yaml'})
550+
assert result['error']['code'] == -32602
551+
552+
553+
def test_get_transaction_xdr_format_non_string_returns_invalid_params(server: StellarRpcServer) -> None:
554+
result = _rpc(server.port(), 'getTransaction', {'hash': '0' * 64, 'xdrFormat': 42})
555+
assert result['error']['code'] == -32602
556+
557+
558+
def test_send_transaction_xdr_format_json_returns_invalid_params_without_executing(server: StellarRpcServer) -> None:
559+
"""An unsupported xdrFormat is rejected before the transaction runs: no state change."""
560+
result = _rpc(server.port(), 'sendTransaction', {'transaction': _create_account_xdr(), 'xdrFormat': 'json'})
561+
assert result['error']['code'] == -32602
562+
assert 'json' in result['error']['message'].lower()
563+
564+
# The transaction must not have executed: no receipt written, ledger not advanced.
565+
assert list((server.io_dir / 'receipts').iterdir()) == []
566+
assert _rpc(server.port(), 'getLatestLedger', {})['result']['sequence'] == 0
567+
568+
569+
def test_send_transaction_xdr_format_invalid_value_returns_invalid_params(server: StellarRpcServer) -> None:
570+
result = _rpc(server.port(), 'sendTransaction', {'transaction': _create_account_xdr(), 'xdrFormat': 'yaml'})
571+
assert result['error']['code'] == -32602
572+
assert list((server.io_dir / 'receipts').iterdir()) == []
573+
574+
575+
# ----------------------------------------------------------------------
576+
# JSON-RPC 2.0 batch requests
577+
#
578+
# Per JSON-RPC 2.0 section 6: an array of request objects yields an array of
579+
# response objects (matched by id, order not significant); an empty array is a
580+
# single Invalid Request error; invalid batch elements each yield an Invalid
581+
# Request error with id null; notifications (no id) get no response, and a
582+
# batch of only notifications yields no response body at all.
583+
# ----------------------------------------------------------------------
584+
585+
586+
def _batch(port: int, requests: list[dict[str, Any]]) -> Any:
587+
return _post(port, json.dumps(requests).encode())
588+
589+
590+
def test_batch_request_returns_array_of_responses(server: StellarRpcServer) -> None:
591+
responses = _batch(
592+
server.port(),
593+
[
594+
{'jsonrpc': '2.0', 'id': 1, 'method': 'getHealth', 'params': {}},
595+
{'jsonrpc': '2.0', 'id': 2, 'method': 'getLatestLedger', 'params': {}},
596+
],
597+
)
598+
assert isinstance(responses, list)
599+
assert len(responses) == 2
600+
by_id = {response['id']: response for response in responses}
601+
assert set(by_id) == {1, 2}
602+
for response in responses:
603+
assert response['jsonrpc'] == '2.0'
604+
assert by_id[1]['result']['status'] == 'healthy'
605+
assert by_id[2]['result']['sequence'] == 0
606+
607+
608+
def test_empty_batch_returns_single_invalid_request(server: StellarRpcServer) -> None:
609+
"""An empty array is not a valid batch: one Invalid Request error object, not an array."""
610+
result = _post(server.port(), b'[]')
611+
assert isinstance(result, dict)
612+
assert result['error']['code'] == -32600
613+
assert result['id'] is None
614+
615+
616+
def test_batch_of_invalid_elements_returns_error_per_element(server: StellarRpcServer) -> None:
617+
"""rpc call with an invalid batch: one Invalid Request response per element, id null."""
618+
responses = _post(server.port(), b'[1, 2, 3]')
619+
assert isinstance(responses, list)
620+
assert len(responses) == 3
621+
for response in responses:
622+
assert response['jsonrpc'] == '2.0'
623+
assert response['error']['code'] == -32600
624+
assert response['id'] is None
625+
626+
627+
def test_batch_mixed_valid_and_invalid_elements(server: StellarRpcServer) -> None:
628+
responses = _batch(
629+
server.port(),
630+
[
631+
{'jsonrpc': '2.0', 'id': 1, 'method': 'getHealth', 'params': {}},
632+
{'foo': 'boo'},
633+
{'jsonrpc': '2.0', 'id': 2, 'method': 'noSuchMethod', 'params': {}},
634+
],
635+
)
636+
assert isinstance(responses, list)
637+
assert len(responses) == 3
638+
by_id = {response['id']: response for response in responses}
639+
assert by_id[1]['result']['status'] == 'healthy'
640+
assert by_id[None]['error']['code'] == -32600
641+
assert by_id[2]['error']['code'] == -32601
642+
643+
644+
def test_batch_notification_gets_no_response(server: StellarRpcServer) -> None:
645+
"""A request without an id is a notification: it is executed but not answered."""
646+
responses = _batch(
647+
server.port(),
648+
[
649+
{'jsonrpc': '2.0', 'id': 1, 'method': 'getHealth', 'params': {}},
650+
{'jsonrpc': '2.0', 'method': 'getHealth', 'params': {}}, # notification
651+
],
652+
)
653+
assert isinstance(responses, list)
654+
assert len(responses) == 1
655+
assert responses[0]['id'] == 1
656+
assert responses[0]['result']['status'] == 'healthy'
657+
658+
659+
def test_batch_of_only_notifications_returns_nothing(server: StellarRpcServer) -> None:
660+
"""If every batch element is a notification, the server must not return an empty array."""
661+
body = json.dumps([{'jsonrpc': '2.0', 'method': 'getHealth', 'params': {}}]).encode()
662+
raw = _post_raw(server.port(), body)
663+
assert raw.strip() == b''

0 commit comments

Comments
 (0)