88from typing import Any , Optional
99
1010import pytest
11- from fastapi import HTTPException , Request , status
11+ from fastapi import HTTPException , status
1212from ogx_client import ApiException
1313from pytest_mock import MockerFixture
1414
2727 RHIdentityConfiguration ,
2828)
2929from tests .integration .conftest import (
30+ build_a2a_request ,
3031 create_text_agent_stream_events ,
3132 make_openai_model ,
3233 make_openai_models_list_response ,
@@ -90,9 +91,10 @@ def configure_a2a_agent_card(test_config: AppConfig) -> None:
9091
9192 Autouse because most tests in this module go through
9293 ``_create_a2a_app``/``get_lightspeed_agent_card``, which require one.
93- It's a harmless no-op for the handful of tests (e.g. the missing-
94- credentials check) that bypass the real endpoint entirely and never
95- read ``configuration.customization``.
94+ It's a harmless no-op for the handful of tests (e.g.
95+ ``test_a2a_jsonrpc_forbidden_without_action``) that are rejected by the
96+ ``@authorize`` decorator before the real endpoint body ever runs, and so
97+ never read ``configuration.customization``.
9698 """
9799 assert test_config ._configuration is not None
98100 test_config ._configuration .customization = Customization (
@@ -114,33 +116,6 @@ def _install_agent(mocker: MockerFixture, *contents: str) -> Any:
114116 return mocker .patch ("app.endpoints.a2a.build_agent" , return_value = mock_agent )
115117
116118
117- def _a2a_request (body : dict [str , Any ] | bytes ) -> Request :
118- """Build a POST ``/a2a`` request for ``handle_a2a_jsonrpc_post``.
119-
120- ``body`` may be a JSON-RPC payload (dict) or raw bytes, so callers can
121- also exercise malformed/non-JSON request bodies.
122- """
123- body_bytes = body if isinstance (body , bytes ) else json .dumps (body ).encode ()
124-
125- async def receive () -> dict [str , Any ]:
126- """Return the JSON-RPC body."""
127- return {"type" : "http.request" , "body" : body_bytes , "more_body" : False }
128-
129- return Request (
130- scope = {
131- "type" : "http" ,
132- "method" : "POST" ,
133- "path" : "/a2a" ,
134- "root_path" : "" ,
135- "query_string" : b"" ,
136- "headers" : [(b"content-type" , b"application/json" )],
137- "scheme" : "http" ,
138- "server" : ("localhost" , 8080 ),
139- },
140- receive = receive ,
141- )
142-
143-
144119def _jsonrpc_body (
145120 text : str ,
146121 * ,
@@ -185,10 +160,10 @@ def _context_id(result: dict[str, Any]) -> str:
185160 return result .get ("contextId" , "" )
186161
187162
188- async def _post_a2a (body : dict [str , Any ] | bytes , auth : AuthTuple ) -> Any :
163+ async def _post_a2a (body : dict [str , Any ], auth : AuthTuple ) -> Any :
189164 """POST a JSON-RPC request to the A2A handler."""
190165 return await a2a_endpoint .handle_a2a_jsonrpc_post (
191- request = _a2a_request (body ),
166+ request = build_a2a_request (body ),
192167 auth = auth ,
193168 mcp_headers = {},
194169 )
@@ -211,20 +186,6 @@ def _jsonrpc_result(response: Any) -> dict[str, Any]:
211186 return result
212187
213188
214- def _jsonrpc_error (response : Any ) -> dict [str , Any ]:
215- """Decode a JSON-RPC error result and return the ``error`` object.
216-
217- Per the JSON-RPC 2.0 spec (https://www.jsonrpc.org/specification),
218- error responses are still delivered over HTTP 200; the error details
219- (including the standard ``code``) live in the JSON body.
220- """
221- payload = _parse_jsonrpc_response (response )
222- assert payload .get ("error" ), payload
223- error = payload ["error" ]
224- assert isinstance (error , dict ), payload
225- return error
226-
227-
228189@pytest .mark .asyncio
229190async def test_a2a_jsonrpc_forbidden_without_action (
230191 test_config : AppConfig ,
@@ -367,51 +328,3 @@ async def test_message_send_without_text_returns_input_required(
367328 # unlike the Python enum member name (TaskState.input_required).
368329 assert _task_state (result ) == "input-required"
369330 build_agent .assert_not_called ()
370-
371-
372- @pytest .mark .asyncio
373- @pytest .mark .parametrize (
374- ("body" , "expected_code" ),
375- [
376- pytest .param (b"{not valid json" , - 32700 , id = "malformed-json-parse-error" ),
377- pytest .param (
378- {"jsonrpc" : "2.0" , "id" : "1" , "params" : {}},
379- - 32600 ,
380- id = "missing-method-invalid-request" ,
381- ),
382- pytest .param (
383- {
384- "jsonrpc" : "2.0" ,
385- "id" : "1" ,
386- "method" : "message/does-not-exist" ,
387- "params" : {},
388- },
389- - 32601 ,
390- id = "unknown-method-not-found" ,
391- ),
392- pytest .param (
393- {"jsonrpc" : "2.0" , "id" : "1" , "method" : "message/send" },
394- - 32602 ,
395- id = "message-send-missing-params-invalid-params" ,
396- ),
397- ],
398- )
399- async def test_a2a_jsonrpc_rejects_malformed_requests (
400- test_auth : AuthTuple ,
401- body : dict [str , Any ] | bytes ,
402- expected_code : int ,
403- ) -> None :
404- """Malformed JSON-RPC requests get the standard JSON-RPC 2.0 error code.
405-
406- These codes are not project-specific: they come straight from the public
407- JSON-RPC 2.0 spec (https://www.jsonrpc.org/specification, section on
408- error objects) and are produced by the ``a2a-sdk``'s own request
409- validation in ``JSONRPCApplication._handle_requests`` (base
410- ``JSONRPCRequest`` validation for -32600, method lookup for -32601,
411- method-specific param validation, e.g. ``SendMessageRequest``, for
412- -32602, and a JSON parse failure for -32700) -- this project doesn't
413- define or override any of that error handling, it's inherited verbatim
414- from the SDK.
415- """
416- error = _jsonrpc_error (await _post_a2a (body , test_auth ))
417- assert error ["code" ] == expected_code
0 commit comments