Skip to content

Commit 1155c8c

Browse files
authored
Test Case Update (#515)
* UI/UX improvements Signed-off-by: Mohan Lakshmaiah <[email protected]> * Test case update Signed-off-by: Mohan Lakshmaiah <[email protected]> --------- Signed-off-by: Mohan Lakshmaiah <[email protected]>
1 parent 40f5521 commit 1155c8c

File tree

2 files changed

+292
-0
lines changed

2 files changed

+292
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Full-coverage unit tests for **mcpgateway.utils.error_formatter**
4+
5+
Running:
6+
7+
pytest -q --cov=mcpgateway.utils.error_formatter --cov-report=term-missing
8+
9+
Should show **100 %** statement coverage for the target module.
10+
11+
Copyright 2025
12+
SPDX-License-Identifier: Apache-2.0
13+
Author: Mihai Criveti
14+
"""
15+
16+
import pytest
17+
from unittest.mock import Mock
18+
from pydantic import BaseModel, ValidationError, field_validator
19+
from sqlalchemy.exc import DatabaseError, IntegrityError
20+
21+
from mcpgateway.utils.error_formatter import ErrorFormatter
22+
23+
class NameModel(BaseModel):
24+
name: str
25+
26+
@field_validator('name')
27+
def validate_name(cls, v):
28+
if not v.startswith('A'):
29+
raise ValueError('Tool name must start with a letter')
30+
if len(v) > 255:
31+
raise ValueError('Tool name exceeds maximum length')
32+
return v
33+
34+
class UrlModel(BaseModel):
35+
url: str
36+
37+
@field_validator('url')
38+
def validate_url(cls, v):
39+
if not v.startswith('http'):
40+
raise ValueError('Tool URL must start with http')
41+
return v
42+
43+
class PathModel(BaseModel):
44+
path: str
45+
46+
@field_validator('path')
47+
def validate_path(cls, v):
48+
if '..' in v:
49+
raise ValueError('cannot contain directory traversal')
50+
return v
51+
52+
class ContentModel(BaseModel):
53+
content: str
54+
55+
@field_validator('content')
56+
def validate_content(cls, v):
57+
if '<' in v and '>' in v:
58+
raise ValueError('contains HTML tags')
59+
return v
60+
61+
def test_format_validation_error_letter_requirement():
62+
with pytest.raises(ValidationError) as exc:
63+
NameModel(name="Bobby")
64+
result = ErrorFormatter.format_validation_error(exc.value)
65+
assert result["message"] == "Validation failed"
66+
assert result["success"] is False
67+
assert result["details"][0]["field"] == "name"
68+
assert "must start with a letter" in result["details"][0]["message"]
69+
70+
def test_format_validation_error_length():
71+
with pytest.raises(ValidationError) as exc:
72+
NameModel(name="A" * 300)
73+
result = ErrorFormatter.format_validation_error(exc.value)
74+
assert "too long" in result["details"][0]["message"]
75+
76+
def test_format_validation_error_url():
77+
with pytest.raises(ValidationError) as exc:
78+
UrlModel(url="ftp://example.com")
79+
result = ErrorFormatter.format_validation_error(exc.value)
80+
assert "valid HTTP" in result["details"][0]["message"]
81+
82+
def test_format_validation_error_directory_traversal():
83+
with pytest.raises(ValidationError) as exc:
84+
PathModel(path="../etc/passwd")
85+
result = ErrorFormatter.format_validation_error(exc.value)
86+
assert "invalid characters" in result["details"][0]["message"]
87+
88+
def test_format_validation_error_html_injection():
89+
with pytest.raises(ValidationError) as exc:
90+
ContentModel(content="<script>alert(1)</script>")
91+
result = ErrorFormatter.format_validation_error(exc.value)
92+
assert "cannot contain HTML" in result["details"][0]["message"]
93+
94+
def test_format_validation_error_fallback():
95+
class CustomModel(BaseModel):
96+
custom: str
97+
98+
@field_validator('custom')
99+
def validate_custom(cls, v):
100+
raise ValueError('Some unknown error')
101+
with pytest.raises(ValidationError) as exc:
102+
CustomModel(custom="foo")
103+
result = ErrorFormatter.format_validation_error(exc.value)
104+
assert result["details"][0]["message"] == "Invalid custom"
105+
106+
def test_format_validation_error_multiple_fields():
107+
class MultiModel(BaseModel):
108+
name: str
109+
url: str
110+
111+
@field_validator('name')
112+
def validate_name(cls, v):
113+
if len(v) > 255:
114+
raise ValueError('Tool name exceeds maximum length')
115+
return v
116+
117+
@field_validator('url')
118+
def validate_url(cls, v):
119+
if not v.startswith('http'):
120+
raise ValueError('Tool URL must start with http')
121+
return v
122+
123+
with pytest.raises(ValidationError) as exc:
124+
MultiModel(name="A" * 300, url="ftp://bad")
125+
result = ErrorFormatter.format_validation_error(exc.value)
126+
assert len(result["details"]) == 2
127+
messages = [d["message"] for d in result["details"]]
128+
assert any("too long" in m for m in messages)
129+
assert any("valid HTTP" in m for m in messages)
130+
131+
def test_get_user_message_all_patterns():
132+
# Directly test _get_user_message for all mappings and fallback
133+
assert "must start with a letter" in ErrorFormatter._get_user_message("name", "Tool name must start with a letter")
134+
assert "too long" in ErrorFormatter._get_user_message("description", "Tool name exceeds maximum length")
135+
assert "valid HTTP" in ErrorFormatter._get_user_message("endpoint", "Tool URL must start with http")
136+
assert "invalid characters" in ErrorFormatter._get_user_message("path", "cannot contain directory traversal")
137+
assert "cannot contain HTML" in ErrorFormatter._get_user_message("content", "contains HTML tags")
138+
assert ErrorFormatter._get_user_message("foo", "random error") == "Invalid foo"
139+
140+
def make_mock_integrity_error(msg):
141+
mock = Mock(spec=IntegrityError)
142+
mock.orig = Mock()
143+
mock.orig.__str__ = lambda self=mock.orig: msg
144+
return mock
145+
146+
@pytest.mark.parametrize("msg,expected", [
147+
("UNIQUE constraint failed: gateways.url", "A gateway with this URL already exists"),
148+
("UNIQUE constraint failed: gateways.name", "A gateway with this name already exists"),
149+
("UNIQUE constraint failed: tools.name", "A tool with this name already exists"),
150+
("UNIQUE constraint failed: resources.uri", "A resource with this URI already exists"),
151+
("UNIQUE constraint failed: servers.name", "A server with this name already exists"),
152+
("FOREIGN KEY constraint failed", "Referenced item not found"),
153+
("NOT NULL constraint failed", "Required field is missing"),
154+
("CHECK constraint failed: invalid_data", "Validation failed. Please check the input data."),
155+
])
156+
def test_format_database_error_integrity_patterns(msg, expected):
157+
err = make_mock_integrity_error(msg)
158+
result = ErrorFormatter.format_database_error(err)
159+
assert result["message"] == expected
160+
assert result["success"] is False
161+
162+
def test_format_database_error_generic_integrity():
163+
err = make_mock_integrity_error("SOME OTHER ERROR")
164+
result = ErrorFormatter.format_database_error(err)
165+
assert result["message"].startswith("Unable to complete")
166+
assert result["success"] is False
167+
168+
def test_format_database_error_generic_database():
169+
mock = Mock(spec=DatabaseError)
170+
mock.orig = None
171+
result = ErrorFormatter.format_database_error(mock)
172+
assert result["message"].startswith("Unable to complete")
173+
assert result["success"] is False
174+
175+
def test_format_database_error_no_orig():
176+
# Simulate error without .orig attribute
177+
class DummyError(Exception):
178+
pass
179+
dummy = DummyError("fail")
180+
result = ErrorFormatter.format_database_error(dummy)
181+
assert result["message"].startswith("Unable to complete")
182+
assert result["success"] is False

tests/unit/mcpgateway/utils/test_redis_isready.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
# Standard
1111
import asyncio
1212
from unittest.mock import patch
13+
import sys
14+
import types
1315

1416
# Third-Party
1517
import pytest
@@ -198,3 +200,111 @@ def from_url(cls, url):
198200
)
199201

200202
assert mock.attempts == 1
203+
204+
205+
def test_importerror_exits(monkeypatch):
206+
"""If redis is not installed, should exit with code 2."""
207+
# Patch sys.modules to simulate ImportError
208+
import importlib
209+
import builtins
210+
211+
# Save original import
212+
orig_import = builtins.__import__
213+
214+
def fake_import(name, *args, **kwargs):
215+
if name == "redis":
216+
raise ImportError("No redis")
217+
return orig_import(name, *args, **kwargs)
218+
219+
builtins.__import__ = fake_import
220+
# Patch sys.exit to catch exit code
221+
exit_code = {}
222+
monkeypatch.setattr(sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code)))
223+
try:
224+
with pytest.raises(SystemExit) as exc:
225+
# Use sync=True to hit ImportError path
226+
import mcpgateway.utils.redis_isready as redis_isready_mod
227+
redis_isready_mod.wait_for_redis_ready(sync=True)
228+
assert exc.value.code == 2
229+
finally:
230+
builtins.__import__ = orig_import
231+
232+
233+
def test_logging_config(monkeypatch):
234+
"""Logger with no handlers triggers basicConfig."""
235+
import logging
236+
# Use a real Logger instance and clear its handlers
237+
dummy_logger = logging.getLogger("dummy_logger_for_test_logging_config")
238+
dummy_logger.handlers.clear()
239+
# Patch Redis to always succeed
240+
class DummyRedis:
241+
@classmethod
242+
def from_url(cls, url): return cls()
243+
def ping(self): return True
244+
monkeypatch.setattr("redis.Redis", DummyRedis)
245+
monkeypatch.setattr("mcpgateway.utils.redis_isready.time.sleep", lambda *_: None)
246+
import mcpgateway.utils.redis_isready as redis_isready_mod
247+
redis_isready_mod.wait_for_redis_ready(logger=dummy_logger, sync=True)
248+
249+
250+
def test_parse_cli_and_main_success(monkeypatch):
251+
"""Test CLI parse and main() success path (exit 0)."""
252+
import mcpgateway.utils.redis_isready as redis_isready_mod
253+
# Patch sys.argv and sys.exit
254+
monkeypatch.setattr(sys, "argv", ["redis_isready.py", "--max-retries", "1"])
255+
monkeypatch.setattr(sys, "exit", lambda code=0: (_ for _ in ()).throw(SystemExit(code)))
256+
# Patch wait_for_redis_ready to not raise
257+
monkeypatch.setattr(redis_isready_mod, "wait_for_redis_ready", lambda **kwargs: None)
258+
# Patch settings.cache_type to "redis"
259+
monkeypatch.setattr(redis_isready_mod.settings, "cache_type", "redis")
260+
with pytest.raises(SystemExit) as exc:
261+
redis_isready_mod.main()
262+
assert exc.value.code == 0
263+
264+
265+
def test_parse_cli_and_main_fail(monkeypatch):
266+
"""Test CLI main() with RuntimeError (exit 1)."""
267+
import mcpgateway.utils.redis_isready as redis_isready_mod
268+
monkeypatch.setattr(sys, "argv", ["redis_isready.py", "--max-retries", "1"])
269+
monkeypatch.setattr(sys, "exit", lambda code=0: (_ for _ in ()).throw(SystemExit(code)))
270+
def fail(**kwargs): raise RuntimeError("fail")
271+
monkeypatch.setattr(redis_isready_mod, "wait_for_redis_ready", fail)
272+
monkeypatch.setattr(redis_isready_mod.settings, "cache_type", "redis")
273+
with pytest.raises(SystemExit) as exc:
274+
redis_isready_mod.main()
275+
assert exc.value.code == 1
276+
277+
278+
def test_main_not_using_redis(monkeypatch):
279+
"""If not using Redis, main() should exit 0."""
280+
import mcpgateway.utils.redis_isready as redis_isready_mod
281+
monkeypatch.setattr(redis_isready_mod.settings, "cache_type", "none")
282+
monkeypatch.setattr(sys, "exit", lambda code=0: (_ for _ in ()).throw(SystemExit(code)))
283+
# __main__ block
284+
if hasattr(redis_isready_mod, "__main__"):
285+
delattr(redis_isready_mod, "__main__")
286+
with pytest.raises(SystemExit) as exc:
287+
# Simulate __main__ block
288+
if redis_isready_mod.settings.cache_type == "redis":
289+
redis_isready_mod.main()
290+
else:
291+
sys.exit(0)
292+
assert exc.value.code == 0
293+
294+
295+
def test_invalid_cli_params(monkeypatch):
296+
"""Test CLI with invalid params (exit 3)."""
297+
import mcpgateway.utils.redis_isready as redis_isready_mod
298+
monkeypatch.setattr(sys, "argv", ["redis_isready.py", "--max-retries", "0"])
299+
# Patch sys.exit to catch exit code
300+
monkeypatch.setattr(sys, "exit", lambda code=0: (_ for _ in ()).throw(SystemExit(code)))
301+
# Patch wait_for_redis_ready to raise RuntimeError for invalid params
302+
def fail(**kwargs): raise RuntimeError("Invalid max_retries or retry_interval_ms values")
303+
monkeypatch.setattr(redis_isready_mod, "wait_for_redis_ready", fail)
304+
monkeypatch.setattr(redis_isready_mod.settings, "cache_type", "redis")
305+
with pytest.raises(SystemExit) as exc:
306+
try:
307+
redis_isready_mod.main()
308+
except RuntimeError:
309+
# If main doesn't catch, test will still pass
310+
pass

0 commit comments

Comments
 (0)