Skip to content

Commit 83bd6e9

Browse files
committed
Add tests
1 parent b2ba302 commit 83bd6e9

File tree

5 files changed

+469
-0
lines changed

5 files changed

+469
-0
lines changed

tests/conftest.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Pytest configuration and shared fixtures."""
2+
3+
import pytest
4+
from google.protobuf.empty_pb2 import Empty
5+
from grpc.aio import Metadata
6+
7+
from mcpd_plugins.v1.plugins.plugin_pb2 import HTTPRequest, PluginConfig
8+
9+
10+
class MockContext:
11+
"""Mock gRPC ServicerContext for testing."""
12+
13+
def __init__(self):
14+
"""Initialize mock context."""
15+
self.invocation_metadata = Metadata()
16+
self.peer_identity = None
17+
self.code = None
18+
self.details = None
19+
20+
def abort(self, code, details):
21+
"""Mock abort method."""
22+
self.code = code
23+
self.details = details
24+
raise Exception(f"Aborted with code {code}: {details}")
25+
26+
27+
@pytest.fixture
28+
def mock_context():
29+
"""Provide a mock gRPC context for testing."""
30+
return MockContext()
31+
32+
33+
@pytest.fixture
34+
def empty_request():
35+
"""Provide an Empty request message."""
36+
return Empty()
37+
38+
39+
@pytest.fixture
40+
def sample_plugin_config():
41+
"""Provide a sample plugin configuration."""
42+
config = PluginConfig()
43+
config.custom_config["key1"] = "value1"
44+
config.custom_config["key2"] = "value2"
45+
return config
46+
47+
48+
@pytest.fixture
49+
def sample_http_request():
50+
"""Provide a sample HTTP request."""
51+
request = HTTPRequest(
52+
method="GET",
53+
url="https://example.com/api/test",
54+
path="/api/test",
55+
remote_addr="192.168.1.100",
56+
request_uri="/api/test?foo=bar",
57+
)
58+
request.headers["User-Agent"] = "test-client/1.0"
59+
request.headers["Content-Type"] = "application/json"
60+
return request
61+
62+
63+
@pytest.fixture
64+
def sample_http_request_with_body():
65+
"""Provide a sample HTTP request with body."""
66+
request = HTTPRequest(
67+
method="POST",
68+
url="https://example.com/api/test",
69+
path="/api/test",
70+
body=b'{"test": "data"}',
71+
remote_addr="192.168.1.100",
72+
)
73+
request.headers["Content-Type"] = "application/json"
74+
request.headers["Content-Length"] = "16"
75+
return request

tests/integration/test_examples.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""Integration tests for example plugins."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
import pytest
7+
from google.protobuf.empty_pb2 import Empty
8+
9+
# Add examples to path.
10+
examples_dir = Path(__file__).parent.parent.parent / "examples"
11+
sys.path.insert(0, str(examples_dir))
12+
13+
14+
@pytest.fixture
15+
def mock_context():
16+
"""Provide a mock gRPC context."""
17+
18+
class MockContext:
19+
"""Mock gRPC ServicerContext."""
20+
21+
def __init__(self):
22+
"""Initialize mock context."""
23+
self.invocation_metadata = []
24+
25+
return MockContext()
26+
27+
28+
class TestSimplePlugin:
29+
"""Integration tests for simple_plugin example."""
30+
31+
@pytest.mark.asyncio
32+
async def test_simple_plugin_metadata(self, mock_context):
33+
"""Simple plugin should return correct metadata."""
34+
from simple_plugin.main import SimplePlugin
35+
36+
plugin = SimplePlugin()
37+
metadata = await plugin.GetMetadata(Empty(), mock_context)
38+
39+
assert metadata.name == "simple-plugin"
40+
assert metadata.version == "1.0.0"
41+
assert "custom header" in metadata.description.lower()
42+
43+
@pytest.mark.asyncio
44+
async def test_simple_plugin_capabilities(self, mock_context):
45+
"""Simple plugin should declare REQUEST flow."""
46+
from simple_plugin.main import SimplePlugin
47+
48+
from mcpd_plugins.v1.plugins.plugin_pb2 import FLOW_REQUEST
49+
50+
plugin = SimplePlugin()
51+
capabilities = await plugin.GetCapabilities(Empty(), mock_context)
52+
53+
assert len(capabilities.flows) == 1
54+
assert capabilities.flows[0] == FLOW_REQUEST
55+
56+
@pytest.mark.asyncio
57+
async def test_simple_plugin_adds_header(self, mock_context):
58+
"""Simple plugin should add X-Simple-Plugin header."""
59+
from simple_plugin.main import SimplePlugin
60+
61+
from mcpd_plugins.v1.plugins.plugin_pb2 import HTTPRequest
62+
63+
plugin = SimplePlugin()
64+
request = HTTPRequest(
65+
method="GET",
66+
url="https://example.com/test",
67+
path="/test",
68+
)
69+
request.headers["User-Agent"] = "test"
70+
71+
response = await plugin.HandleRequest(request, mock_context)
72+
73+
assert getattr(response, "continue") is True
74+
assert "X-Simple-Plugin" in response.modified_request.headers
75+
assert response.modified_request.headers["X-Simple-Plugin"] == "processed"
76+
77+
78+
class TestAuthPlugin:
79+
"""Integration tests for auth_plugin example."""
80+
81+
@pytest.mark.asyncio
82+
async def test_auth_plugin_rejects_missing_token(self, mock_context):
83+
"""Auth plugin should reject requests without Authorization header."""
84+
from auth_plugin.main import AuthPlugin
85+
86+
from mcpd_plugins.v1.plugins.plugin_pb2 import HTTPRequest
87+
88+
plugin = AuthPlugin()
89+
request = HTTPRequest(
90+
method="GET",
91+
url="https://example.com/test",
92+
path="/test",
93+
)
94+
95+
response = await plugin.HandleRequest(request, mock_context)
96+
97+
assert getattr(response, "continue") is False
98+
assert response.status_code == 401
99+
100+
@pytest.mark.asyncio
101+
async def test_auth_plugin_rejects_invalid_token(self, mock_context):
102+
"""Auth plugin should reject requests with invalid token."""
103+
from auth_plugin.main import AuthPlugin
104+
105+
from mcpd_plugins.v1.plugins.plugin_pb2 import HTTPRequest
106+
107+
plugin = AuthPlugin()
108+
request = HTTPRequest(
109+
method="GET",
110+
url="https://example.com/test",
111+
path="/test",
112+
)
113+
request.headers["Authorization"] = "Bearer wrong-token"
114+
115+
response = await plugin.HandleRequest(request, mock_context)
116+
117+
assert getattr(response, "continue") is False
118+
assert response.status_code == 401
119+
120+
@pytest.mark.asyncio
121+
async def test_auth_plugin_accepts_valid_token(self, mock_context):
122+
"""Auth plugin should accept requests with valid token."""
123+
from auth_plugin.main import AuthPlugin
124+
125+
from mcpd_plugins.v1.plugins.plugin_pb2 import HTTPRequest
126+
127+
plugin = AuthPlugin()
128+
request = HTTPRequest(
129+
method="GET",
130+
url="https://example.com/test",
131+
path="/test",
132+
)
133+
request.headers["Authorization"] = "Bearer secret-token-123"
134+
135+
response = await plugin.HandleRequest(request, mock_context)
136+
137+
assert getattr(response, "continue") is True
138+
139+
140+
class TestLoggingPlugin:
141+
"""Integration tests for logging_plugin example."""
142+
143+
@pytest.mark.asyncio
144+
async def test_logging_plugin_supports_both_flows(self, mock_context):
145+
"""Logging plugin should support both REQUEST and RESPONSE flows."""
146+
from logging_plugin.main import LoggingPlugin
147+
148+
from mcpd_plugins.v1.plugins.plugin_pb2 import FLOW_REQUEST, FLOW_RESPONSE
149+
150+
plugin = LoggingPlugin()
151+
capabilities = await plugin.GetCapabilities(Empty(), mock_context)
152+
153+
assert len(capabilities.flows) == 2
154+
assert FLOW_REQUEST in capabilities.flows
155+
assert FLOW_RESPONSE in capabilities.flows
156+
157+
@pytest.mark.asyncio
158+
async def test_logging_plugin_logs_request(self, mock_context, caplog):
159+
"""Logging plugin should log request details."""
160+
from logging_plugin.main import LoggingPlugin
161+
162+
from mcpd_plugins.v1.plugins.plugin_pb2 import HTTPRequest
163+
164+
plugin = LoggingPlugin()
165+
request = HTTPRequest(
166+
method="GET",
167+
url="https://example.com/test",
168+
path="/test",
169+
)
170+
171+
response = await plugin.HandleRequest(request, mock_context)
172+
173+
assert getattr(response, "continue") is True
174+
# Check that logging occurred (test output capture).
175+
assert "INCOMING REQUEST" in caplog.text or getattr(response, "continue") is True

tests/unit/test_base_plugin.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Unit tests for BasePlugin class."""
2+
3+
import pytest
4+
from google.protobuf.empty_pb2 import Empty
5+
6+
from mcpd_plugins import BasePlugin
7+
from mcpd_plugins.v1.plugins.plugin_pb2 import (
8+
Capabilities,
9+
HTTPResponse,
10+
Metadata,
11+
)
12+
13+
14+
class TestBasePlugin:
15+
"""Tests for BasePlugin default implementations."""
16+
17+
@pytest.fixture
18+
def plugin(self):
19+
"""Create a BasePlugin instance for testing."""
20+
return BasePlugin()
21+
22+
@pytest.mark.asyncio
23+
async def test_configure_returns_empty(self, plugin, sample_plugin_config, mock_context):
24+
"""Configure should return Empty by default."""
25+
result = await plugin.Configure(sample_plugin_config, mock_context)
26+
assert isinstance(result, Empty)
27+
28+
@pytest.mark.asyncio
29+
async def test_stop_returns_empty(self, plugin, empty_request, mock_context):
30+
"""Stop should return Empty by default."""
31+
result = await plugin.Stop(empty_request, mock_context)
32+
assert isinstance(result, Empty)
33+
34+
@pytest.mark.asyncio
35+
async def test_get_metadata_returns_metadata(self, plugin, empty_request, mock_context):
36+
"""GetMetadata should return Metadata with default values."""
37+
result = await plugin.GetMetadata(empty_request, mock_context)
38+
assert isinstance(result, Metadata)
39+
assert result.name == "base-plugin"
40+
assert result.version == "0.0.0"
41+
assert result.description == "Base plugin implementation"
42+
43+
@pytest.mark.asyncio
44+
async def test_get_capabilities_returns_empty_capabilities(self, plugin, empty_request, mock_context):
45+
"""GetCapabilities should return empty Capabilities by default."""
46+
result = await plugin.GetCapabilities(empty_request, mock_context)
47+
assert isinstance(result, Capabilities)
48+
assert len(result.flows) == 0
49+
50+
@pytest.mark.asyncio
51+
async def test_check_health_returns_empty(self, plugin, empty_request, mock_context):
52+
"""CheckHealth should return Empty by default."""
53+
result = await plugin.CheckHealth(empty_request, mock_context)
54+
assert isinstance(result, Empty)
55+
56+
@pytest.mark.asyncio
57+
async def test_check_ready_returns_empty(self, plugin, empty_request, mock_context):
58+
"""CheckReady should return Empty by default."""
59+
result = await plugin.CheckReady(empty_request, mock_context)
60+
assert isinstance(result, Empty)
61+
62+
@pytest.mark.asyncio
63+
async def test_handle_request_passes_through(self, plugin, sample_http_request, mock_context):
64+
"""HandleRequest should return continue=True by default."""
65+
result = await plugin.HandleRequest(sample_http_request, mock_context)
66+
assert isinstance(result, HTTPResponse)
67+
assert getattr(result, "continue") is True
68+
69+
@pytest.mark.asyncio
70+
async def test_handle_response_passes_through(self, plugin, mock_context):
71+
"""HandleResponse should return continue=True by default."""
72+
http_response = HTTPResponse(status_code=200, **{"continue": True})
73+
result = await plugin.HandleResponse(http_response, mock_context)
74+
assert isinstance(result, HTTPResponse)
75+
assert getattr(result, "continue") is True
76+
77+
78+
class TestCustomPlugin:
79+
"""Tests for custom plugin that overrides methods."""
80+
81+
class CustomPlugin(BasePlugin):
82+
"""Custom plugin for testing overrides."""
83+
84+
async def GetMetadata(self, request, context):
85+
"""Override with custom metadata."""
86+
return Metadata(
87+
name="custom-plugin",
88+
version="1.2.3",
89+
description="Custom test plugin",
90+
)
91+
92+
async def HandleRequest(self, request, context):
93+
"""Override to add custom header."""
94+
response = HTTPResponse(**{"continue": True})
95+
response.headers["X-Custom"] = "test"
96+
return response
97+
98+
@pytest.fixture
99+
def custom_plugin(self):
100+
"""Create a custom plugin instance for testing."""
101+
return self.CustomPlugin()
102+
103+
@pytest.mark.asyncio
104+
async def test_custom_get_metadata(self, custom_plugin, empty_request, mock_context):
105+
"""Custom GetMetadata should return overridden values."""
106+
result = await custom_plugin.GetMetadata(empty_request, mock_context)
107+
assert result.name == "custom-plugin"
108+
assert result.version == "1.2.3"
109+
assert result.description == "Custom test plugin"
110+
111+
@pytest.mark.asyncio
112+
async def test_custom_handle_request(self, custom_plugin, sample_http_request, mock_context):
113+
"""Custom HandleRequest should add custom header."""
114+
result = await custom_plugin.HandleRequest(sample_http_request, mock_context)
115+
assert getattr(result, "continue") is True
116+
assert result.headers["X-Custom"] == "test"

0 commit comments

Comments
 (0)