feat: mock配置迁移至JSON格式并修复body匹配
- 将mock配置从YAML格式迁移到JSON格式 - 修复JSON字符串格式body匹配失败问题 - 添加MCP功能模块 - 更新mock-spec.md规范文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use tempfile::tempdir;
|
||||
use serde_json::json;
|
||||
use mock_server::models::{MockRule, MockSource, Payload};
|
||||
use mock_server::models::{MockRule, Payload};
|
||||
use mock_server::loader::MockLoader;
|
||||
use mock_server::router::MockRouter;
|
||||
use std::collections::HashMap;
|
||||
@@ -11,42 +11,27 @@ use std::collections::HashMap;
|
||||
#[test]
|
||||
fn test_config_parsing_scenarios() {
|
||||
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
||||
let yaml_single = r#"
|
||||
name: "auth_v1"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/v1/login"
|
||||
body: { "user": "admin" }
|
||||
response: { status: 200, body: "welcome" }
|
||||
"#;
|
||||
let source_s: MockSource = serde_yaml::from_str(yaml_single).expect("解析单接口失败");
|
||||
let rules = source_s.flatten();
|
||||
assert_eq!(rules.len(), 1);
|
||||
// 验证 body 是否被成功解析为 Value::Object
|
||||
assert!(rules[0].request.body.is_some());
|
||||
assert_eq!(rules[0].request.body.as_ref().unwrap()["user"], "admin");
|
||||
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: 验证多接口配置
|
||||
let yaml_multi = r#"
|
||||
- name: "api_1"
|
||||
request: { method: "GET", path: "/health" }
|
||||
response: { status: 200, body: "ok" }
|
||||
- name: "api_2"
|
||||
request: { method: "GET", path: "/version" }
|
||||
response: { status: 200, body: "1.0" }
|
||||
"#;
|
||||
let source_m: MockSource = serde_yaml::from_str(yaml_multi).expect("解析多接口失败");
|
||||
assert_eq!(source_m.flatten().len(), 2);
|
||||
|
||||
// 场景 C: 验证 Smart Body 的 file:// 协议字符串解析
|
||||
let yaml_file = r#"
|
||||
name: "export_api"
|
||||
request: { method: "GET", path: "/download" }
|
||||
response: { status: 200, body: "file://./storage/data.zip" }
|
||||
"#;
|
||||
let source_f: MockSource = serde_yaml::from_str(yaml_file).unwrap();
|
||||
let rule = &source_f.flatten()[0];
|
||||
assert!(rule.response.body.starts_with("file://"));
|
||||
// 场景 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 递归扫描与索引构建
|
||||
@@ -58,11 +43,11 @@ fn test_loader_recursive_indexing() {
|
||||
let auth_path = root_path.join("v1/auth");
|
||||
fs::create_dir_all(&auth_path).unwrap();
|
||||
|
||||
let mut f1 = File::create(auth_path.join("single_login.yaml")).unwrap();
|
||||
writeln!(f1, "name: 'l1'\nrequest: {{ method: 'POST', path: '/api/v1/login' }}\nresponse: {{ status: 200, body: 'ok' }}").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("sys.yaml")).unwrap();
|
||||
writeln!(f2, "- name: 's1'\n request: {{ method: 'GET', path: '/health' }}\n response: {{ status: 200, body: 'up' }}").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);
|
||||
|
||||
@@ -78,21 +63,19 @@ fn test_router_matching_logic() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
// 1. 准备带有 Body 的规则
|
||||
let rule_auth = serde_yaml::from_str::<MockSource>(
|
||||
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" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
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[0].clone()]);
|
||||
index.insert("api".to_string(), vec![rule_auth.clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 2. 测试场景 A:JSON 完全匹配
|
||||
@@ -139,48 +122,39 @@ fn test_payload_type_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
// XML 规则
|
||||
let xml_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "xml_api"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/xml"
|
||||
body: "<root><name>test</name></root>"
|
||||
response: { status: 200, body: "ok" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
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 = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "form_api"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/form"
|
||||
body: { "username": "admin", "password": "123456" }
|
||||
response: { status: 200, body: "login_ok" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
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 = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "text_api"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/text"
|
||||
body: "plain text content"
|
||||
response: { status: 200, body: "text_ok" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
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[0].clone(), form_rule[0].clone(), text_rule[0].clone()]);
|
||||
index.insert("api".to_string(), vec![xml_rule, form_rule, text_rule]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试 XML 匹配
|
||||
@@ -252,34 +226,28 @@ fn test_multipart_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
// 数组形式:只匹配字段名
|
||||
let array_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "upload_array"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/upload/array"
|
||||
body: ["file", "description"]
|
||||
response: { status: 200, body: "ok" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
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 = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "upload_object"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/upload/object"
|
||||
body: { "username": "admin", "role": "user" }
|
||||
response: { status: 200, body: "ok" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
// 对象形式:匹配键名
|
||||
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[0].clone(), object_rule[0].clone()]);
|
||||
index.insert("api".to_string(), vec![array_rule, object_rule]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试数组形式:匹配字段名
|
||||
@@ -299,7 +267,7 @@ response: { status: 200, body: "ok" }
|
||||
|
||||
// 测试对象形式:键名匹配(值被忽略)
|
||||
let mut correct_data = HashMap::new();
|
||||
correct_data.insert("username".to_string(), "any_value".to_string()); // 值不重要
|
||||
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);
|
||||
@@ -308,118 +276,43 @@ response: { status: 200, body: "ok" }
|
||||
// 测试对象形式:键名不匹配
|
||||
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()); // 缺少 role 字段
|
||||
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(), "缺少键名不应该成功");
|
||||
}
|
||||
|
||||
/// 模块七:验证 YAML 块语法(折叠 `>` 和字面量 `|`)
|
||||
#[test]
|
||||
fn test_yaml_block_syntax() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
// 使用折叠语法 `>` 的规则
|
||||
let folded_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "folded_api"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/folded"
|
||||
body: >
|
||||
{"username":"admin","password":"123456"}
|
||||
response:
|
||||
status: 200
|
||||
body: "ok"
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
// 使用字面量语法 `|` 的规则
|
||||
let literal_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "literal_api"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/literal"
|
||||
body: |
|
||||
{"username":"test","password":"abcdef"}
|
||||
response:
|
||||
status: 200
|
||||
body: "ok"
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![folded_rule[0].clone(), literal_rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试折叠语法:YAML 中 `>` 会把内容合并成一行,但仍是字符串
|
||||
// 程序应该能将字符串解析为 JSON 后匹配
|
||||
let folded_payload = Payload::Json(json!({"username":"admin","password":"123456"}));
|
||||
let folded_matched = router.match_rule("POST", "/api/folded", &HashMap::new(), &HashMap::new(), &folded_payload);
|
||||
assert!(folded_matched.is_some(), "折叠语法应该匹配 JSON 请求");
|
||||
assert_eq!(folded_matched.unwrap().name, "folded_api");
|
||||
|
||||
// 测试字面量语法:YAML 中 `|` 保留换行
|
||||
let literal_payload = Payload::Json(json!({"username":"test","password":"abcdef"}));
|
||||
let literal_matched = router.match_rule("POST", "/api/literal", &HashMap::new(), &HashMap::new(), &literal_payload);
|
||||
assert!(literal_matched.is_some(), "字面量语法应该匹配 JSON 请求");
|
||||
assert_eq!(literal_matched.unwrap().name, "literal_api");
|
||||
|
||||
// 测试不匹配的情况
|
||||
let wrong_payload = Payload::Json(json!({"username":"wrong","password":"wrong"}));
|
||||
let wrong_matched = router.match_rule("POST", "/api/folded", &HashMap::new(), &HashMap::new(), &wrong_payload);
|
||||
assert!(wrong_matched.is_none(), "错误的 body 不应该匹配");
|
||||
}
|
||||
|
||||
/// 模块八:验证 XML 格式化匹配
|
||||
/// 模块七:验证 XML 格式化匹配
|
||||
#[test]
|
||||
fn test_xml_normalized_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
// YAML 中的 XML(带格式化)
|
||||
let xml_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
name: "xml_api"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/xml"
|
||||
body: |
|
||||
<user>
|
||||
<id>1001</id>
|
||||
<name>张三</name>
|
||||
</user>
|
||||
response:
|
||||
status: 200
|
||||
body: "ok"
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
// 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[0].clone()]);
|
||||
index.insert("api".to_string(), vec![xml_rule]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试 1:完全相同的格式化 XML
|
||||
let formatted_xml = Payload::Xml("<user>\n <id>1001</id>\n <name>张三</name>\n</user>".to_string());
|
||||
let matched1 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &formatted_xml);
|
||||
assert!(matched1.is_some(), "格式化 XML 应该匹配");
|
||||
|
||||
// 测试 2:紧凑格式的 XML
|
||||
// 测试 1:紧凑格式的 XML
|
||||
let compact_xml = Payload::Xml("<user><id>1001</id><name>张三</name></user>".to_string());
|
||||
let matched2 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &compact_xml);
|
||||
assert!(matched2.is_some(), "紧凑格式 XML 应该匹配");
|
||||
let matched1 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &compact_xml);
|
||||
assert!(matched1.is_some(), "紧凑格式 XML 应该匹配");
|
||||
|
||||
// 测试 3:带 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 matched3 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_with_decl);
|
||||
assert!(matched3.is_some(), "带声明的 XML 应该匹配");
|
||||
let matched2 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_with_decl);
|
||||
assert!(matched2.is_some(), "带声明的 XML 应该匹配");
|
||||
|
||||
// 测试 4:内容不同,不应该匹配
|
||||
// 测试 3:内容不同,不应该匹配
|
||||
let wrong_xml = Payload::Xml("<user><id>1002</id><name>李四</name></user>".to_string());
|
||||
let matched4 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &wrong_xml);
|
||||
assert!(matched4.is_none(), "内容不同的 XML 不应该匹配");
|
||||
let matched3 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &wrong_xml);
|
||||
assert!(matched3.is_none(), "内容不同的 XML 不应该匹配");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user