Skip to content

Commit 2bedd9d

Browse files
committed
test: move new resource tests to module level per repo convention
The eight resource-template tests added in this PR were placed inside the legacy TestServer and TestContextInjection classes to match surrounding code, but the repo convention is standalone module-level functions. Moved to the bottom of the file alongside the existing standalone tests.
1 parent 4a45f59 commit 2bedd9d

File tree

1 file changed

+124
-116
lines changed

1 file changed

+124
-116
lines changed

tests/server/mcpserver/test_server.py

Lines changed: 124 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -142,99 +142,6 @@ async def test_add_resource_decorator_incorrect_usage(self):
142142
def get_data(x: str) -> str: # pragma: no cover
143143
return f"Data: {x}"
144144

145-
async def test_resource_decorator_rfc6570_reserved_expansion(self):
146-
# Regression: old regex-based param extraction couldn't see `path`
147-
# in `{+path}` and failed with a confusing mismatch error.
148-
mcp = MCPServer()
149-
150-
@mcp.resource("file://docs/{+path}")
151-
def read_doc(path: str) -> str:
152-
raise NotImplementedError
153-
154-
templates = await mcp.list_resource_templates()
155-
assert [t.uri_template for t in templates] == ["file://docs/{+path}"]
156-
157-
async def test_resource_decorator_rejects_malformed_template(self):
158-
mcp = MCPServer()
159-
with pytest.raises(InvalidUriTemplate, match="Unclosed expression"):
160-
mcp.resource("file://{name")
161-
162-
async def test_resource_optional_query_params_use_function_defaults(self):
163-
"""Omitted {?...} query params should fall through to the
164-
handler's Python defaults. Partial and reordered params work."""
165-
mcp = MCPServer()
166-
167-
@mcp.resource("logs://{service}{?since,level}")
168-
def tail_logs(service: str, since: str = "1h", level: str = "info") -> str:
169-
return f"{service}|{since}|{level}"
170-
171-
async with Client(mcp) as client:
172-
# No query → all defaults
173-
r = await client.read_resource("logs://api")
174-
assert isinstance(r.contents[0], TextResourceContents)
175-
assert r.contents[0].text == "api|1h|info"
176-
177-
# Partial query → one default
178-
r = await client.read_resource("logs://api?since=15m")
179-
assert isinstance(r.contents[0], TextResourceContents)
180-
assert r.contents[0].text == "api|15m|info"
181-
182-
# Reordered, both present
183-
r = await client.read_resource("logs://api?level=error&since=5m")
184-
assert isinstance(r.contents[0], TextResourceContents)
185-
assert r.contents[0].text == "api|5m|error"
186-
187-
# Extra param ignored
188-
r = await client.read_resource("logs://api?since=2h&utm=x")
189-
assert isinstance(r.contents[0], TextResourceContents)
190-
assert r.contents[0].text == "api|2h|info"
191-
192-
async def test_resource_security_default_rejects_traversal(self):
193-
mcp = MCPServer()
194-
195-
@mcp.resource("data://items/{name}")
196-
def get_item(name: str) -> str:
197-
return f"item:{name}"
198-
199-
async with Client(mcp) as client:
200-
# Safe value passes through to the handler
201-
r = await client.read_resource("data://items/widget")
202-
assert isinstance(r.contents[0], TextResourceContents)
203-
assert r.contents[0].text == "item:widget"
204-
205-
# ".." as a path component is rejected by default policy
206-
with pytest.raises(MCPError, match="Unknown resource"):
207-
await client.read_resource("data://items/..")
208-
209-
async def test_resource_security_per_resource_override(self):
210-
mcp = MCPServer()
211-
212-
@mcp.resource(
213-
"git://diff/{+range}",
214-
security=ResourceSecurity(exempt_params={"range"}),
215-
)
216-
def git_diff(range: str) -> str:
217-
return f"diff:{range}"
218-
219-
async with Client(mcp) as client:
220-
# "../foo" would be rejected by default, but "range" is exempt
221-
result = await client.read_resource("git://diff/../foo")
222-
assert isinstance(result.contents[0], TextResourceContents)
223-
assert result.contents[0].text == "diff:../foo"
224-
225-
async def test_resource_security_server_wide_override(self):
226-
mcp = MCPServer(resource_security=ResourceSecurity(reject_path_traversal=False))
227-
228-
@mcp.resource("data://items/{name}")
229-
def get_item(name: str) -> str:
230-
return f"item:{name}"
231-
232-
async with Client(mcp) as client:
233-
# Server-wide policy disabled traversal check; ".." now allowed
234-
result = await client.read_resource("data://items/..")
235-
assert isinstance(result.contents[0], TextResourceContents)
236-
assert result.contents[0].text == "item:.."
237-
238145

239146
class TestDnsRebindingProtection:
240147
"""Tests for automatic DNS rebinding protection on localhost.
@@ -1227,29 +1134,6 @@ def resource_with_context(name: str, ctx: Context) -> str:
12271134
# Should have either request_id or indication that context was injected
12281135
assert "Resource test - context injected" == content.text
12291136

1230-
async def test_static_resource_with_context_param_errors(self):
1231-
"""A non-template URI with a Context-only handler should error
1232-
at decoration time with a clear message, not silently register
1233-
an unreachable resource."""
1234-
mcp = MCPServer()
1235-
1236-
with pytest.raises(ValueError, match="Context injection for static resources is not yet supported"):
1237-
1238-
@mcp.resource("weather://current")
1239-
def current_weather(ctx: Context) -> str:
1240-
raise NotImplementedError
1241-
1242-
async def test_static_resource_with_extra_params_errors(self):
1243-
"""A non-template URI with non-Context params should error at
1244-
decoration time."""
1245-
mcp = MCPServer()
1246-
1247-
with pytest.raises(ValueError, match="has no URI template variables"):
1248-
1249-
@mcp.resource("data://fixed")
1250-
def get_data(name: str) -> str:
1251-
raise NotImplementedError
1252-
12531137
async def test_resource_without_context(self):
12541138
"""Test that resources without context work normally."""
12551139
mcp = MCPServer()
@@ -1536,6 +1420,130 @@ def prompt_fn(name: str) -> str: ... # pragma: no branch
15361420
await client.get_prompt("prompt_fn")
15371421

15381422

1423+
async def test_resource_decorator_rfc6570_reserved_expansion():
1424+
# Regression: old regex-based param extraction couldn't see `path`
1425+
# in `{+path}` and failed with a confusing mismatch error.
1426+
mcp = MCPServer()
1427+
1428+
@mcp.resource("file://docs/{+path}")
1429+
def read_doc(path: str) -> str:
1430+
raise NotImplementedError
1431+
1432+
templates = await mcp.list_resource_templates()
1433+
assert [t.uri_template for t in templates] == ["file://docs/{+path}"]
1434+
1435+
1436+
async def test_resource_decorator_rejects_malformed_template():
1437+
mcp = MCPServer()
1438+
with pytest.raises(InvalidUriTemplate, match="Unclosed expression"):
1439+
mcp.resource("file://{name")
1440+
1441+
1442+
async def test_resource_optional_query_params_use_function_defaults():
1443+
"""Omitted {?...} query params should fall through to the
1444+
handler's Python defaults. Partial and reordered params work."""
1445+
mcp = MCPServer()
1446+
1447+
@mcp.resource("logs://{service}{?since,level}")
1448+
def tail_logs(service: str, since: str = "1h", level: str = "info") -> str:
1449+
return f"{service}|{since}|{level}"
1450+
1451+
async with Client(mcp) as client:
1452+
# No query → all defaults
1453+
r = await client.read_resource("logs://api")
1454+
assert isinstance(r.contents[0], TextResourceContents)
1455+
assert r.contents[0].text == "api|1h|info"
1456+
1457+
# Partial query → one default
1458+
r = await client.read_resource("logs://api?since=15m")
1459+
assert isinstance(r.contents[0], TextResourceContents)
1460+
assert r.contents[0].text == "api|15m|info"
1461+
1462+
# Reordered, both present
1463+
r = await client.read_resource("logs://api?level=error&since=5m")
1464+
assert isinstance(r.contents[0], TextResourceContents)
1465+
assert r.contents[0].text == "api|5m|error"
1466+
1467+
# Extra param ignored
1468+
r = await client.read_resource("logs://api?since=2h&utm=x")
1469+
assert isinstance(r.contents[0], TextResourceContents)
1470+
assert r.contents[0].text == "api|2h|info"
1471+
1472+
1473+
async def test_resource_security_default_rejects_traversal():
1474+
mcp = MCPServer()
1475+
1476+
@mcp.resource("data://items/{name}")
1477+
def get_item(name: str) -> str:
1478+
return f"item:{name}"
1479+
1480+
async with Client(mcp) as client:
1481+
# Safe value passes through to the handler
1482+
r = await client.read_resource("data://items/widget")
1483+
assert isinstance(r.contents[0], TextResourceContents)
1484+
assert r.contents[0].text == "item:widget"
1485+
1486+
# ".." as a path component is rejected by default policy
1487+
with pytest.raises(MCPError, match="Unknown resource"):
1488+
await client.read_resource("data://items/..")
1489+
1490+
1491+
async def test_resource_security_per_resource_override():
1492+
mcp = MCPServer()
1493+
1494+
@mcp.resource(
1495+
"git://diff/{+range}",
1496+
security=ResourceSecurity(exempt_params={"range"}),
1497+
)
1498+
def git_diff(range: str) -> str:
1499+
return f"diff:{range}"
1500+
1501+
async with Client(mcp) as client:
1502+
# "../foo" would be rejected by default, but "range" is exempt
1503+
result = await client.read_resource("git://diff/../foo")
1504+
assert isinstance(result.contents[0], TextResourceContents)
1505+
assert result.contents[0].text == "diff:../foo"
1506+
1507+
1508+
async def test_resource_security_server_wide_override():
1509+
mcp = MCPServer(resource_security=ResourceSecurity(reject_path_traversal=False))
1510+
1511+
@mcp.resource("data://items/{name}")
1512+
def get_item(name: str) -> str:
1513+
return f"item:{name}"
1514+
1515+
async with Client(mcp) as client:
1516+
# Server-wide policy disabled traversal check; ".." now allowed
1517+
result = await client.read_resource("data://items/..")
1518+
assert isinstance(result.contents[0], TextResourceContents)
1519+
assert result.contents[0].text == "item:.."
1520+
1521+
1522+
async def test_static_resource_with_context_param_errors():
1523+
"""A non-template URI with a Context-only handler should error
1524+
at decoration time with a clear message, not silently register
1525+
an unreachable resource."""
1526+
mcp = MCPServer()
1527+
1528+
with pytest.raises(ValueError, match="Context injection for static resources is not yet supported"):
1529+
1530+
@mcp.resource("weather://current")
1531+
def current_weather(ctx: Context) -> str:
1532+
raise NotImplementedError
1533+
1534+
1535+
async def test_static_resource_with_extra_params_errors():
1536+
"""A non-template URI with non-Context params should error at
1537+
decoration time."""
1538+
mcp = MCPServer()
1539+
1540+
with pytest.raises(ValueError, match="has no URI template variables"):
1541+
1542+
@mcp.resource("data://fixed")
1543+
def get_data(name: str) -> str:
1544+
raise NotImplementedError
1545+
1546+
15391547
async def test_completion_decorator() -> None:
15401548
"""Test that the completion decorator registers a working handler."""
15411549
mcp = MCPServer()

0 commit comments

Comments
 (0)