Skip to content

Commit d2331cd

Browse files
committed
event mocked tests
1 parent 40029b8 commit d2331cd

File tree

4 files changed

+172
-14
lines changed

4 files changed

+172
-14
lines changed

src/mcpcat/modules/event_queue.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@
2424
class EventQueue:
2525
"""Manages event queue and sending to MCPCat API."""
2626

27-
def __init__(self):
27+
def __init__(self, api_client=None):
2828
self.queue: queue.Queue[UnredactedEvent] = queue.Queue(maxsize=10000)
2929
self.max_retries = 3
3030
self.max_queue_size = 10000 # Prevent unbounded growth
3131
self.concurrency = 5 # Max parallel requests
32-
config = Configuration(host=MCPCAT_API_URL)
33-
self.api_client = EventsApi(config)
32+
33+
# Allow injection of api_client for testing
34+
if api_client is None:
35+
config = Configuration(host=MCPCAT_API_URL)
36+
self.api_client = EventsApi(config)
37+
else:
38+
self.api_client = api_client
39+
3440
self._shutdown = False
3541
self._shutdown_event = threading.Event()
3642

@@ -86,10 +92,9 @@ def _process_event(self, event: UnredactedEvent) -> None:
8692
# Redact sensitive information if a redaction function is provided
8793
try:
8894
redacted_event = redact_event_sync(event)
95+
# The redacted event is already the full event object, not a dict
96+
event = redacted_event
8997
event.redaction_fn = None # Clear the function to avoid reprocessing
90-
# Update event with redacted data
91-
for key, value in redacted_event.items():
92-
setattr(event, key, value)
9398
except Exception as error:
9499
write_to_log(f"WARNING: Dropping event {event.id or 'unknown'} due to redaction failure: {error}")
95100
return # Skip this event if redaction fails
@@ -157,6 +162,14 @@ def _shutdown_handler(*_):
157162
event_queue.destroy()
158163

159164

165+
def set_event_queue(new_queue: EventQueue) -> None:
166+
"""Replace the global event queue instance (for testing)."""
167+
global event_queue
168+
# Destroy the old queue first
169+
event_queue.destroy()
170+
event_queue = new_queue
171+
172+
160173
# Register shutdown handlers
161174
signal.signal(signal.SIGINT, _shutdown_handler)
162175
signal.signal(signal.SIGTERM, _shutdown_handler)

src/mcpcat/modules/overrides/mcp_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
146146

147147
parameters=request.params.model_dump() if request.params else {},
148148
event_type=EventType.MCP_TOOLS_CALL.value,
149-
resourceName=tool_name,
149+
resource_name=tool_name,
150150
redaction_fn=data.options.redact_sensitive_information,
151151
)
152152

tests/test_event_queue.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,21 @@ def test_process_event_with_redaction(self, mock_redact):
116116
"""Test processing event with redaction function."""
117117
eq = EventQueue()
118118
mock_redaction_fn = MagicMock()
119-
# Mock redact_event_sync to return a dict with redacted values
120-
mock_redact.return_value = {"parameters": {"secret": "[REDACTED]"}, "user_intent": "redacted intent"}
119+
120+
# Create a redacted event
121+
redacted_event = UnredactedEvent(
122+
id="test-id",
123+
event_type="mcp:tools/call",
124+
project_id="project-123",
125+
session_id="session-123",
126+
timestamp=datetime.now(),
127+
parameters={"secret": "[REDACTED]"},
128+
user_intent="redacted intent",
129+
redaction_fn=None # This should be cleared after redaction
130+
)
131+
132+
# Mock redact_event_sync to return the redacted event
133+
mock_redact.return_value = redacted_event
121134

122135
event = UnredactedEvent(
123136
id="test-id",
@@ -134,10 +147,11 @@ def test_process_event_with_redaction(self, mock_redact):
134147
eq._process_event(event)
135148

136149
mock_redact.assert_called_once_with(event)
137-
assert event.redaction_fn is None
138-
# Check that attributes were updated
139-
assert event.parameters == {"secret": "[REDACTED]"}
140-
assert event.user_intent == "redacted intent"
150+
# The send_event should be called with the redacted event
151+
called_event = mock_send.call_args[0][0]
152+
assert called_event.parameters == {"secret": "[REDACTED]"}
153+
assert called_event.user_intent == "redacted intent"
154+
assert called_event.redaction_fn is None
141155
mock_send.assert_called_once()
142156

143157
@patch('mcpcat.modules.event_queue.redact_event_sync')

tests/test_report_missing.py

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Test report_missing functionality."""
22

33
import pytest
4+
from unittest.mock import MagicMock
5+
import time
46

57
from mcpcat import MCPCatOptions, track
68

@@ -285,4 +287,133 @@ async def test_report_missing_with_invalid_types(self):
285287
)
286288
# Should convert to string and work
287289
assert result.content[0].text
288-
assert "12345" in result.content[0].text
290+
assert "12345" in result.content[0].text
291+
292+
@pytest.mark.asyncio
293+
async def test_report_missing_publishes_event(self):
294+
"""Verify that calling report_missing tool publishes an event to the queue."""
295+
from mcpcat.modules.event_queue import EventQueue, set_event_queue
296+
297+
# Create a mock API client
298+
mock_api_client = MagicMock()
299+
mock_api_client.publish_event = MagicMock(return_value=None)
300+
301+
# Create a new EventQueue with our mock
302+
test_queue = EventQueue(api_client=mock_api_client)
303+
304+
# Replace the global event queue
305+
set_event_queue(test_queue)
306+
307+
try:
308+
server = create_todo_server()
309+
options = MCPCatOptions(enable_report_missing=True, enable_tracing=True)
310+
track(server, "test_project", options)
311+
312+
async with create_test_client(server) as client:
313+
# Call the report_missing tool
314+
await client.call_tool(
315+
"report_missing",
316+
{
317+
"missing_tool": "image_resize",
318+
"description": "Need to resize images to different dimensions"
319+
}
320+
)
321+
322+
# Give the event queue worker thread time to process
323+
time.sleep(1.0)
324+
325+
# Verify that publish_event was called
326+
assert mock_api_client.publish_event.called
327+
assert mock_api_client.publish_event.call_count >= 1 # At least one call
328+
329+
# Find the tool call event
330+
tool_call_event = None
331+
for call in mock_api_client.publish_event.call_args_list:
332+
event = call[1]["publish_event_request"]
333+
if event.event_type == "mcp:tools/call" and event.resource_name == "report_missing":
334+
tool_call_event = event
335+
break
336+
337+
assert tool_call_event is not None, "No report_missing tool call event found"
338+
339+
# Verify event properties
340+
assert tool_call_event.project_id == "test_project"
341+
342+
# Verify the arguments contain our input
343+
assert tool_call_event.parameters["arguments"]["missing_tool"] == "image_resize"
344+
assert tool_call_event.parameters["arguments"]["description"] == "Need to resize images to different dimensions"
345+
346+
finally:
347+
# Clean up: restore original event queue
348+
from mcpcat.modules.event_queue import EventQueue, set_event_queue
349+
set_event_queue(EventQueue())
350+
351+
@pytest.mark.asyncio
352+
async def test_multiple_tool_calls_publish_multiple_events(self):
353+
"""Verify that multiple tool calls result in multiple events being published."""
354+
from mcpcat.modules.event_queue import EventQueue, set_event_queue
355+
356+
# Create a mock API client
357+
mock_api_client = MagicMock()
358+
mock_api_client.publish_event = MagicMock(return_value=None)
359+
360+
# Create a new EventQueue with our mock
361+
test_queue = EventQueue(api_client=mock_api_client)
362+
363+
# Replace the global event queue
364+
set_event_queue(test_queue)
365+
366+
try:
367+
server = create_todo_server()
368+
options = MCPCatOptions(enable_report_missing=True, enable_tracing=True)
369+
track(server, "test_project", options)
370+
371+
async with create_test_client(server) as client:
372+
# Call report_missing tool
373+
await client.call_tool(
374+
"report_missing",
375+
{"missing_tool": "tool1", "description": "desc1"}
376+
)
377+
378+
# Call a regular tool
379+
await client.call_tool(
380+
"add_todo",
381+
{"text": "Test todo item"}
382+
)
383+
384+
# Call report_missing again
385+
await client.call_tool(
386+
"report_missing",
387+
{"missing_tool": "tool2", "description": "desc2"}
388+
)
389+
390+
# Allow time for processing
391+
import time
392+
time.sleep(1.0)
393+
394+
# Should have at least 3 tool call events (plus initialize and list_tools events)
395+
assert mock_api_client.publish_event.call_count >= 3
396+
397+
# Get all published events
398+
events = [call[1]["publish_event_request"] for call in mock_api_client.publish_event.call_args_list]
399+
400+
# Filter to just tool call events
401+
tool_events = [e for e in events if e.event_type == "mcp:tools/call"]
402+
403+
# Should have exactly 3 tool calls
404+
assert len(tool_events) == 3
405+
406+
# Verify event types and tool names
407+
assert tool_events[0].resource_name == "report_missing"
408+
assert tool_events[0].parameters["arguments"]["missing_tool"] == "tool1"
409+
410+
assert tool_events[1].resource_name == "add_todo"
411+
assert tool_events[1].parameters["arguments"]["text"] == "Test todo item"
412+
413+
assert tool_events[2].resource_name == "report_missing"
414+
assert tool_events[2].parameters["arguments"]["missing_tool"] == "tool2"
415+
416+
finally:
417+
# Clean up: restore original event queue
418+
from mcpcat.modules.event_queue import EventQueue, set_event_queue
419+
set_event_queue(EventQueue())

0 commit comments

Comments
 (0)