import pytest from fastmcp.resources import ResourceTemplate class TestQueryParameterExtraction: """Test basic query parameter extraction from URIs.""" async def test_single_query_param(self): """Test resource template with single query parameter.""" def get_data(id: str, format: str = "json") -> str: return f"Data {id} in {format}" template = ResourceTemplate.from_function( fn=get_data, uri_template="data://{id}{?format}", name="test", ) # Match without query param (uses default) params = template.matches("data://123") assert params == {"id": "123"} # Match with query param params = template.matches("data://123?format=xml") assert params == {"id": "123", "format": "xml"} async def test_multiple_query_params(self): """Test resource template with multiple query parameters.""" def get_items(category: str, page: int = 1, limit: int = 10) -> str: return f"Category {category}, page {page}, limit {limit}" template = ResourceTemplate.from_function( fn=get_items, uri_template="items://{category}{?page,limit}", name="test", ) # No query params params = template.matches("items://books") assert params == {"category": "books"} # One query param params = template.matches("items://books?page=2") assert params == {"category": "books", "page": "2"} # Both query params params = template.matches("items://books?page=2&limit=20") assert params == {"category": "books", "page": "2", "limit": "20"} class TestQueryParameterTypeCoercion: """Test type coercion for query parameters.""" async def test_int_coercion(self): """Test integer type coercion for query parameters.""" def get_page(resource: str, page: int = 1) -> dict: return {"resource": resource, "page": page, "type": type(page).__name__} template = ResourceTemplate.from_function( fn=get_page, uri_template="resource://{resource}{?page}", name="test", ) # Create resource with string query param resource = await template.create_resource( "resource://docs?page=5", {"resource": "docs", "page": "5"}, ) # read() returns raw dict result = await resource.read() assert isinstance(result, dict) assert result["page"] == 5 assert result["type"] == "int" async def test_bool_coercion(self): """Test boolean type coercion for query parameters.""" def get_config(name: str, enabled: bool = False) -> dict: return {"name": name, "enabled": enabled, "type": type(enabled).__name__} template = ResourceTemplate.from_function( fn=get_config, uri_template="config://{name}{?enabled}", name="test", ) # Test true value resource = await template.create_resource( "config://feature?enabled=true", {"name": "feature", "enabled": "true"}, ) # read() returns raw dict result = await resource.read() assert isinstance(result, dict) assert result["enabled"] is True # Test false value resource = await template.create_resource( "config://feature?enabled=false", {"name": "feature", "enabled": "false"}, ) result = await resource.read() assert isinstance(result, dict) assert result["enabled"] is False async def test_float_coercion(self): """Test float type coercion for query parameters.""" def get_metrics(service: str, threshold: float = 0.5) -> dict: return { "service": service, "threshold": threshold, "type": type(threshold).__name__, } template = ResourceTemplate.from_function( fn=get_metrics, uri_template="metrics://{service}{?threshold}", name="test", ) resource = await template.create_resource( "metrics://api?threshold=0.95", {"service": "api", "threshold": "0.95"}, ) # read() returns raw dict result = await resource.read() assert isinstance(result, dict) assert result["threshold"] == 0.95 assert result["type"] == "float" class TestQueryParameterValidation: """Test validation rules for query parameters.""" def test_query_params_must_be_optional(self): """Test that query parameters must have default values.""" def invalid_func(id: str, format: str) -> str: return f"Data {id} in {format}" with pytest.raises( ValueError, match="Query parameters .* must be optional function parameters with default values", ): ResourceTemplate.from_function( fn=invalid_func, uri_template="data://{id}{?format}", name="test", ) def test_required_params_in_path(self): """Test that required parameters must be in path.""" def valid_func(id: str, format: str = "json") -> str: return f"Data {id} in {format}" # This should work - required param in path, optional in query template = ResourceTemplate.from_function( fn=valid_func, uri_template="data://{id}{?format}", name="test", ) assert template.uri_template == "data://{id}{?format}" class TestQueryParameterWithDefaults: """Test that missing query parameters use default values.""" async def test_missing_query_param_uses_default(self): """Test that missing query parameters fall back to defaults.""" def get_data(id: str, format: str = "json", verbose: bool = False) -> dict: return {"id": id, "format": format, "verbose": verbose} template = ResourceTemplate.from_function( fn=get_data, uri_template="data://{id}{?format,verbose}", name="test", ) # No query params - should use defaults resource = await template.create_resource( "data://123", {"id": "123"}, ) # read() returns raw dict result = await resource.read() assert isinstance(result, dict) assert result["format"] == "json" assert result["verbose"] is False async def test_partial_query_params(self): """Test providing only some query parameters.""" def get_data( id: str, format: str = "json", limit: int = 10, offset: int = 0 ) -> dict: return {"id": id, "format": format, "limit": limit, "offset": offset} template = ResourceTemplate.from_function( fn=get_data, uri_template="data://{id}{?format,limit,offset}", name="test", ) # Provide only some query params resource = await template.create_resource( "data://123?limit=20", {"id": "123", "limit": "20"}, ) # read() returns raw dict result = await resource.read() assert isinstance(result, dict) assert result["format"] == "json" # default assert result["limit"] == 20 # provided assert result["offset"] == 0 # default class TestQueryParameterWithWildcards: """Test query parameters combined with wildcard path parameters.""" async def test_wildcard_with_query_params(self): """Test combining wildcard path params with query params.""" def get_file(path: str, encoding: str = "utf-8", lines: int = 100) -> dict: return {"path": path, "encoding": encoding, "lines": lines} template = ResourceTemplate.from_function( fn=get_file, uri_template="files://{path*}{?encoding,lines}", name="test", ) # Match path with query params params = template.matches("files://src/test/data.txt?encoding=ascii&lines=50") assert params == { "path": "src/test/data.txt", "encoding": "ascii", "lines": "50", } # Create resource resource = await template.create_resource( "files://src/test/data.txt?lines=50", {"path": "src/test/data.txt", "lines": "50"}, ) # read() returns raw dict result = await resource.read() assert isinstance(result, dict) assert result["path"] == "src/test/data.txt" assert result["encoding"] == "utf-8" # default assert result["lines"] == 50 # provided class TestBooleanQueryParameterValidation: """Test that invalid boolean query parameter values raise errors.""" async def _make_template(self): def get_config(name: str, enabled: bool = False) -> dict: return {"name": name, "enabled": enabled} return ResourceTemplate.from_function( fn=get_config, uri_template="config://{name}{?enabled}", name="test", ) async def test_invalid_boolean_value_raises_error(self): """Test that nonsense boolean values like 'banana' raise ValueError.""" template = await self._make_template() with pytest.raises(ValueError, match="Invalid boolean value for enabled"): resource = await template.create_resource( "config://feature?enabled=banana", {"name": "feature", "enabled": "banana"}, ) await resource.read() @pytest.mark.parametrize( "value", ["true", "True", "TRUE", "1", "yes", "Yes", "YES"] ) async def test_valid_true_values(self, value: str): """Test that all accepted truthy string values coerce to True.""" template = await self._make_template() resource = await template.create_resource( f"config://feature?enabled={value}", {"name": "feature", "enabled": value}, ) result = await resource.read() assert isinstance(result, dict) assert result["enabled"] is True @pytest.mark.parametrize( "value", ["false", "False", "FALSE", "0", "no", "No", "NO"] ) async def test_valid_false_values(self, value: str): """Test that all accepted falsy string values coerce to False.""" template = await self._make_template() resource = await template.create_resource( f"config://feature?enabled={value}", {"name": "feature", "enabled": value}, ) result = await resource.read() assert isinstance(result, dict) assert result["enabled"] is False @pytest.mark.parametrize("value", ["banana", "nope", "2", "truee", ""]) async def test_various_invalid_boolean_values(self, value: str): """Test that various invalid boolean strings raise ValueError.""" template = await self._make_template() with pytest.raises(ValueError, match="Invalid boolean value for enabled"): resource = await template.create_resource( f"config://feature?enabled={value}", {"name": "feature", "enabled": value}, ) await resource.read() class TestResourceTemplateFieldDefaults: """Test resource templates with Field() defaults.""" async def test_field_with_default(self): """Test that Field(default=...) correctly provides default values in resource templates.""" from pydantic import Field def get_data( id: str = Field(description="Resource ID"), format: str = Field(default="json", description="Output format"), ) -> str: return f"id={id}, format={format}" template = ResourceTemplate.from_function( fn=get_data, uri_template="data://{id}{?format}", name="test", ) # Test with only required parameter resource = await template.create_resource("data://123", {"id": "123"}) result = await resource.read() assert result == "id=123, format=json" # Test with override resource = await template.create_resource( "data://123?format=xml", {"id": "123", "format": "xml"} ) result = await resource.read() assert result == "id=123, format=xml" async def test_multiple_field_defaults(self): """Test multiple query parameters with Field() defaults.""" from typing import Any from pydantic import Field def fetch_data( resource_id: str = Field(description="Resource ID"), limit: int = Field(default=10, description="Result limit"), offset: int = Field(default=0, description="Result offset"), format: str = Field(default="json", description="Output format"), ) -> dict[str, Any]: return { "resource_id": resource_id, "limit": limit, "offset": offset, "format": format, } template = ResourceTemplate.from_function( fn=fetch_data, uri_template="api://{resource_id}{?limit,offset,format}", name="test", ) # Test with only required parameter - all defaults should apply resource1 = await template.create_resource( "api://user123", {"resource_id": "user123"} ) result1 = await resource1.read() assert isinstance(result1, dict) assert result1["resource_id"] == "user123" assert result1["limit"] == 10 assert result1["offset"] == 0 assert result1["format"] == "json" # Test with some overrides resource2 = await template.create_resource( "api://user123?limit=50&format=xml", {"resource_id": "user123", "limit": "50", "format": "xml"}, ) result2 = await resource2.read() assert isinstance(result2, dict) assert result2["resource_id"] == "user123" assert result2["limit"] == 50 # overridden assert result2["offset"] == 0 # default assert result2["format"] == "xml" # overridden