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