Skip to content

Commit fc98490

Browse files
committed
refactor integration test and moving common method to shared fixtures file
1 parent 5694fae commit fc98490

4 files changed

Lines changed: 48 additions & 117 deletions

File tree

‎tests/integration/conftest.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# pylint: disable=too-many-lines
44

55
import importlib
6+
import json
67
import os
78
from collections import defaultdict
89
from collections.abc import AsyncIterator, Generator
@@ -586,6 +587,42 @@ def shutdown_integration_otel_provider(provider: TracerProvider) -> None:
586587
trace._TRACER_PROVIDER = None # pylint: disable=protected-access
587588

588589

590+
def build_a2a_request(body: dict[str, Any]) -> Request:
591+
"""Build a POST ``/a2a`` FastAPI Request wrapping a JSON-RPC body.
592+
593+
Shared by the A2A integration test modules (``test_a2a_integration.py``
594+
and ``test_compaction_a2a.py``) so the ASGI scope/receive wiring for a
595+
synthetic ``/a2a`` request isn't duplicated across files.
596+
597+
Parameters:
598+
body: JSON-RPC request payload (e.g. a ``message/send`` body) to
599+
serialize as the request body.
600+
601+
Returns:
602+
Request: FastAPI Request object whose body yields the serialized
603+
``body`` dict when read (e.g. via ``await request.body()``).
604+
"""
605+
body_bytes = json.dumps(body).encode()
606+
607+
async def receive() -> dict[str, Any]:
608+
"""Return the JSON-RPC body as a single ASGI receive event."""
609+
return {"type": "http.request", "body": body_bytes, "more_body": False}
610+
611+
return Request(
612+
scope={
613+
"type": "http",
614+
"method": "POST",
615+
"path": "/a2a",
616+
"root_path": "",
617+
"query_string": b"",
618+
"headers": [(b"content-type", b"application/json")],
619+
"scheme": "http",
620+
"server": ("localhost", 8080),
621+
},
622+
receive=receive,
623+
)
624+
625+
589626
# ==========================================
590627
# In-Memory Conversation Store
591628
# ==========================================

‎tests/integration/endpoints/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Shared helpers for conversation compaction integration tests.
1212

1313
Integration tests for the /authorized endpoint.
1414

15-
## [test_a2a_integration.py](test_authorized_endpoint.py)
15+
## [test_a2a_integration.py](test_a2a_integration.py)
1616

1717
Integration tests for the A2A JSON-RPC endpoint.
1818

‎tests/integration/endpoints/test_a2a_integration.py‎

Lines changed: 8 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, Optional
99

1010
import pytest
11-
from fastapi import HTTPException, Request, status
11+
from fastapi import HTTPException, status
1212
from ogx_client import ApiException
1313
from pytest_mock import MockerFixture
1414

@@ -27,6 +27,7 @@
2727
RHIdentityConfiguration,
2828
)
2929
from 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-
144119
def _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
229190
async 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

‎tests/integration/endpoints/test_compaction_a2a.py‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# pylint: disable=too-many-positional-arguments
55

66
import asyncio
7-
import json
87
import uuid
98
from collections.abc import AsyncIterator
109
from typing import Any
@@ -25,7 +24,7 @@
2524
from configuration import AppConfig
2625
from models.common.responses.responses_api_params import ResponsesApiParams
2726
from models.compaction import ConversationSummary
28-
from tests.integration.conftest import InMemoryConversationStore
27+
from tests.integration.conftest import InMemoryConversationStore, build_a2a_request
2928
from tests.integration.endpoints._compaction_helpers import (
3029
CONV_ID_LLAMA,
3130
DEFAULT_MODEL_RESPONSE,
@@ -77,25 +76,7 @@ def _build_a2a_request(user_input: str) -> Request:
7776
}
7877
},
7978
}
80-
body_bytes = json.dumps(body_dict).encode()
81-
82-
async def receive() -> dict[str, Any]:
83-
"""Return the pre-built body as an ASGI receive event."""
84-
return {"type": "http.request", "body": body_bytes, "more_body": False}
85-
86-
return Request(
87-
scope={
88-
"type": "http",
89-
"method": "POST",
90-
"path": "/a2a",
91-
"root_path": "",
92-
"query_string": b"",
93-
"headers": [
94-
(b"content-type", b"application/json"),
95-
],
96-
},
97-
receive=receive,
98-
)
79+
return build_a2a_request(body_dict)
9980

10081

10182
def _mock_a2a_agent(mocker: MockerFixture) -> Any:

0 commit comments

Comments
 (0)