Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions tests/unit/auth/test_hydra_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ def test_save_credentials_sets_restrictive_permissions(self):
save_agent_credentials(credentials, creds_dir)

creds_file = creds_dir / "oauth_credentials.json"
# Check permissions (owner read/write only)
assert oct(creds_file.stat().st_mode)[-3:] == "600"
# Check permissions (owner read/write only) on non-Windows platforms
import sys
if sys.platform != "win32":
assert oct(creds_file.stat().st_mode)[-3:] == "600"

def test_save_credentials_preserves_existing_entries(self):
"""Test that saving new credentials preserves existing ones."""
Expand Down
180 changes: 180 additions & 0 deletions tests/unit/server/endpoints/test_a2a_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""Unit tests for A2A protocol endpoint."""

import json
import uuid
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from starlette.requests import Request
from starlette.responses import Response

from bindu.common.protocol.types import (
InternalError,
JSONParseError,
MethodNotFoundError,
)
from bindu.server.applications import BinduApplication
from bindu.server.endpoints.a2a_protocol import agent_run_endpoint


@pytest.fixture
def mock_app():
"""Create a mock BinduApplication."""
app = MagicMock(spec=BinduApplication)
app.task_manager = MagicMock()
# Mock the handler method on task_manager
app.task_manager.mock_handler = AsyncMock(return_value={"result": "success"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mock handler should return complete JSON-RPC 2.0 response structure.

The handler currently returns {"result": "success"}, but a valid JSON-RPC 2.0 response must include "jsonrpc": "2.0" and "id" fields. Real handlers return complete responses, and this test should validate the full protocol contract.

📋 Proposed fix to return complete JSON-RPC response
-    app.task_manager.mock_handler = AsyncMock(return_value={"result": "success"})
+    app.task_manager.mock_handler = AsyncMock(
+        return_value={"jsonrpc": "2.0", "result": "success", "id": None}
+    )

Then update the test assertion at line 71 to validate all required fields:

assert content["jsonrpc"] == "2.0"
assert content["result"] == "success"
assert "id" in content
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.task_manager.mock_handler = AsyncMock(return_value={"result": "success"})
app.task_manager.mock_handler = AsyncMock(
return_value={"jsonrpc": "2.0", "result": "success", "id": None}
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/server/endpoints/test_a2a_protocol.py` at line 26, The mock
handler return value is not a complete JSON-RPC 2.0 response—update the
AsyncMock assigned to app.task_manager.mock_handler so its return_value includes
the full JSON-RPC structure (include "jsonrpc": "2.0", "result": "success", and
an "id" field) and then adjust the test assertions that inspect the response
(the assertions referencing content/result) to verify content["jsonrpc"] ==
"2.0", content["result"] == "success", and that an "id" key is present; use the
existing AsyncMock on app.task_manager.mock_handler and the existing response
assertions to locate where to change the mock and add the new checks.

return app


@pytest.fixture
def mock_settings():
"""Mock app settings."""
with patch("bindu.server.endpoints.a2a_protocol.app_settings") as mock:
mock.agent.method_handlers = {
"tasks/list": "mock_handler",
"message/send": "mock_handler",
}
mock.auth.enabled = False
mock.auth.require_permissions = False
yield mock


@pytest.mark.asyncio
async def test_valid_request(mock_app, mock_settings):
"""Test successful processing of a valid A2A request."""
# Setup request with valid JSON body
body = {
"jsonrpc": "2.0",
"method": "tasks/list",
"params": {},
"id": str(uuid.uuid4()),
}

mock_request = MagicMock(spec=Request)
mock_request.body = AsyncMock(return_value=json.dumps(body).encode())

class MockState:
pass
mock_request.state = MockState()

# Call endpoint
response = await agent_run_endpoint(mock_app, mock_request)

# Verify task manager handler was called
mock_app.task_manager.mock_handler.assert_called_once()

# Verify response
assert isinstance(response, Response)
assert response.status_code == 200
content = json.loads(response.body)
assert content["result"] == "success"


@pytest.mark.asyncio
async def test_invalid_json(mock_app):
"""Test handling of invalid JSON body."""
mock_request = MagicMock(spec=Request)
mock_request.body = AsyncMock(return_value=b"invalid json")

response = await agent_run_endpoint(mock_app, mock_request)

assert response.status_code == 400


@pytest.mark.asyncio
async def test_unsupported_method(mock_app, mock_settings):
"""Test handling of unsupported method."""
body = {
"jsonrpc": "2.0",
"method": "unknown/method",
"id": str(uuid.uuid4()),
}

mock_request = MagicMock(spec=Request)
mock_request.body = AsyncMock(return_value=json.dumps(body).encode())

response = await agent_run_endpoint(mock_app, mock_request)

assert response.status_code == 400
# Validation fails for unknown method tag in discriminated union

Comment on lines +85 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the Pydantic schema definition for A2A requests to verify valid method tags
rg -nP -A5 'class.*A2ARequest|a2a_request_ta.*=' --type=py

Repository: GetBindu/Bindu

Length of output: 580


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where A2ARequest is defined and how the discriminator works
rg -n "class A2ARequest|A2ARequest\s*=" bindu/common/protocol/types.py
rg -n "discriminator|Field\\(.*discriminator|tag\\b|method\\b.*discriminator" bindu/common/protocol/types.py

# 2) Show the relevant region around A2ARequest
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("bindu/common/protocol/types.py")
txt = p.read_text(encoding="utf-8")
m = re.search(r"class\s+A2ARequest\b", txt)
if not m:
    # fallback: look for type alias
    m = re.search(r"A2ARequest\s*=", txt)
    if not m:
        print("Could not find A2ARequest definition marker")
        raise SystemExit(1)
start = max(0, txt.count("\n", 0, m.start()) - 20)
# compute line number for m.start()
line = txt[:m.start()].count("\n") + 1
print(f"A2ARequest definition around line ~{line}")
PY

# Print a bounded chunk around the definition (first hit by line number)
LINE=$(python3 - <<'PY'
import re, pathlib
p = pathlib.Path("bindu/common/protocol/types.py")
txt = p.read_text(encoding="utf-8")
for pat in [r"class\s+A2ARequest\b", r"A2ARequest\s*="]:
    m = re.search(pat, txt)
    if m:
        line = txt[:m.start()].count("\n")+1
        print(line)
        break
else:
    raise SystemExit(1)
PY
)
sed -n "$((LINE-40)),$((LINE+120))p" bindu/common/protocol/types.py

# 3) Inspect endpoint error paths in a2a_protocol.py
rg -n "agent_run_endpoint|status_code\s*==|return .*400|return .*404|MethodNotFoundError|JSONParseError|validation" tests -S
rg -n "agent_run_endpoint" -S
fd -e py a2a_protocol.py bindu tests || true
# Use fd to locate if different path exists
fd -e py "a2a_protocol.py" . || true
# Then display surrounding code where 400/404 are set
FOUND=$(fd -e py "a2a_protocol.py" . | head -n 1)
if [ -n "$FOUND" ]; then
  rg -n "MethodNotFoundError|JSONParseError|status_code|return" "$FOUND"
  # Show the chunk around where the handler-not-found logic likely is
  rg -n "MethodNotFoundError" "$FOUND" | head -n 1 | awk -F: '{print $1}' | while read -r L; do
    sed -n "$((L-60)),$((L+80))p" "$FOUND"
  done
fi

Repository: GetBindu/Bindu

Length of output: 52212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the test code around the target lines
sed -n '70,130p' tests/unit/server/endpoints/test_a2a_protocol.py

# Locate and show endpoint error handling blocks (avoid negative ranges)
rg -n "MethodNotFoundError|JSONParseError|status_code\s*=\s*400|status_code\s*=\s*404" bindu/server/endpoints/a2a_protocol.py
rg -n "agent_run_endpoint" bindu/server/endpoints/a2a_protocol.py

# Show a chunk around the endpoint's main logic
# (use line numbers from rg to pick a non-negative window)
ENDPOINT_LINE=$(rg -n "async def agent_run_endpoint" bindu/server/endpoints/a2a_protocol.py | head -n 1 | cut -d: -f1)
START=$((ENDPOINT_LINE-80)); if [ "$START" -lt 1 ]; then START=1; fi
sed -n "${START},$((ENDPOINT_LINE+220))p" bindu/server/endpoints/a2a_protocol.py

Repository: GetBindu/Bindu

Length of output: 9984


Clarify error path: test_unsupported_method is validating the request schema (HTTP 400), not testing handler-not-found (HTTP 404).

agent_run_endpoint first parses the body with a2a_request_ta.validate_json(data); A2ARequest is a discriminated union on "method" with fixed Literal[...] method tags, so "unknown/method" fails schema validation and returns the JSONParseError400. The later MethodNotFoundError404 branch only applies when "method" is valid but not present in app_settings.agent.method_handlers.

  • Rename the test (and/or comment) to reflect the 400/validation case
  • Add a separate test for the 404 handler-not-found path using a valid method tag that isn’t mapped
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/server/endpoints/test_a2a_protocol.py` around lines 85 - 101, The
test test_unsupported_method is asserting a 400 because agent_run_endpoint first
calls a2a_request_ta.validate_json (A2ARequest is a discriminated union on
"method") so an unknown "method" literal triggers JSONParseError → 400; update
the test name/comment to reflect it validates schema/400 rather than
handler-not-found/404, and add a new test that sends a valid method literal (one
of A2ARequest's method tags) but ensures that method is not present in
app_settings.agent.method_handlers to exercise the MethodNotFoundError branch
and assert a 404 from agent_run_endpoint.


@pytest.mark.asyncio
async def test_internal_error(mock_app, mock_settings):
"""Test handling of internal errors."""
body = {
"jsonrpc": "2.0",
"method": "tasks/list",
"params": {},
"id": str(uuid.uuid4()),
}

mock_request = MagicMock(spec=Request)
mock_request.body = AsyncMock(return_value=json.dumps(body).encode())

class MockState:
pass
mock_request.state = MockState()

# Simulate handler raising exception
mock_app.task_manager.mock_handler.side_effect = Exception("Crash")

response = await agent_run_endpoint(mock_app, mock_request)

assert response.status_code == 500
content = json.loads(response.body)
assert "error" in content
assert content["error"]["code"] == -32603 # Internal error


@pytest.mark.asyncio
async def test_payment_context_injection(mock_app):
"""Test injection of payment context into message metadata."""
# Mock settings specifically for this test to ensure handler is found
with patch("bindu.server.endpoints.a2a_protocol.app_settings") as mock_settings:
mock_settings.agent.method_handlers = {"message/send": "mock_handler"}
mock_settings.auth.enabled = False
mock_settings.auth.require_permissions = False

body = {
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"configuration": {
"acceptedOutputModes": ["text"]
},
"message": {
"messageId": "123e4567-e89b-12d3-a456-426614174000",
"contextId": "123e4567-e89b-12d3-a456-426614174001",
"taskId": "123e4567-e89b-12d3-a456-426614174002",
"kind": "message",
"role": "user",
"parts": [{"kind": "text", "text": "hello"}]
}
},
"id": str(uuid.uuid4()),
}

mock_request = MagicMock(spec=Request)
mock_request.body = AsyncMock(return_value=json.dumps(body).encode())

# Setup payment context in request state
class MockState:
payment_payload = {"amount": 100}
payment_requirements = {"token": "USDC"}
verify_response = {"status": "verified"}
mock_request.state = MockState()

# Call endpoint
await agent_run_endpoint(mock_app, mock_request)

# Verify handler called with modified request
call_args = mock_app.task_manager.mock_handler.call_args[0][0]
metadata = call_args["params"]["message"]["metadata"]

assert "_payment_context" in metadata
context = metadata["_payment_context"]
assert context["payment_payload"] == {"amount": 100}
assert context["payment_requirements"] == {"token": "USDC"}
assert context["verify_response"] == {"status": "verified"}
30 changes: 30 additions & 0 deletions tests/unit/server/middleware/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,33 @@ async def test_dispatch_decrements_on_error(self):

mock_metrics.increment_requests_in_flight.assert_called_once()
mock_metrics.decrement_requests_in_flight.assert_called_once()

@pytest.mark.asyncio
async def test_dispatch_ignores_recording_error(self):
"""Test that errors during metric recording don't crash request."""
mock_request = Mock()
mock_request.url.path = "/api/test"
mock_request.headers = {}

mock_response = Mock()
mock_response.status_code = 200

mock_call_next = AsyncMock(return_value=mock_response)

mock_metrics = Mock()
mock_metrics.increment_requests_in_flight = Mock()
mock_metrics.decrement_requests_in_flight = Mock()
# Simulate error in recording
mock_metrics.record_http_request = Mock(side_effect=Exception("Metrics DB down"))

middleware = MetricsMiddleware(app=Mock())

with patch(
"bindu.server.middleware.metrics.get_metrics", return_value=mock_metrics
):
# Should not raise exception
response = await middleware.dispatch(mock_request, mock_call_next)

assert response == mock_response
mock_metrics.decrement_requests_in_flight.assert_called_once() # cleanup always runs

Loading