Skip to content

Commit 86645bb

Browse files
test: fix RuntimeWarnings from improper AsyncMock usage in tests (#1344)
* test: fix RuntimeWarnings from improper AsyncMock usage in tests Fixed RuntimeWarnings caused by using AsyncMock() for synchronous methods across multiple test files. The issue occurred when tests mocked sync methods (like db.commit(), response.raise_for_status(), Redis.from_url()) with AsyncMock, creating unawaited coroutines. Changes: - Use Mock()/MagicMock() for synchronous methods instead of AsyncMock() - Fixed SQLAlchemy session methods (commit, refresh, close) - Fixed HTTP response methods (raise_for_status) - Fixed Redis client methods (from_url, pubsub, close) - Fixed asyncio methods (create_task return values) - Fixed event loop methods (call_soon_threadsafe) - Removed unused event_loop fixture from conftest.py Files modified: - tests/unit/mcpgateway/test_reverse_proxy.py: Fixed process/task mocks - tests/unit/mcpgateway/cache/test_session_registry.py: Fixed db session mocks - tests/unit/mcpgateway/cache/test_session_registry_extended.py: Fixed Redis mocks - tests/unit/mcpgateway/services/test_tool_service.py: Fixed db/HTTP mocks - tests/unit/mcpgateway/services/test_logging_service_comprehensive.py: Fixed event loop mocks - tests/unit/mcpgateway/services/test_role_service.py: Fixed patch.object usage - tests/conftest.py: Removed deprecated event_loop fixture Test results: - 3108 tests passed - Reduced RuntimeWarnings from many to 4 false-positive attributions - All individual tests pass with no RuntimeWarnings Signed-off-by: Jonathan Springer <[email protected]> * chore: remove accidentally committed .orig file Remove test_translate.py.orig which was accidentally included in the previous commit. This is a merge/rebase artifact that should not be tracked in version control. Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Jonathan Springer <[email protected]> Signed-off-by: Mihai Criveti <[email protected]> Co-authored-by: Mihai Criveti <[email protected]>
1 parent a5a24f7 commit 86645bb

File tree

10 files changed

+181
-210
lines changed

10 files changed

+181
-210
lines changed

mcpgateway/reverse_proxy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ def __init__(
258258
token: Optional[str] = None,
259259
reconnect_delay: float = DEFAULT_RECONNECT_DELAY,
260260
max_retries: int = DEFAULT_MAX_RETRIES,
261-
keepalive_interval: int = DEFAULT_KEEPALIVE_INTERVAL,
261+
keepalive_interval: float = DEFAULT_KEEPALIVE_INTERVAL,
262262
):
263263
"""Initialize reverse proxy client.
264264

tests/conftest.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,11 @@
2323
from mcpgateway.db import Base
2424

2525
# Local
26-
# Test utilities - import before mcpgateway modules
2726

2827
# Skip session-level RBAC patching for now - let individual tests handle it
2928
# _session_rbac_originals = patch_rbac_decorators()
3029

3130

32-
@pytest.fixture(scope="session")
33-
def event_loop():
34-
"""Create an instance of the default event loop for each test session."""
35-
loop = asyncio.get_event_loop_policy().new_event_loop()
36-
yield loop
37-
loop.close()
38-
39-
4031
@pytest.fixture(scope="session")
4132
def test_db_url():
4233
"""Return the URL for the test database."""
@@ -143,17 +134,6 @@ def mock_websocket():
143134
return mock
144135

145136

146-
# @pytest.fixture(scope="session", autouse=True)
147-
# def _patch_stdio_first():
148-
# """
149-
# Runs once, *before* the test session collects other modules,
150-
# so no rogue coroutine can be created.
151-
# """
152-
# import mcpgateway.translate as translate
153-
# translate._run_stdio_to_sse = AsyncMock(return_value=None)
154-
# translate._run_sse_to_stdio = AsyncMock(return_value=None)
155-
156-
157137
@pytest.fixture(scope="module") # one DB per test module is usually fine
158138
def app_with_temp_db():
159139
"""Return a FastAPI app wired to a fresh SQLite database."""

tests/unit/mcpgateway/cache/test_session_registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import re
3434
import sys
3535
from typing import Any, Dict, List
36-
from unittest.mock import AsyncMock, Mock, patch
36+
from unittest.mock import AsyncMock, MagicMock, Mock, patch
3737

3838
# Third-Party
3939
from fastapi import HTTPException
@@ -284,7 +284,7 @@ async def test_broadcast_database_input(monkeypatch, registry: SessionRegistry,
284284
monkeypatch.setattr("mcpgateway.cache.session_registry.SQLALCHEMY_AVAILABLE", True)
285285
registry._backend = "database"
286286

287-
mock_db = AsyncMock()
287+
mock_db = MagicMock() # Use MagicMock for sync SQLAlchemy session methods
288288
monkeypatch.setattr("mcpgateway.cache.session_registry.SQLALCHEMY_AVAILABLE", True)
289289
monkeypatch.setattr("mcpgateway.cache.session_registry.get_db", lambda: iter([mock_db]), raising=True)
290290

tests/unit/mcpgateway/cache/test_session_registry_extended.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -857,10 +857,12 @@ class TestRedisSessionRefresh:
857857
async def test_refresh_redis_sessions_general_error(self, monkeypatch, caplog):
858858
"""Test _refresh_redis_sessions handles general errors."""
859859
mock_redis = AsyncMock()
860+
mock_redis.close = Mock() # Redis close() is synchronous
861+
mock_redis.pubsub = Mock(return_value=AsyncMock()) # pubsub() is synchronous, returns async object
860862

861863
with patch("mcpgateway.cache.session_registry.REDIS_AVAILABLE", True):
862864
with patch("mcpgateway.cache.session_registry.Redis") as MockRedis:
863-
MockRedis.from_url.return_value = mock_redis
865+
MockRedis.from_url = Mock(return_value=mock_redis) # from_url is synchronous
864866

865867
registry = SessionRegistry(backend="redis", redis_url="redis://localhost")
866868

@@ -929,7 +931,7 @@ async def test_redis_initialization_subscribe(self, monkeypatch):
929931

930932
with patch("mcpgateway.cache.session_registry.REDIS_AVAILABLE", True):
931933
with patch("mcpgateway.cache.session_registry.Redis") as MockRedis:
932-
MockRedis.from_url.return_value = mock_redis
934+
MockRedis.from_url = Mock(return_value=mock_redis) # from_url is synchronous
933935

934936
registry = SessionRegistry(backend="redis", redis_url="redis://localhost")
935937
await registry.initialize()

tests/unit/mcpgateway/services/test_logging_service_comprehensive.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ async def test_storage_handler_emit_format_error():
581581

582582
# Mock the event loop
583583
mock_loop = MagicMock()
584+
mock_loop.call_soon_threadsafe = MagicMock() # Ensure call_soon_threadsafe is sync
584585
handler.loop = mock_loop
585586

586587
# Should not raise

tests/unit/mcpgateway/services/test_role_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,8 @@ async def test_assign_team_role_without_scope_id(self, role_service, sample_role
540540
"""Test assigning team-scoped role without scope_id."""
541541
sample_role.scope = "team"
542542

543-
with patch.object(role_service, "get_role_by_id", new=AsyncMock(return_value=sample_role)):
543+
mock_get_role = AsyncMock(return_value=sample_role)
544+
with patch.object(role_service, "get_role_by_id", new=mock_get_role):
544545
with pytest.raises(ValueError, match="scope_id required"):
545546
await role_service.assign_role_to_user(user_email="[email protected]", role_id="role-123", scope="team", scope_id=None, granted_by="[email protected]")
546547

tests/unit/mcpgateway/services/test_tool_service.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,8 +1114,8 @@ async def test_update_tool_extra_fields(self, tool_service, mock_tool, test_db):
11141114
# Mock DB get to return None
11151115
mock_tool.id = "999"
11161116
test_db.get = Mock(return_value=mock_tool)
1117-
test_db.commit = AsyncMock()
1118-
test_db.refresh = AsyncMock()
1117+
test_db.commit = Mock() # SQLAlchemy commit is synchronous
1118+
test_db.refresh = Mock() # SQLAlchemy refresh is synchronous
11191119

11201120
# Create update request
11211121
tool_update = ToolUpdate(integration_type="REST", request_type="POST", headers={"key": "value"}, input_schema={"key2": "value2"}, annotations={"key3": "value3"}, jsonpath_filter="test_filter")
@@ -1136,8 +1136,8 @@ async def test_update_tool_basic_auth(self, tool_service, mock_tool, test_db):
11361136
# Mock DB get to return None
11371137
mock_tool.id = "999"
11381138
test_db.get = Mock(return_value=mock_tool)
1139-
test_db.commit = AsyncMock()
1140-
test_db.refresh = AsyncMock()
1139+
test_db.commit = Mock() # SQLAlchemy commit is synchronous
1140+
test_db.refresh = Mock() # SQLAlchemy refresh is synchronous
11411141

11421142
# Basic auth_value
11431143
# Create auth_value with the following values
@@ -1162,8 +1162,8 @@ async def test_update_tool_bearer_auth(self, tool_service, mock_tool, test_db):
11621162
# Mock DB get to return None
11631163
mock_tool.id = "999"
11641164
test_db.get = Mock(return_value=mock_tool)
1165-
test_db.commit = AsyncMock()
1166-
test_db.refresh = AsyncMock()
1165+
test_db.commit = Mock() # SQLAlchemy commit is synchronous
1166+
test_db.refresh = Mock() # SQLAlchemy refresh is synchronous
11671167

11681168
# Bearer auth_value
11691169
# Create auth_value with the following values
@@ -1183,8 +1183,8 @@ async def test_update_tool_empty_auth(self, tool_service, mock_tool, test_db):
11831183
# Mock DB get to return None
11841184
mock_tool.id = "999"
11851185
test_db.get = Mock(return_value=mock_tool)
1186-
test_db.commit = AsyncMock()
1187-
test_db.refresh = AsyncMock()
1186+
test_db.commit = Mock() # SQLAlchemy commit is synchronous
1187+
test_db.refresh = Mock() # SQLAlchemy refresh is synchronous
11881188

11891189
# Create update request
11901190
tool_update = ToolUpdate(auth=AuthenticationValues())
@@ -1243,7 +1243,7 @@ async def test_invoke_tool_rest_get(self, tool_service, mock_tool, test_db):
12431243

12441244
# --------------- HTTP ------------------
12451245
mock_response = AsyncMock()
1246-
mock_response.raise_for_status = AsyncMock()
1246+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
12471247
mock_response.status_code = 200
12481248
# <-- make json() *synchronous*
12491249
mock_response.json = Mock(return_value={"result": "REST tool response"})
@@ -1268,7 +1268,7 @@ async def test_invoke_tool_rest_get(self, tool_service, mock_tool, test_db):
12681268

12691269
# Test 204 status
12701270
mock_response = AsyncMock()
1271-
mock_response.raise_for_status = AsyncMock()
1271+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
12721272
mock_response.status_code = 204
12731273
mock_response.json = Mock(return_value=ToolResult(content=[TextContent(type="text", text="Request completed successfully (No Content)")]))
12741274

@@ -1284,7 +1284,7 @@ async def test_invoke_tool_rest_get(self, tool_service, mock_tool, test_db):
12841284

12851285
# Test 205 status
12861286
mock_response = AsyncMock()
1287-
mock_response.raise_for_status = AsyncMock()
1287+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
12881288
mock_response.status_code = 205
12891289
mock_response.json = Mock(return_value=ToolResult(content=[TextContent(type="text", text="Tool error encountered")]))
12901290

@@ -1314,7 +1314,7 @@ async def test_invoke_tool_rest_post(self, tool_service, mock_tool, test_db):
13141314

13151315
# Mock HTTP client response
13161316
mock_response = AsyncMock()
1317-
mock_response.raise_for_status = AsyncMock()
1317+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
13181318
mock_response.status_code = 200
13191319
mock_response.json = Mock(return_value={"result": "REST tool response"}) # Make json() synchronous
13201320
tool_service._http_client.request.return_value = mock_response
@@ -2038,7 +2038,7 @@ async def test_invoke_tool_rest_oauth_success(self, tool_service, mock_tool, tes
20382038

20392039
# Mock HTTP client response
20402040
mock_response = AsyncMock()
2041-
mock_response.raise_for_status = AsyncMock()
2041+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
20422042
mock_response.status_code = 200
20432043
mock_response.json = Mock(return_value={"result": "OAuth success"})
20442044
tool_service._http_client.request.return_value = mock_response
@@ -2150,7 +2150,7 @@ async def test_invoke_tool_with_passthrough_headers_rest(self, tool_service, moc
21502150

21512151
# Mock HTTP client response
21522152
mock_response = AsyncMock()
2153-
mock_response.raise_for_status = AsyncMock()
2153+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
21542154
mock_response.status_code = 200
21552155
mock_response.json = Mock(return_value={"result": "success with headers"})
21562156
tool_service._http_client.request.return_value = mock_response
@@ -2242,7 +2242,7 @@ async def test_invoke_tool_with_plugin_post_invoke_success(self, tool_service, m
22422242

22432243
# Mock HTTP client response
22442244
mock_response = AsyncMock()
2245-
mock_response.raise_for_status = AsyncMock()
2245+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
22462246
mock_response.status_code = 200
22472247
mock_response.json = Mock(return_value={"result": "original response"})
22482248
tool_service._http_client.request.return_value = mock_response
@@ -2283,7 +2283,7 @@ async def test_invoke_tool_with_plugin_post_invoke_modified_payload(self, tool_s
22832283

22842284
# Mock HTTP client response
22852285
mock_response = AsyncMock()
2286-
mock_response.raise_for_status = AsyncMock()
2286+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
22872287
mock_response.status_code = 200
22882288
mock_response.json = Mock(return_value={"result": "original response"})
22892289
tool_service._http_client.request.return_value = mock_response
@@ -2327,7 +2327,7 @@ async def test_invoke_tool_with_plugin_post_invoke_invalid_modified_payload(self
23272327

23282328
# Mock HTTP client response
23292329
mock_response = AsyncMock()
2330-
mock_response.raise_for_status = AsyncMock()
2330+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
23312331
mock_response.status_code = 200
23322332
mock_response.json = Mock(return_value={"result": "original response"})
23332333
tool_service._http_client.request.return_value = mock_response
@@ -2371,7 +2371,7 @@ async def test_invoke_tool_with_plugin_post_invoke_error_fail_on_error(self, too
23712371

23722372
# Mock HTTP client response
23732373
mock_response = AsyncMock()
2374-
mock_response.raise_for_status = AsyncMock()
2374+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
23752375
mock_response.status_code = 200
23762376
mock_response.json = Mock(return_value={"result": "original response"})
23772377
tool_service._http_client.request.return_value = mock_response
@@ -2414,7 +2414,7 @@ async def test_invoke_tool_with_plugin_metadata_rest(self, tool_service, mock_to
24142414

24152415
# Mock HTTP client response
24162416
mock_response = AsyncMock()
2417-
mock_response.raise_for_status = AsyncMock()
2417+
mock_response.raise_for_status = Mock() # HTTP response raise_for_status is synchronous
24182418
mock_response.status_code = 200
24192419
mock_response.json = Mock(return_value={"result": "original response"})
24202420
tool_service._http_client.request.return_value = mock_response

tests/unit/mcpgateway/test_cli_export_import_coverage.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
from pathlib import Path
1414
import tempfile
15-
from unittest.mock import MagicMock, patch
15+
from unittest.mock import AsyncMock, MagicMock, patch
1616

1717
# Third-Party
1818
import pytest
@@ -179,8 +179,7 @@ def test_main_with_subcommands_export():
179179
from mcpgateway.cli_export_import import main_with_subcommands
180180

181181
with patch.object(sys, "argv", ["mcpgateway", "export", "--help"]):
182-
with patch("mcpgateway.cli_export_import.asyncio.run") as mock_run:
183-
mock_run.side_effect = SystemExit(0) # Simulate help exit
182+
with patch("mcpgateway.cli_export_import.export_command", new_callable=AsyncMock, side_effect=SystemExit(0)):
184183
with pytest.raises(SystemExit):
185184
main_with_subcommands()
186185

@@ -194,8 +193,7 @@ def test_main_with_subcommands_import():
194193
from mcpgateway.cli_export_import import main_with_subcommands
195194

196195
with patch.object(sys, "argv", ["mcpgateway", "import", "--help"]):
197-
with patch("mcpgateway.cli_export_import.asyncio.run") as mock_run:
198-
mock_run.side_effect = SystemExit(0) # Simulate help exit
196+
with patch("mcpgateway.cli_export_import.import_command", new_callable=AsyncMock, side_effect=SystemExit(0)):
199197
with pytest.raises(SystemExit):
200198
main_with_subcommands()
201199

@@ -665,19 +663,18 @@ def test_main_with_subcommands_keyboard_interrupt():
665663

666664
mock_parser = MagicMock()
667665
mock_args = MagicMock()
668-
mock_args.func = MagicMock()
666+
mock_args.func = AsyncMock(side_effect=KeyboardInterrupt())
669667
mock_args.include_dependencies = True
670668
mock_parser.parse_args.return_value = mock_args
671669

672670
with patch.object(sys, "argv", ["mcpgateway", "import", "test.json"]):
673671
with patch("mcpgateway.cli_export_import.create_parser", return_value=mock_parser):
674-
with patch("mcpgateway.cli_export_import.asyncio.run", side_effect=KeyboardInterrupt()):
675-
with patch("builtins.print") as mock_print:
676-
with pytest.raises(SystemExit) as exc_info:
677-
main_with_subcommands()
672+
with patch("builtins.print") as mock_print:
673+
with pytest.raises(SystemExit) as exc_info:
674+
main_with_subcommands()
678675

679-
assert exc_info.value.code == 1
680-
mock_print.assert_called_with("\n❌ Operation cancelled by user", file=sys.stderr)
676+
assert exc_info.value.code == 1
677+
mock_print.assert_called_with("\n❌ Operation cancelled by user", file=sys.stderr)
681678

682679

683680
def test_main_with_subcommands_include_dependencies_handling():
@@ -690,15 +687,14 @@ def test_main_with_subcommands_include_dependencies_handling():
690687

691688
mock_parser = MagicMock()
692689
mock_args = MagicMock()
693-
mock_args.func = MagicMock()
690+
mock_args.func = AsyncMock()
694691
mock_args.no_dependencies = True # This should set include_dependencies to False
695692
mock_parser.parse_args.return_value = mock_args
696693

697694
with patch.object(sys, "argv", ["mcpgateway", "export", "--no-dependencies"]):
698695
with patch("mcpgateway.cli_export_import.create_parser", return_value=mock_parser):
699-
with patch("mcpgateway.cli_export_import.asyncio.run") as mock_run:
700-
main_with_subcommands()
696+
main_with_subcommands()
701697

702-
# Verify include_dependencies was set to False (opposite of no_dependencies)
703-
assert mock_args.include_dependencies is False
704-
mock_run.assert_called_once_with(mock_args.func(mock_args))
698+
# Verify include_dependencies was set to False (opposite of no_dependencies)
699+
assert mock_args.include_dependencies is False
700+
mock_args.func.assert_called_once_with(mock_args)

0 commit comments

Comments
 (0)