Skip to content

Commit f9f55da

Browse files
committed
Unit tests for Python lambda client
1 parent c904688 commit f9f55da

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed

src/python/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ log_format = "%(asctime)s - %(levelname)s - %(message)s"
5757
log_date_format = "%Y-%m-%d %H:%M:%S"
5858
log_cli_level = "DEBUG"
5959
log_cli = true
60+
addopts = "--cov=src/mcp_lambda --cov-report=term --cov-report=html --cov-fail-under=90"
6061

6162
[tool.pytest_env]
6263
LOG_LEVEL = "DEBUG"
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import anyio
2+
import json
3+
import pytest
4+
import time
5+
from unittest import mock
6+
from unittest.mock import AsyncMock
7+
8+
from mcp.types import (
9+
JSONRPCError,
10+
JSONRPCMessage,
11+
JSONRPCNotification,
12+
JSONRPCRequest,
13+
JSONRPCResponse,
14+
)
15+
from mcp_lambda import (
16+
lambda_function_client,
17+
LambdaFunctionParameters,
18+
)
19+
20+
lambda_parameters = LambdaFunctionParameters(
21+
function_name="mock-function", region_name="us-west-2"
22+
)
23+
24+
25+
def mock_lambda_client(mock_client_creator, response, payload):
26+
mock_client = AsyncMock()
27+
28+
mock_payload = AsyncMock()
29+
mock_payload.__aenter__.return_value = AsyncMock()
30+
mock_payload.__aenter__.return_value.read = AsyncMock(
31+
return_value=json.dumps(payload).encode("utf-8")
32+
)
33+
34+
mock_client.invoke.return_value = {"Payload": mock_payload} | response
35+
36+
mock_client_creator.return_value.__aenter__.return_value = mock_client
37+
38+
return mock_client
39+
40+
41+
@pytest.mark.anyio
42+
@mock.patch("aiobotocore.session.get_session")
43+
@mock.patch("aiobotocore.session.ClientCreatorContext")
44+
async def test_lambda_function_client_success(mock_client_creator, mock_session):
45+
mock_session.create_client = mock_client_creator
46+
47+
mock_client = mock_lambda_client(
48+
mock_client_creator,
49+
response={"StatusCode": 200},
50+
payload={
51+
"jsonrpc": "2.0",
52+
"id": "response-id",
53+
"result": {"message": "success"},
54+
},
55+
)
56+
57+
# Create a test message
58+
test_message = JSONRPCMessage(
59+
root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
60+
)
61+
62+
async with lambda_function_client(lambda_parameters) as (read_stream, write_stream):
63+
# Send a message
64+
async with write_stream:
65+
await write_stream.send(test_message)
66+
67+
# Receive the response
68+
async with read_stream:
69+
response = await read_stream.receive()
70+
71+
# Verify the response
72+
assert isinstance(response, JSONRPCMessage)
73+
assert isinstance(response.root, JSONRPCResponse)
74+
assert response.root.id == "response-id"
75+
assert response.root.result == {"message": "success"}
76+
77+
# Verify Lambda was invoked with correct parameters
78+
mock_client.invoke.assert_called_once()
79+
call_args = mock_client.invoke.call_args[1]
80+
assert call_args["FunctionName"] == "mock-function"
81+
assert call_args["InvocationType"] == "RequestResponse"
82+
assert json.loads(call_args["Payload"].decode("utf-8")) == {
83+
"jsonrpc": "2.0",
84+
"id": 1,
85+
"method": "ping",
86+
}
87+
88+
89+
@pytest.mark.anyio
90+
@mock.patch("aiobotocore.session.get_session")
91+
@mock.patch("aiobotocore.session.ClientCreatorContext")
92+
async def test_lambda_function_notification_success(mock_client_creator, mock_session):
93+
mock_session.create_client = mock_client_creator
94+
95+
mock_client = mock_lambda_client(
96+
mock_client_creator,
97+
response={"StatusCode": 200},
98+
payload={},
99+
)
100+
101+
# Create a test message
102+
test_message = JSONRPCMessage(
103+
root=JSONRPCNotification(jsonrpc="2.0", method="notifications/initialized")
104+
)
105+
106+
async with lambda_function_client(lambda_parameters) as (read_stream, write_stream):
107+
# Send a message
108+
async with write_stream:
109+
await write_stream.send(test_message)
110+
111+
# Test that we don't receive a response
112+
async with read_stream:
113+
with pytest.raises(anyio.WouldBlock):
114+
time.sleep(1)
115+
await read_stream.receive_nowait()
116+
117+
# Verify Lambda was invoked with correct parameters
118+
mock_client.invoke.assert_called_once()
119+
call_args = mock_client.invoke.call_args[1]
120+
assert call_args["FunctionName"] == "mock-function"
121+
assert call_args["InvocationType"] == "RequestResponse"
122+
assert json.loads(call_args["Payload"].decode("utf-8")) == {
123+
"jsonrpc": "2.0",
124+
"method": "notifications/initialized",
125+
}
126+
127+
128+
@pytest.mark.anyio
129+
@mock.patch("aiobotocore.session.get_session")
130+
@mock.patch("aiobotocore.session.ClientCreatorContext")
131+
async def test_lambda_function_client_function_error(mock_client_creator, mock_session):
132+
mock_session.create_client = mock_client_creator
133+
134+
mock_client = mock_lambda_client(
135+
mock_client_creator,
136+
response={"StatusCode": 200, "FunctionError": "Unhandled"},
137+
payload={
138+
"jsonrpc": "2.0",
139+
"id": "response-id",
140+
"result": {
141+
"errorMessage": "Something went wrong",
142+
"errorType": "RuntimeError",
143+
},
144+
},
145+
)
146+
147+
# Create a test message
148+
test_message = JSONRPCMessage(
149+
root=JSONRPCRequest(
150+
jsonrpc="2.0", id=1, method="call/tool", params={"hello": "world"}
151+
)
152+
)
153+
154+
async with lambda_function_client(lambda_parameters) as (read_stream, write_stream):
155+
# Send a message
156+
async with write_stream:
157+
await write_stream.send(test_message)
158+
159+
# Receive the response
160+
async with read_stream:
161+
response = await read_stream.receive()
162+
163+
# Verify the response is an error message
164+
assert isinstance(response, JSONRPCMessage)
165+
assert isinstance(response.root, JSONRPCError)
166+
assert response.root.id == 1
167+
assert response.root.error is not None
168+
assert response.root.error.code == 500
169+
assert (
170+
"Function invoke returned a function error" in response.root.error.message
171+
)
172+
173+
# Verify Lambda was invoked with correct parameters
174+
mock_client.invoke.assert_called_once()
175+
call_args = mock_client.invoke.call_args[1]
176+
assert call_args["FunctionName"] == "mock-function"
177+
assert call_args["InvocationType"] == "RequestResponse"
178+
assert json.loads(call_args["Payload"].decode("utf-8")) == {
179+
"jsonrpc": "2.0",
180+
"id": 1,
181+
"method": "call/tool",
182+
"params": {"hello": "world"},
183+
}
184+
185+
186+
@pytest.mark.anyio
187+
@mock.patch("aiobotocore.session.get_session")
188+
@mock.patch("aiobotocore.session.ClientCreatorContext")
189+
async def test_lambda_function_client_invoke_exception(
190+
mock_client_creator, mock_session
191+
):
192+
mock_session.create_client = mock_client_creator
193+
194+
# Make invoke raise an exception
195+
mock_client = AsyncMock()
196+
mock_client_creator.return_value.__aenter__.return_value = mock_client
197+
mock_client.invoke.side_effect = Exception("Connection error")
198+
199+
# Create a test message
200+
test_message = JSONRPCMessage(
201+
root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
202+
)
203+
204+
async with lambda_function_client(lambda_parameters) as (read_stream, write_stream):
205+
# Send a message
206+
async with write_stream:
207+
await write_stream.send(test_message)
208+
209+
# Receive the response
210+
async with read_stream:
211+
response = await read_stream.receive()
212+
213+
# Verify the response is an error message
214+
assert isinstance(response, JSONRPCMessage)
215+
assert isinstance(response.root, JSONRPCError)
216+
assert response.root.id == 1
217+
assert response.root.error is not None
218+
assert response.root.error.code == 500
219+
assert "Connection error" in response.root.error.message
220+
221+
# Verify Lambda was invoked with correct parameters
222+
mock_client.invoke.assert_called_once()
223+
call_args = mock_client.invoke.call_args[1]
224+
assert call_args["FunctionName"] == "mock-function"
225+
assert call_args["InvocationType"] == "RequestResponse"
226+
assert json.loads(call_args["Payload"].decode("utf-8")) == {
227+
"jsonrpc": "2.0",
228+
"id": 1,
229+
"method": "ping",
230+
}
231+
232+
233+
@pytest.mark.anyio
234+
@mock.patch("aiobotocore.session.get_session")
235+
@mock.patch("aiobotocore.session.ClientCreatorContext")
236+
async def test_lambda_function_client_invalid_response(
237+
mock_client_creator, mock_session
238+
):
239+
mock_session.create_client = mock_client_creator
240+
241+
mock_client = mock_lambda_client(
242+
mock_client_creator,
243+
response={"StatusCode": 200},
244+
payload="invalid json",
245+
)
246+
247+
# Create a test message
248+
test_message = JSONRPCMessage(
249+
root=JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
250+
)
251+
252+
async with lambda_function_client(lambda_parameters) as (read_stream, write_stream):
253+
# Send a message
254+
async with write_stream:
255+
await write_stream.send(test_message)
256+
257+
# Receive the response
258+
async with read_stream:
259+
response = await read_stream.receive()
260+
261+
# Verify the response is an error message
262+
assert isinstance(response, JSONRPCMessage)
263+
assert isinstance(response.root, JSONRPCError)
264+
assert response.root.id == 1
265+
assert response.root.error is not None
266+
assert response.root.error.code == 500
267+
assert "4 validation errors for JSONRPCMessage" in response.root.error.message
268+
269+
# Verify Lambda was invoked with correct parameters
270+
mock_client.invoke.assert_called_once()
271+
call_args = mock_client.invoke.call_args[1]
272+
assert call_args["FunctionName"] == "mock-function"
273+
assert call_args["InvocationType"] == "RequestResponse"
274+
assert json.loads(call_args["Payload"].decode("utf-8")) == {
275+
"jsonrpc": "2.0",
276+
"id": 1,
277+
"method": "ping",
278+
}

0 commit comments

Comments
 (0)