Skip to content
1 change: 1 addition & 0 deletions src/a2a/server/apps/jsonrpc/jsonrpc_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911

# 3) Build call context and wrap the request for downstream handling
call_context = self._context_builder.build(request)
call_context.tenant = getattr(specific_request, 'tenant', '')
call_context.state['method'] = method
call_context.state['request_id'] = request_id

Expand Down
94 changes: 94 additions & 0 deletions tests/integration/test_jsonrpc_tenant_context.py
Comment thread
sokoliva marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest
from unittest.mock import AsyncMock, MagicMock
from httpx import ASGITransport, AsyncClient

from a2a.client import ClientFactory, ClientConfig
from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication
from a2a.server.request_handlers.request_handler import RequestHandler
from a2a.types.a2a_pb2 import (
AgentCard,
AgentInterface,
AgentCapabilities,
ListTasksRequest,
ListTasksResponse,
Task,
)
from a2a.server.context import ServerCallContext
from a2a.utils.constants import TransportProtocol


@pytest.fixture
def mock_handler():
handler = AsyncMock(spec=RequestHandler)
handler.on_list_tasks.return_value = ListTasksResponse(
tasks=[Task(id='task-1')]
)
return handler


@pytest.fixture
def agent_card():
return AgentCard(
supported_interfaces=[
AgentInterface(
url='http://testserver/jsonrpc',
protocol_binding=TransportProtocol.JSONRPC,
tenant='my-test-tenant',
),
],
capabilities=AgentCapabilities(
streaming=False,
push_notifications=False,
),
)


@pytest.fixture
def server_app(agent_card, mock_handler):
app = A2AStarletteApplication(
agent_card=agent_card,
http_handler=mock_handler,
).build(rpc_url='/jsonrpc')
return app


@pytest.mark.asyncio
async def test_jsonrpc_tenant_context_population(
server_app, mock_handler, agent_card
):
"""
Integration test to verify that a tenant configured in the client
is correctly propagated to the ServerCallContext in the server
via the JSON-RPC transport.
"""
# 1. Setup the client using the server app as the transport
# We use ASGITransport so httpx calls go directly to the Starlette app
transport = ASGITransport(app=server_app)
async with AsyncClient(
transport=transport, base_url='http://testserver'
) as httpx_client:
# Create the A2A client properly configured
config = ClientConfig(
httpx_client=httpx_client,
supported_protocol_bindings=[TransportProtocol.JSONRPC],
)
factory = ClientFactory(config)
client = factory.create(agent_card)

# 2. Make the call (list_tasks)
response = await client.list_tasks(ListTasksRequest())

# 3. Verify response
assert len(response.tasks) == 1
assert response.tasks[0].id == 'task-1'

# 4. Verify ServerCallContext on the server side
mock_handler.on_list_tasks.assert_called_once()
call_args = mock_handler.on_list_tasks.call_args

# call_args[0] are positional args: (request, context)
# Check call_args signature in jsonrpc_handler.py: await self.handler.list_tasks(request_obj, context)

server_context = call_args[0][1]
assert isinstance(server_context, ServerCallContext)
assert server_context.tenant == 'my-test-tenant'
88 changes: 88 additions & 0 deletions tests/server/apps/jsonrpc/test_jsonrpc_tenant.py
Comment thread
ishymko marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from unittest.mock import AsyncMock, MagicMock

import pytest
from starlette.testclient import TestClient

from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication
from a2a.server.context import ServerCallContext
from a2a.server.request_handlers.request_handler import RequestHandler
from a2a.types.a2a_pb2 import AgentCard, Message, Part, Role


@pytest.fixture
def mock_handler():
handler = AsyncMock(spec=RequestHandler)
# Return a proto Message object directly - the handler wraps it in SendMessageResponse
handler.on_message_send.return_value = Message(
message_id='test',
role=Role.ROLE_AGENT,
parts=[Part(text='response message')],
)
return handler


@pytest.fixture
def test_app(mock_handler):
mock_agent_card = MagicMock(spec=AgentCard)
mock_agent_card.url = 'http://mockurl.com'
# Set up capabilities.streaming to avoid validation issues
mock_agent_card.capabilities = MagicMock()
mock_agent_card.capabilities.streaming = False

return A2AStarletteApplication(
agent_card=mock_agent_card, http_handler=mock_handler
)


@pytest.fixture
def client(test_app):
return TestClient(test_app.build(rpc_url='/jsonrpc'))


def _make_send_message_request(
text: str = 'hi', tenant: str | None = None
) -> dict:
"""Helper to create a JSON-RPC send message request."""
params = {
'message': {
'messageId': '1',
'role': 'ROLE_USER',
'parts': [{'text': text}],
}
}
if tenant:
params['tenant'] = tenant

return {
'jsonrpc': '2.0',
'id': '1',
'method': 'SendMessage',
'params': params,
}


def test_tenant_extraction_from_params(client, mock_handler):
tenant_id = 'my-tenant-123'
response = client.post(
'/jsonrpc',
json=_make_send_message_request(tenant=tenant_id),
)
response.raise_for_status()

mock_handler.on_message_send.assert_called_once()
call_context = mock_handler.on_message_send.call_args[0][1]
assert isinstance(call_context, ServerCallContext)
assert call_context.tenant == tenant_id


def test_no_tenant_extraction(client, mock_handler):
response = client.post(
'/jsonrpc',
json=_make_send_message_request(tenant=None),
)
response.raise_for_status()

mock_handler.on_message_send.assert_called_once()
call_context = mock_handler.on_message_send.call_args[0][1]
assert isinstance(call_context, ServerCallContext)
assert call_context.tenant == ''
Loading