- 将mock配置从YAML格式迁移到JSON格式 - 修复JSON字符串格式body匹配失败问题 - 添加MCP功能模块 - 更新mock-spec.md规范文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
319 lines
12 KiB
Rust
319 lines
12 KiB
Rust
use std::fs::{self, File};
|
||
use std::io::Write;
|
||
use tempfile::tempdir;
|
||
use serde_json::json;
|
||
use mock_server::models::{MockRule, Payload};
|
||
use mock_server::loader::MockLoader;
|
||
use mock_server::router::MockRouter;
|
||
use std::collections::HashMap;
|
||
|
||
/// 模块一:验证 Config 反序列化逻辑
|
||
#[test]
|
||
fn test_config_parsing_scenarios() {
|
||
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
||
let json_single = r#"{
|
||
"name": "auth_v1",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/v1/login",
|
||
"body": { "user": "admin" }
|
||
},
|
||
"response": { "status": 200, "body": "welcome" }
|
||
}"#;
|
||
let rule: MockRule = serde_json::from_str(json_single).expect("解析单接口失败");
|
||
assert!(rule.request.body.is_some());
|
||
assert_eq!(rule.request.body.as_ref().unwrap()["user"], "admin");
|
||
|
||
// 场景 B: 验证 Smart Body 的 file:// 协议字符串解析
|
||
let json_file = r#"{
|
||
"name": "export_api",
|
||
"request": { "method": "GET", "path": "/download" },
|
||
"response": { "status": 200, "body": "file://./storage/data.zip" }
|
||
}"#;
|
||
let rule_file: MockRule = serde_json::from_str(json_file).unwrap();
|
||
assert!(rule_file.response.body.starts_with("file://"));
|
||
}
|
||
|
||
/// 模块二:验证 Loader 递归扫描与索引构建
|
||
#[test]
|
||
fn test_loader_recursive_indexing() {
|
||
let temp_root = tempdir().expect("无法创建临时目录");
|
||
let root_path = temp_root.path();
|
||
|
||
let auth_path = root_path.join("v1/auth");
|
||
fs::create_dir_all(&auth_path).unwrap();
|
||
|
||
let mut f1 = File::create(auth_path.join("login.json")).unwrap();
|
||
writeln!(f1, r#"{{"name":"l1","request":{{"method":"POST","path":"/api/v1/login"}},"response":{{"status":200,"body":"ok"}}}}"#).unwrap();
|
||
|
||
let mut f2 = File::create(root_path.join("health.json")).unwrap();
|
||
writeln!(f2, r#"{{"name":"s1","request":{{"method":"GET","path":"/health"}},"response":{{"status":200,"body":"up"}}}}"#).unwrap();
|
||
|
||
let index = MockLoader::load_all_from_dir(root_path);
|
||
|
||
assert!(index.contains_key("api"));
|
||
assert!(index.contains_key("health"));
|
||
let total: usize = index.values().map(|v| v.len()).sum();
|
||
assert_eq!(total, 2);
|
||
}
|
||
|
||
/// 模块三:验证 Router 匹配逻辑(使用新的 Payload 类型)
|
||
#[test]
|
||
fn test_router_matching_logic() {
|
||
let mut index = HashMap::new();
|
||
|
||
// 1. 准备带有 Body 的规则
|
||
let rule_json = r#"{
|
||
"name": "auth_v1",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/v1/login",
|
||
"headers": { "Content-Type": "application/json" },
|
||
"body": { "code": 123 }
|
||
},
|
||
"response": { "status": 200, "body": "token_123" }
|
||
}"#;
|
||
let rule_auth: MockRule = serde_json::from_str(rule_json).unwrap();
|
||
|
||
index.insert("api".to_string(), vec![rule_auth.clone()]);
|
||
let router = MockRouter::new(index);
|
||
|
||
// 2. 测试场景 A:JSON 完全匹配
|
||
let mut headers = HashMap::new();
|
||
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||
|
||
let payload = Payload::Json(json!({ "code": 123 }));
|
||
|
||
let matched = router.match_rule(
|
||
"POST",
|
||
"/api/v1/login",
|
||
&HashMap::new(),
|
||
&headers,
|
||
&payload,
|
||
);
|
||
assert!(matched.is_some());
|
||
assert_eq!(matched.unwrap().name, "auth_v1");
|
||
|
||
// 3. 测试场景 B:Body 不匹配
|
||
let wrong_payload = Payload::Json(json!({ "code": 456 }));
|
||
let matched_fail = router.match_rule(
|
||
"POST",
|
||
"/api/v1/login",
|
||
&HashMap::new(),
|
||
&headers,
|
||
&wrong_payload,
|
||
);
|
||
assert!(matched_fail.is_none(), "Body 不一致时不应匹配成功");
|
||
|
||
// 4. 测试场景 C:末尾斜杠兼容性测试
|
||
let matched_slash = router.match_rule(
|
||
"POST",
|
||
"/api/v1/login/",
|
||
&HashMap::new(),
|
||
&headers,
|
||
&payload,
|
||
);
|
||
assert!(matched_slash.is_some());
|
||
}
|
||
|
||
/// 模块四:验证不同 Payload 类型的匹配
|
||
#[test]
|
||
fn test_payload_type_matching() {
|
||
let mut index = HashMap::new();
|
||
|
||
// XML 规则
|
||
let xml_rule: MockRule = serde_json::from_str(r#"{
|
||
"name": "xml_api",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/xml",
|
||
"body": "<root><name>test</name></root>"
|
||
},
|
||
"response": { "status": 200, "body": "ok" }
|
||
}"#).unwrap();
|
||
|
||
// Form 规则
|
||
let form_rule: MockRule = serde_json::from_str(r#"{
|
||
"name": "form_api",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/form",
|
||
"body": { "username": "admin", "password": "123456" }
|
||
},
|
||
"response": { "status": 200, "body": "login_ok" }
|
||
}"#).unwrap();
|
||
|
||
// Text 规则
|
||
let text_rule: MockRule = serde_json::from_str(r#"{
|
||
"name": "text_api",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/text",
|
||
"body": "plain text content"
|
||
},
|
||
"response": { "status": 200, "body": "text_ok" }
|
||
}"#).unwrap();
|
||
|
||
index.insert("api".to_string(), vec![xml_rule, form_rule, text_rule]);
|
||
let router = MockRouter::new(index);
|
||
|
||
// 测试 XML 匹配
|
||
let xml_payload = Payload::Xml("<root><name>test</name></root>".to_string());
|
||
let xml_matched = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_payload);
|
||
assert!(xml_matched.is_some(), "XML 应该匹配");
|
||
assert_eq!(xml_matched.unwrap().name, "xml_api");
|
||
|
||
// 测试 Form 匹配
|
||
let mut form_data = HashMap::new();
|
||
form_data.insert("username".to_string(), "admin".to_string());
|
||
form_data.insert("password".to_string(), "123456".to_string());
|
||
let form_payload = Payload::Form(form_data);
|
||
let form_matched = router.match_rule("POST", "/api/form", &HashMap::new(), &HashMap::new(), &form_payload);
|
||
assert!(form_matched.is_some(), "Form 应该匹配");
|
||
assert_eq!(form_matched.unwrap().name, "form_api");
|
||
|
||
// 测试 Text 匹配
|
||
let text_payload = Payload::Text("plain text content".to_string());
|
||
let text_matched = router.match_rule("POST", "/api/text", &HashMap::new(), &HashMap::new(), &text_payload);
|
||
assert!(text_matched.is_some(), "Text 应该匹配");
|
||
assert_eq!(text_matched.unwrap().name, "text_api");
|
||
|
||
// 测试 None 不匹配
|
||
let none_payload = Payload::None;
|
||
let none_matched = router.match_rule("POST", "/api/text", &HashMap::new(), &HashMap::new(), &none_payload);
|
||
assert!(none_matched.is_none(), "None 不应该匹配有 body 的规则");
|
||
}
|
||
|
||
/// 模块五:验证 Payload 方法
|
||
#[test]
|
||
fn test_payload_methods() {
|
||
// 测试 content_type_name
|
||
assert_eq!(Payload::Json(json!({})).content_type_name(), "application/json");
|
||
assert_eq!(Payload::Xml("".to_string()).content_type_name(), "application/xml");
|
||
assert_eq!(Payload::Form(HashMap::new()).content_type_name(), "application/x-www-form-urlencoded");
|
||
assert_eq!(Payload::Multipart(HashMap::new()).content_type_name(), "multipart/form-data");
|
||
assert_eq!(Payload::Text("".to_string()).content_type_name(), "text/plain");
|
||
assert_eq!(Payload::None.content_type_name(), "none");
|
||
|
||
// 测试 to_compare_string
|
||
let json_payload = Payload::Json(json!({"a": 1, "b": 2}));
|
||
assert!(json_payload.to_compare_string().contains("\"a\":1"));
|
||
|
||
let text_payload = Payload::Text("hello world".to_string());
|
||
assert_eq!(text_payload.to_compare_string(), "hello world");
|
||
|
||
let none_payload = Payload::None;
|
||
assert_eq!(none_payload.to_compare_string(), "");
|
||
|
||
// 测试 Form 排序后的字符串
|
||
let mut form_map = HashMap::new();
|
||
form_map.insert("b".to_string(), "2".to_string());
|
||
form_map.insert("a".to_string(), "1".to_string());
|
||
let form_payload = Payload::Form(form_map);
|
||
assert_eq!(form_payload.to_compare_string(), "a=1&b=2");
|
||
|
||
// 测试 Multipart 排序后的字符串
|
||
let mut multipart_map = HashMap::new();
|
||
multipart_map.insert("b".to_string(), "2".to_string());
|
||
multipart_map.insert("a".to_string(), "1".to_string());
|
||
let multipart_payload = Payload::Multipart(multipart_map);
|
||
assert_eq!(multipart_payload.to_compare_string(), "a=1&b=2");
|
||
}
|
||
|
||
/// 模块六:验证 Multipart 匹配
|
||
#[test]
|
||
fn test_multipart_matching() {
|
||
let mut index = HashMap::new();
|
||
|
||
// 数组形式:只匹配字段名
|
||
let array_rule: MockRule = serde_json::from_str(r#"{
|
||
"name": "upload_array",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/upload/array",
|
||
"body": ["file", "description"]
|
||
},
|
||
"response": { "status": 200, "body": "ok" }
|
||
}"#).unwrap();
|
||
|
||
// 对象形式:匹配键名
|
||
let object_rule: MockRule = serde_json::from_str(r#"{
|
||
"name": "upload_object",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/upload/object",
|
||
"body": { "username": "admin", "role": "user" }
|
||
},
|
||
"response": { "status": 200, "body": "ok" }
|
||
}"#).unwrap();
|
||
|
||
index.insert("api".to_string(), vec![array_rule, object_rule]);
|
||
let router = MockRouter::new(index);
|
||
|
||
// 测试数组形式:匹配字段名
|
||
let mut full_data = HashMap::new();
|
||
full_data.insert("file".to_string(), "file_content".to_string());
|
||
full_data.insert("description".to_string(), "test file".to_string());
|
||
let full_payload = Payload::Multipart(full_data);
|
||
let matched = router.match_rule("POST", "/api/upload/array", &HashMap::new(), &HashMap::new(), &full_payload);
|
||
assert!(matched.is_some(), "包含所有字段应该匹配");
|
||
|
||
// 测试数组形式:缺少字段
|
||
let mut partial_data = HashMap::new();
|
||
partial_data.insert("file".to_string(), "file_content".to_string());
|
||
let partial_payload = Payload::Multipart(partial_data);
|
||
let not_matched = router.match_rule("POST", "/api/upload/array", &HashMap::new(), &HashMap::new(), &partial_payload);
|
||
assert!(not_matched.is_none(), "缺少字段不应该匹配");
|
||
|
||
// 测试对象形式:键名匹配(值被忽略)
|
||
let mut correct_data = HashMap::new();
|
||
correct_data.insert("username".to_string(), "any_value".to_string());
|
||
correct_data.insert("role".to_string(), "any_role".to_string());
|
||
let correct_payload = Payload::Multipart(correct_data);
|
||
let object_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &correct_payload);
|
||
assert!(object_matched.is_some(), "键名匹配应该成功");
|
||
|
||
// 测试对象形式:键名不匹配
|
||
let mut wrong_data = HashMap::new();
|
||
wrong_data.insert("username".to_string(), "admin".to_string());
|
||
wrong_data.insert("other_field".to_string(), "value".to_string());
|
||
let wrong_payload = Payload::Multipart(wrong_data);
|
||
let wrong_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &wrong_payload);
|
||
assert!(wrong_matched.is_none(), "缺少键名不应该成功");
|
||
}
|
||
|
||
/// 模块七:验证 XML 格式化匹配
|
||
#[test]
|
||
fn test_xml_normalized_matching() {
|
||
let mut index = HashMap::new();
|
||
|
||
// JSON 中的 XML(紧凑格式)
|
||
let xml_rule: MockRule = serde_json::from_str(r#"{
|
||
"name": "xml_api",
|
||
"request": {
|
||
"method": "POST",
|
||
"path": "/api/xml",
|
||
"body": "<user><id>1001</id><name>张三</name></user>"
|
||
},
|
||
"response": { "status": 200, "body": "ok" }
|
||
}"#).unwrap();
|
||
|
||
index.insert("api".to_string(), vec![xml_rule]);
|
||
let router = MockRouter::new(index);
|
||
|
||
// 测试 1:紧凑格式的 XML
|
||
let compact_xml = Payload::Xml("<user><id>1001</id><name>张三</name></user>".to_string());
|
||
let matched1 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &compact_xml);
|
||
assert!(matched1.is_some(), "紧凑格式 XML 应该匹配");
|
||
|
||
// 测试 2:带 XML 声明的请求
|
||
let xml_with_decl = Payload::Xml("<?xml version=\"1.0\" encoding=\"UTF-8\"?><user><id>1001</id><name>张三</name></user>".to_string());
|
||
let matched2 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_with_decl);
|
||
assert!(matched2.is_some(), "带声明的 XML 应该匹配");
|
||
|
||
// 测试 3:内容不同,不应该匹配
|
||
let wrong_xml = Payload::Xml("<user><id>1002</id><name>李四</name></user>".to_string());
|
||
let matched3 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &wrong_xml);
|
||
assert!(matched3.is_none(), "内容不同的 XML 不应该匹配");
|
||
}
|