mcpkit_testing/
fixtures.rs

1//! Test fixtures for MCP testing.
2//!
3//! This module provides pre-built fixtures for common testing scenarios.
4
5use crate::mock::{MockPrompt, MockResource, MockTool};
6use mcpkit_core::types::{Prompt, PromptArgument, Resource, Tool, ToolAnnotations, ToolOutput};
7
8/// Create a set of sample tools for testing.
9///
10/// Returns tools:
11/// - `echo`: Echoes back the input
12/// - `add`: Adds two numbers
13/// - `multiply`: Multiplies two numbers
14/// - `fail`: Always returns an error
15pub fn sample_tools() -> Vec<MockTool> {
16    vec![
17        MockTool::new("echo")
18            .description("Echo back the input")
19            .input_schema(serde_json::json!({
20                "type": "object",
21                "properties": {
22                    "message": { "type": "string" }
23                },
24                "required": ["message"]
25            }))
26            .handler(|args| {
27                let message = args
28                    .get("message")
29                    .and_then(|v| v.as_str())
30                    .unwrap_or("(no message)");
31                Ok(ToolOutput::text(message))
32            }),
33        MockTool::new("add")
34            .description("Add two numbers")
35            .input_schema(serde_json::json!({
36                "type": "object",
37                "properties": {
38                    "a": { "type": "number" },
39                    "b": { "type": "number" }
40                },
41                "required": ["a", "b"]
42            }))
43            .annotations(ToolAnnotations::read_only())
44            .handler(|args| {
45                let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
46                let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
47                Ok(ToolOutput::text(format!("{}", a + b)))
48            }),
49        MockTool::new("multiply")
50            .description("Multiply two numbers")
51            .input_schema(serde_json::json!({
52                "type": "object",
53                "properties": {
54                    "a": { "type": "number" },
55                    "b": { "type": "number" }
56                },
57                "required": ["a", "b"]
58            }))
59            .annotations(ToolAnnotations::read_only())
60            .handler(|args| {
61                let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
62                let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
63                Ok(ToolOutput::text(format!("{}", a * b)))
64            }),
65        MockTool::new("fail")
66            .description("Always fails")
67            .returns_error("This tool always fails"),
68    ]
69}
70
71/// Create a set of sample resources for testing.
72///
73/// Returns resources:
74/// - `test://readme`: A sample README file
75/// - `test://config`: A sample configuration file
76/// - `test://data`: Sample JSON data
77pub fn sample_resources() -> Vec<MockResource> {
78    vec![
79        MockResource::new("test://readme", "README")
80            .description("A sample README file")
81            .mime_type("text/markdown")
82            .content("# Test Server\n\nThis is a test server."),
83        MockResource::new("test://config", "Configuration")
84            .description("Server configuration")
85            .mime_type("application/json")
86            .content(r#"{"debug": true, "port": 8080}"#),
87        MockResource::new("test://data", "Sample Data")
88            .description("Sample data for testing")
89            .mime_type("application/json")
90            .content(r#"{"items": [1, 2, 3], "count": 3}"#),
91    ]
92}
93
94/// Create a set of sample prompts for testing.
95///
96/// Returns prompts:
97/// - `summarize`: A prompt for summarizing text
98/// - `translate`: A prompt for translation
99pub fn sample_prompts() -> Vec<MockPrompt> {
100    vec![
101        MockPrompt::new("summarize")
102            .description("Summarize the given text")
103            .template("Please summarize the following text:\n\n{{text}}"),
104        MockPrompt::new("translate")
105            .description("Translate text to another language")
106            .template("Translate the following text to {{language}}:\n\n{{text}}"),
107    ]
108}
109
110/// Create a standard tool definition for testing.
111pub fn tool_definition(name: &str, description: &str) -> Tool {
112    Tool::new(name).description(description)
113}
114
115/// Create a standard resource definition for testing.
116pub fn resource_definition(uri: &str, name: &str) -> Resource {
117    Resource::new(uri, name)
118}
119
120/// Create a standard prompt definition for testing.
121pub fn prompt_definition(name: &str) -> Prompt {
122    Prompt::new(name)
123}
124
125/// Create a prompt with arguments.
126pub fn prompt_with_args(
127    name: &str,
128    description: &str,
129    args: Vec<(&str, &str, bool)>,
130) -> Prompt {
131    let mut prompt = Prompt::new(name).description(description);
132    for (arg_name, arg_desc, required) in args {
133        let arg = if required {
134            PromptArgument::required(arg_name, arg_desc)
135        } else {
136            PromptArgument::optional(arg_name, arg_desc)
137        };
138        prompt = prompt.argument(arg);
139    }
140    prompt
141}
142
143/// Create the calculator tool set.
144///
145/// This is a common fixture for testing arithmetic operations.
146pub fn calculator_tools() -> Vec<MockTool> {
147    vec![
148        MockTool::new("add")
149            .description("Add two numbers together")
150            .input_schema(serde_json::json!({
151                "type": "object",
152                "properties": {
153                    "a": { "type": "number", "description": "First number" },
154                    "b": { "type": "number", "description": "Second number" }
155                },
156                "required": ["a", "b"]
157            }))
158            .handler(|args| {
159                let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
160                let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
161                Ok(ToolOutput::text(format!("{}", a + b)))
162            }),
163        MockTool::new("subtract")
164            .description("Subtract two numbers")
165            .handler(|args| {
166                let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
167                let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
168                Ok(ToolOutput::text(format!("{}", a - b)))
169            }),
170        MockTool::new("multiply")
171            .description("Multiply two numbers")
172            .handler(|args| {
173                let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
174                let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
175                Ok(ToolOutput::text(format!("{}", a * b)))
176            }),
177        MockTool::new("divide")
178            .description("Divide two numbers")
179            .handler(|args| {
180                let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
181                let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
182                if b == 0.0 {
183                    Ok(ToolOutput::error("Cannot divide by zero"))
184                } else {
185                    Ok(ToolOutput::text(format!("{}", a / b)))
186                }
187            }),
188    ]
189}
190
191/// Create a file system resource set.
192///
193/// This is a common fixture for testing file operations.
194pub fn filesystem_resources() -> Vec<MockResource> {
195    vec![
196        MockResource::new("file:///project/src/main.rs", "main.rs")
197            .mime_type("text/x-rust")
198            .content("fn main() {\n    println!(\"Hello, world!\");\n}"),
199        MockResource::new("file:///project/Cargo.toml", "Cargo.toml")
200            .mime_type("text/x-toml")
201            .content("[package]\nname = \"test\"\nversion = \"0.1.0\""),
202        MockResource::new("file:///project/README.md", "README.md")
203            .mime_type("text/markdown")
204            .content("# Test Project\n\nA test project."),
205    ]
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_sample_tools() {
214        let tools = sample_tools();
215        assert_eq!(tools.len(), 4);
216        assert!(tools.iter().any(|t| t.name == "echo"));
217        assert!(tools.iter().any(|t| t.name == "add"));
218    }
219
220    #[test]
221    fn test_sample_resources() {
222        let resources = sample_resources();
223        assert_eq!(resources.len(), 3);
224        assert!(resources.iter().any(|r| r.uri == "test://readme"));
225    }
226
227    #[test]
228    fn test_calculator_tools() {
229        let tools = calculator_tools();
230        assert_eq!(tools.len(), 4);
231
232        // Test the add tool
233        let add = tools.iter().find(|t| t.name == "add").unwrap();
234        let result = add.call(serde_json::json!({"a": 5, "b": 3})).unwrap();
235        match result {
236            ToolOutput::Success(r) => {
237                if let mcpkit_core::types::Content::Text(tc) = &r.content[0] {
238                    assert_eq!(tc.text, "8");
239                }
240            }
241            _ => panic!("Expected success"),
242        }
243
244        // Test the divide tool with zero
245        let divide = tools.iter().find(|t| t.name == "divide").unwrap();
246        let result = divide.call(serde_json::json!({"a": 5, "b": 0})).unwrap();
247        match result {
248            ToolOutput::RecoverableError { message, .. } => {
249                assert!(message.contains("zero"));
250            }
251            _ => panic!("Expected error"),
252        }
253    }
254
255    #[test]
256    fn test_prompt_with_args() {
257        let prompt = prompt_with_args(
258            "test",
259            "A test prompt",
260            vec![
261                ("required_arg", "A required argument", true),
262                ("optional_arg", "An optional argument", false),
263            ],
264        );
265
266        assert_eq!(prompt.name, "test");
267        let args = prompt.arguments.unwrap();
268        assert_eq!(args.len(), 2);
269        assert!(args[0].required.unwrap());
270        assert!(!args[1].required.unwrap());
271    }
272}