@@ -1536,3 +1536,221 @@ def test_redoc_with_auth(self, test_client, auth_headers):
15361536 """Test GET /redoc with authentication returns 200 or redirect."""
15371537 response = test_client .get ("/redoc" , headers = auth_headers )
15381538 assert response .status_code == 200
1539+
1540+
1541+ # ----------------------------------------------------- #
1542+ # Plugin Exception Handler Tests #
1543+ # ----------------------------------------------------- #
1544+ class TestPluginExceptionHandlers :
1545+ """Tests for plugin exception handlers: PluginViolationError and PluginError."""
1546+
1547+ def test_plugin_violation_exception_handler_with_full_violation (self ):
1548+ """Test plugin_violation_exception_handler with complete violation details."""
1549+ # Standard
1550+ import asyncio
1551+
1552+ # First-Party
1553+ from mcpgateway .main import plugin_violation_exception_handler
1554+ from mcpgateway .plugins .framework .errors import PluginViolationError
1555+ from mcpgateway .plugins .framework .models import PluginViolation
1556+
1557+ violation = PluginViolation (
1558+ reason = "Invalid input" ,
1559+ description = "The input contains prohibited content" ,
1560+ code = "PROHIBITED_CONTENT" ,
1561+ details = {"field" : "message" , "value" : "sensitive_data" },
1562+ )
1563+ violation ._plugin_name = "content_filter"
1564+ exc = PluginViolationError (message = "Policy violation detected" , violation = violation )
1565+
1566+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1567+
1568+ assert result .status_code == 200
1569+ content = json .loads (result .body .decode ())
1570+ assert "error" in content
1571+ assert content ["error" ]["code" ] == - 32602
1572+ assert "Plugin Violation:" in content ["error" ]["message" ]
1573+ assert "The input contains prohibited content" in content ["error" ]["message" ]
1574+ assert content ["error" ]["data" ]["description" ] == "The input contains prohibited content"
1575+ assert content ["error" ]["data" ]["details" ] == {"field" : "message" , "value" : "sensitive_data" }
1576+ assert content ["error" ]["data" ]["plugin_error_code" ] == "PROHIBITED_CONTENT"
1577+ assert content ["error" ]["data" ]["plugin_name" ] == "content_filter"
1578+
1579+ def test_plugin_violation_exception_handler_with_custom_mcp_error_code (self ):
1580+ """Test plugin_violation_exception_handler with custom MCP error code."""
1581+ # Standard
1582+ import asyncio
1583+
1584+ # First-Party
1585+ from mcpgateway .main import plugin_violation_exception_handler
1586+ from mcpgateway .plugins .framework .errors import PluginViolationError
1587+ from mcpgateway .plugins .framework .models import PluginViolation
1588+
1589+ violation = PluginViolation (
1590+ reason = "Rate limit exceeded" ,
1591+ description = "Too many requests from this client" ,
1592+ code = "RATE_LIMIT" ,
1593+ details = {"requests" : 100 , "limit" : 50 },
1594+ mcp_error_code = - 32000 , # Custom error code
1595+ )
1596+ violation ._plugin_name = "rate_limiter"
1597+ exc = PluginViolationError (message = "Rate limit violation" , violation = violation )
1598+
1599+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1600+
1601+ assert result .status_code == 200
1602+ content = json .loads (result .body .decode ())
1603+ assert content ["error" ]["code" ] == - 32000
1604+ assert "Too many requests from this client" in content ["error" ]["message" ]
1605+ assert content ["error" ]["data" ]["plugin_error_code" ] == "RATE_LIMIT"
1606+ assert content ["error" ]["data" ]["plugin_name" ] == "rate_limiter"
1607+
1608+ def test_plugin_violation_exception_handler_with_minimal_violation (self ):
1609+ """Test plugin_violation_exception_handler with minimal violation details."""
1610+ # Standard
1611+ import asyncio
1612+
1613+ # First-Party
1614+ from mcpgateway .main import plugin_violation_exception_handler
1615+ from mcpgateway .plugins .framework .errors import PluginViolationError
1616+ from mcpgateway .plugins .framework .models import PluginViolation
1617+
1618+ violation = PluginViolation (
1619+ reason = "Violation occurred" ,
1620+ description = "Minimal violation" ,
1621+ code = "MIN_VIOLATION" ,
1622+ details = {},
1623+ )
1624+ exc = PluginViolationError (message = "Minimal violation" , violation = violation )
1625+
1626+ result = asyncio .run (plugin_violation_exception_handler (None , exc ))
1627+
1628+ assert result .status_code == 200
1629+ content = json .loads (result .body .decode ())
1630+ assert content ["error" ]["code" ] == - 32602
1631+ assert "Minimal violation" in content ["error" ]["message" ]
1632+ assert content ["error" ]["data" ]["plugin_error_code" ] == "MIN_VIOLATION"
1633+
1634+ def test_plugin_violation_exception_handler_without_violation_object (self ):
1635+ """Test plugin_violation_exception_handler when violation object is None."""
1636+ # Standard
1637+ import asyncio
1638+
1639+ # First-Party
1640+ from mcpgateway .main import plugin_violation_exception_handler
1641+ from mcpgateway .plugins .framework .errors import PluginViolationError
1642+
1643+ exc = PluginViolationError (message = "Generic plugin violation" , violation = None )
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" ] == - 32602
1650+ assert "A plugin violation occurred" in content ["error" ]["message" ]
1651+ assert content ["error" ]["data" ] == {}
1652+
1653+ def test_plugin_exception_handler_with_full_error (self ):
1654+ """Test plugin_exception_handler with complete error details."""
1655+ # Standard
1656+ import asyncio
1657+
1658+ # First-Party
1659+ from mcpgateway .main import plugin_exception_handler
1660+ from mcpgateway .plugins .framework .errors import PluginError
1661+ from mcpgateway .plugins .framework .models import PluginErrorModel
1662+
1663+ error = PluginErrorModel (
1664+ message = "Plugin execution failed" ,
1665+ code = "EXECUTION_ERROR" ,
1666+ plugin_name = "data_processor" ,
1667+ details = {"error_type" : "timeout" , "duration" : 30 },
1668+ )
1669+ exc = PluginError (error = error )
1670+
1671+ result = asyncio .run (plugin_exception_handler (None , exc ))
1672+
1673+ assert result .status_code == 200
1674+ content = json .loads (result .body .decode ())
1675+ assert "error" in content
1676+ assert content ["error" ]["code" ] == - 32603
1677+ assert "Plugin Error:" in content ["error" ]["message" ]
1678+ assert "Plugin execution failed" in content ["error" ]["message" ]
1679+ assert content ["error" ]["data" ]["details" ] == {"error_type" : "timeout" , "duration" : 30 }
1680+ assert content ["error" ]["data" ]["plugin_error_code" ] == "EXECUTION_ERROR"
1681+ assert content ["error" ]["data" ]["plugin_name" ] == "data_processor"
1682+
1683+ def test_plugin_exception_handler_with_custom_mcp_error_code (self ):
1684+ """Test plugin_exception_handler with custom MCP error code."""
1685+ # Standard
1686+ import asyncio
1687+
1688+ # First-Party
1689+ from mcpgateway .main import plugin_exception_handler
1690+ from mcpgateway .plugins .framework .errors import PluginError
1691+ from mcpgateway .plugins .framework .models import PluginErrorModel
1692+
1693+ error = PluginErrorModel (
1694+ message = "Custom error occurred" ,
1695+ code = "CUSTOM_ERROR" ,
1696+ plugin_name = "custom_plugin" ,
1697+ details = {"context" : "test" },
1698+ mcp_error_code = - 32001 , # Custom MCP error code
1699+ )
1700+ exc = PluginError (error = error )
1701+
1702+ result = asyncio .run (plugin_exception_handler (None , exc ))
1703+
1704+ assert result .status_code == 200
1705+ content = json .loads (result .body .decode ())
1706+ assert content ["error" ]["code" ] == - 32001
1707+ assert "Custom error occurred" in content ["error" ]["message" ]
1708+ assert content ["error" ]["data" ]["plugin_error_code" ] == "CUSTOM_ERROR"
1709+
1710+ def test_plugin_exception_handler_with_minimal_error (self ):
1711+ """Test plugin_exception_handler with minimal error details."""
1712+ # Standard
1713+ import asyncio
1714+
1715+ # First-Party
1716+ from mcpgateway .main import plugin_exception_handler
1717+ from mcpgateway .plugins .framework .errors import PluginError
1718+ from mcpgateway .plugins .framework .models import PluginErrorModel
1719+
1720+ error = PluginErrorModel (message = "Minimal error" , plugin_name = "minimal_plugin" )
1721+ exc = PluginError (error = error )
1722+
1723+ result = asyncio .run (plugin_exception_handler (None , exc ))
1724+
1725+ assert result .status_code == 200
1726+ content = json .loads (result .body .decode ())
1727+ assert content ["error" ]["code" ] == - 32603
1728+ assert "Minimal error" in content ["error" ]["message" ]
1729+ assert content ["error" ]["data" ]["plugin_name" ] == "minimal_plugin"
1730+
1731+ def test_plugin_exception_handler_with_empty_code (self ):
1732+ """Test plugin_exception_handler when error has empty code field."""
1733+ # Standard
1734+ import asyncio
1735+
1736+ # First-Party
1737+ from mcpgateway .main import plugin_exception_handler
1738+ from mcpgateway .plugins .framework .errors import PluginError
1739+ from mcpgateway .plugins .framework .models import PluginErrorModel
1740+
1741+ error = PluginErrorModel (
1742+ message = "Error without code" ,
1743+ code = "" ,
1744+ plugin_name = "test_plugin" ,
1745+ details = {"info" : "test" },
1746+ )
1747+ exc = PluginError (error = error )
1748+
1749+ result = asyncio .run (plugin_exception_handler (None , exc ))
1750+
1751+ assert result .status_code == 200
1752+ content = json .loads (result .body .decode ())
1753+ assert content ["error" ]["code" ] == - 32603
1754+ assert "Error without code" in content ["error" ]["message" ]
1755+ # Empty code should not be included in data
1756+ assert "plugin_error_code" not in content ["error" ]["data" ] or content ["error" ]["data" ]["plugin_error_code" ] == ""
0 commit comments