@@ -1582,3 +1582,221 @@ def test_jsonpath_modifier_invalid_expressions(sample_people):
15821582
15831583 with pytest .raises (HTTPException ):
15841584 jsonpath_modifier (sample_people , "$[*]" , mappings = {"bad" : "$[" }) # invalid mapping expr
1585+
1586+
1587+ # ----------------------------------------------------- #
1588+ # Plugin Exception Handler Tests #
1589+ # ----------------------------------------------------- #
1590+ class TestPluginExceptionHandlers :
1591+ """Tests for plugin exception handlers: PluginViolationError and PluginError."""
1592+
1593+ def test_plugin_violation_exception_handler_with_full_violation (self ):
1594+ """Test plugin_violation_exception_handler with complete violation details."""
1595+ # Standard
1596+ import asyncio
1597+
1598+ # First-Party
1599+ from mcpgateway .main import plugin_violation_exception_handler
1600+ from mcpgateway .plugins .framework .errors import PluginViolationError
1601+ from mcpgateway .plugins .framework .models import PluginViolation
1602+
1603+ violation = PluginViolation (
1604+ reason = "Invalid input" ,
1605+ description = "The input contains prohibited content" ,
1606+ code = "PROHIBITED_CONTENT" ,
1607+ details = {"field" : "message" , "value" : "sensitive_data" },
1608+ )
1609+ violation ._plugin_name = "content_filter"
1610+ exc = PluginViolationError (message = "Policy violation detected" , violation = violation )
1611+
1612+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1613+
1614+ assert result .status_code == 200
1615+ content = json .loads (result .body .decode ())
1616+ assert "error" in content
1617+ assert content ["error" ]["code" ] == - 32602
1618+ assert "Plugin Violation:" in content ["error" ]["message" ]
1619+ assert "The input contains prohibited content" in content ["error" ]["message" ]
1620+ assert content ["error" ]["data" ]["description" ] == "The input contains prohibited content"
1621+ assert content ["error" ]["data" ]["details" ] == {"field" : "message" , "value" : "sensitive_data" }
1622+ assert content ["error" ]["data" ]["plugin_error_code" ] == "PROHIBITED_CONTENT"
1623+ assert content ["error" ]["data" ]["plugin_name" ] == "content_filter"
1624+
1625+ def test_plugin_violation_exception_handler_with_custom_mcp_error_code (self ):
1626+ """Test plugin_violation_exception_handler with custom MCP error code."""
1627+ # Standard
1628+ import asyncio
1629+
1630+ # First-Party
1631+ from mcpgateway .main import plugin_violation_exception_handler
1632+ from mcpgateway .plugins .framework .errors import PluginViolationError
1633+ from mcpgateway .plugins .framework .models import PluginViolation
1634+
1635+ violation = PluginViolation (
1636+ reason = "Rate limit exceeded" ,
1637+ description = "Too many requests from this client" ,
1638+ code = "RATE_LIMIT" ,
1639+ details = {"requests" : 100 , "limit" : 50 },
1640+ mcp_error_code = - 32000 , # Custom error code
1641+ )
1642+ violation ._plugin_name = "rate_limiter"
1643+ exc = PluginViolationError (message = "Rate limit violation" , violation = violation )
1644+
1645+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1646+
1647+ assert result .status_code == 200
1648+ content = json .loads (result .body .decode ())
1649+ assert content ["error" ]["code" ] == - 32000
1650+ assert "Too many requests from this client" in content ["error" ]["message" ]
1651+ assert content ["error" ]["data" ]["plugin_error_code" ] == "RATE_LIMIT"
1652+ assert content ["error" ]["data" ]["plugin_name" ] == "rate_limiter"
1653+
1654+ def test_plugin_violation_exception_handler_with_minimal_violation (self ):
1655+ """Test plugin_violation_exception_handler with minimal violation details."""
1656+ # Standard
1657+ import asyncio
1658+
1659+ # First-Party
1660+ from mcpgateway .main import plugin_violation_exception_handler
1661+ from mcpgateway .plugins .framework .errors import PluginViolationError
1662+ from mcpgateway .plugins .framework .models import PluginViolation
1663+
1664+ violation = PluginViolation (
1665+ reason = "Violation occurred" ,
1666+ description = "Minimal violation" ,
1667+ code = "MIN_VIOLATION" ,
1668+ details = {},
1669+ )
1670+ exc = PluginViolationError (message = "Minimal violation" , violation = violation )
1671+
1672+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1673+
1674+ assert result .status_code == 200
1675+ content = json .loads (result .body .decode ())
1676+ assert content ["error" ]["code" ] == - 32602
1677+ assert "Minimal violation" in content ["error" ]["message" ]
1678+ assert content ["error" ]["data" ]["plugin_error_code" ] == "MIN_VIOLATION"
1679+
1680+ def test_plugin_violation_exception_handler_without_violation_object (self ):
1681+ """Test plugin_violation_exception_handler when violation object is None."""
1682+ # Standard
1683+ import asyncio
1684+
1685+ # First-Party
1686+ from mcpgateway .main import plugin_violation_exception_handler
1687+ from mcpgateway .plugins .framework .errors import PluginViolationError
1688+
1689+ exc = PluginViolationError (message = "Generic plugin violation" , violation = None )
1690+
1691+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1692+
1693+ assert result .status_code == 200
1694+ content = json .loads (result .body .decode ())
1695+ assert content ["error" ]["code" ] == - 32602
1696+ assert "A plugin violation occurred" in content ["error" ]["message" ]
1697+ assert content ["error" ]["data" ] == {}
1698+
1699+ def test_plugin_exception_handler_with_full_error (self ):
1700+ """Test plugin_exception_handler with complete error details."""
1701+ # Standard
1702+ import asyncio
1703+
1704+ # First-Party
1705+ from mcpgateway .main import plugin_exception_handler
1706+ from mcpgateway .plugins .framework .errors import PluginError
1707+ from mcpgateway .plugins .framework .models import PluginErrorModel
1708+
1709+ error = PluginErrorModel (
1710+ message = "Plugin execution failed" ,
1711+ code = "EXECUTION_ERROR" ,
1712+ plugin_name = "data_processor" ,
1713+ details = {"error_type" : "timeout" , "duration" : 30 },
1714+ )
1715+ exc = PluginError (error = error )
1716+
1717+ result = asyncio .run (plugin_exception_handler (None , exc ))
1718+
1719+ assert result .status_code == 200
1720+ content = json .loads (result .body .decode ())
1721+ assert "error" in content
1722+ assert content ["error" ]["code" ] == - 32603
1723+ assert "Plugin Error:" in content ["error" ]["message" ]
1724+ assert "Plugin execution failed" in content ["error" ]["message" ]
1725+ assert content ["error" ]["data" ]["details" ] == {"error_type" : "timeout" , "duration" : 30 }
1726+ assert content ["error" ]["data" ]["plugin_error_code" ] == "EXECUTION_ERROR"
1727+ assert content ["error" ]["data" ]["plugin_name" ] == "data_processor"
1728+
1729+ def test_plugin_exception_handler_with_custom_mcp_error_code (self ):
1730+ """Test plugin_exception_handler with custom MCP error code."""
1731+ # Standard
1732+ import asyncio
1733+
1734+ # First-Party
1735+ from mcpgateway .main import plugin_exception_handler
1736+ from mcpgateway .plugins .framework .errors import PluginError
1737+ from mcpgateway .plugins .framework .models import PluginErrorModel
1738+
1739+ error = PluginErrorModel (
1740+ message = "Custom error occurred" ,
1741+ code = "CUSTOM_ERROR" ,
1742+ plugin_name = "custom_plugin" ,
1743+ details = {"context" : "test" },
1744+ mcp_error_code = - 32001 , # Custom MCP error code
1745+ )
1746+ exc = PluginError (error = error )
1747+
1748+ result = asyncio .run (plugin_exception_handler (None , exc ))
1749+
1750+ assert result .status_code == 200
1751+ content = json .loads (result .body .decode ())
1752+ assert content ["error" ]["code" ] == - 32001
1753+ assert "Custom error occurred" in content ["error" ]["message" ]
1754+ assert content ["error" ]["data" ]["plugin_error_code" ] == "CUSTOM_ERROR"
1755+
1756+ def test_plugin_exception_handler_with_minimal_error (self ):
1757+ """Test plugin_exception_handler with minimal error details."""
1758+ # Standard
1759+ import asyncio
1760+
1761+ # First-Party
1762+ from mcpgateway .main import plugin_exception_handler
1763+ from mcpgateway .plugins .framework .errors import PluginError
1764+ from mcpgateway .plugins .framework .models import PluginErrorModel
1765+
1766+ error = PluginErrorModel (message = "Minimal error" , plugin_name = "minimal_plugin" )
1767+ exc = PluginError (error = error )
1768+
1769+ result = asyncio .run (plugin_exception_handler (None , exc ))
1770+
1771+ assert result .status_code == 200
1772+ content = json .loads (result .body .decode ())
1773+ assert content ["error" ]["code" ] == - 32603
1774+ assert "Minimal error" in content ["error" ]["message" ]
1775+ assert content ["error" ]["data" ]["plugin_name" ] == "minimal_plugin"
1776+
1777+ def test_plugin_exception_handler_with_empty_code (self ):
1778+ """Test plugin_exception_handler when error has empty code field."""
1779+ # Standard
1780+ import asyncio
1781+
1782+ # First-Party
1783+ from mcpgateway .main import plugin_exception_handler
1784+ from mcpgateway .plugins .framework .errors import PluginError
1785+ from mcpgateway .plugins .framework .models import PluginErrorModel
1786+
1787+ error = PluginErrorModel (
1788+ message = "Error without code" ,
1789+ code = "" ,
1790+ plugin_name = "test_plugin" ,
1791+ details = {"info" : "test" },
1792+ )
1793+ exc = PluginError (error = error )
1794+
1795+ result = asyncio .run (plugin_exception_handler (None , exc ))
1796+
1797+ assert result .status_code == 200
1798+ content = json .loads (result .body .decode ())
1799+ assert content ["error" ]["code" ] == - 32603
1800+ assert "Error without code" in content ["error" ]["message" ]
1801+ # Empty code should not be included in data
1802+ assert "plugin_error_code" not in content ["error" ]["data" ] or content ["error" ]["data" ]["plugin_error_code" ] == ""
0 commit comments