diff --git a/mocks/v1/auth/login.yaml b/mocks/v1/auth/login.yaml deleted file mode 100644 index a24501a..0000000 --- a/mocks/v1/auth/login.yaml +++ /dev/null @@ -1,28 +0,0 @@ -id: "auth_login_001" -request: - method: "POST" - path: "/api/v1/auth/login" - # 必须包含此 Header 才会匹配 - headers: - Content-Type: "application/json" - Authorization: "111" - host: "127.0.0.1:8080" - body: > - { - "username":"user", - "password":"123" - } -response: - status: 200 - headers: - Content-Type: "application/json" - X-Mock-Engine: "Rust-Gemini-v1.2" - # 直接内联 JSON 字符串 - body: > - { - "code": 0, - "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6" }, - "msg": "success" - } -settings: - delay_ms: 2000 # 模拟真实网络延迟 \ No newline at end of file diff --git a/mocks/v1/auth/login_out.yaml b/mocks/v1/auth/login_out.yaml deleted file mode 100644 index 05e4930..0000000 --- a/mocks/v1/auth/login_out.yaml +++ /dev/null @@ -1,50 +0,0 @@ -- id: "auth_login_out_001" - request: - method: "POST" - path: "/api/v1/auth/login_out" - # 必须包含此 Header 才会匹配 - headers: - Content-Type: "application/json" - Authorization: "111" - host: "127.0.0.1:8080" - body: - type: true - response: - status: 200 - headers: - Content-Type: "application/json" - X-Mock-Engine: "Rust-Gemini-v1.2" - # 直接内联 JSON 字符串 - body: > - { - "code": 0, - "data": "退出成功", - "msg": "success" - } - settings: - delay_ms: 200 # 模拟真实网络延迟 -- id: "auth_login_out_002" - request: - method: "POST" - path: "/api/v1/auth/login_out" - # 必须包含此 Header 才会匹配 - headers: - Content-Type: "application/json" - Authorization: "111" - host: "127.0.0.1:8080" - body: - type: false - response: - status: 200 - headers: - Content-Type: "application/json" - X-Mock-Engine: "Rust-Gemini-v1.2" - # 直接内联 JSON 字符串 - body: > - { - "code": 1, - "data": "退出失败", - "msg": "success" - } - settings: - delay_ms: 200 # 模拟真实网络延迟 \ No newline at end of file diff --git a/mocks/v1/auth/multiple_login.yaml b/mocks/v1/auth/multiple_login.yaml new file mode 100644 index 0000000..00c6ce5 --- /dev/null +++ b/mocks/v1/auth/multiple_login.yaml @@ -0,0 +1,118 @@ +# 用户登录 - JSON 格式 +- name: "user_login_002" + request: + path: "/v1/auth/login" + method: "POST" + headers: + Content-Type: "application/json" + Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6" + host: "127.0.0.1:8080" + body: > + { + "username": "user002", + "password": "password123" + } + response: + status: 200 + headers: + Content-Type: "application/json" + body: | + { + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6", + "userId": 10002, + "username": "user002", + "role": "administrator" + } + } + settings: + delay_ms: 2000 # 模拟真实网络延迟 + +- name: "user_login_003" + request: + path: "/v1/auth/login" + method: "POST" + headers: + Content-Type: "application/json" + Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6" + host: "127.0.0.1:8080" + body: | + { + "username": "user003", + "password": "password123" + } + response: + status: 200 + headers: + Content-Type: "application/json" + body: > + { + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6", + "userId": 10003, + "username": "user003", + "role": "administrator" + } + } + settings: + delay_ms: 2000 # 模拟真实网络延迟 + +- name: "user_login_004" + request: + path: "/v1/auth/login" + method: "POST" + headers: + Content-Type: "application/json" + Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6" + host: "127.0.0.1:8080" + body: | + { + "username": "user004", + "password": "password123" + } + response: + status: 200 + headers: + Content-Type: "application/json" + body: | + { + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6", + "userId": 10004, + "username": "user004", + "role": "administrator" + } + } + +- name: "user_login_005" + request: + path: "/v1/auth/login" + method: "POST" + headers: + Content-Type: "application/json" + Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6" + host: "127.0.0.1:8080" + body: + username: "user005" + password: "password123" + response: + status: 200 + headers: + Content-Type: "application/json" + body: | + { + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6", + "userId": 10005, + "username": "user005", + "role": "administrator" + } + } \ No newline at end of file diff --git a/mocks/v1/products/export.yaml b/mocks/v1/products/export.yaml deleted file mode 100644 index faed5af..0000000 --- a/mocks/v1/products/export.yaml +++ /dev/null @@ -1,12 +0,0 @@ -id: "prod_export_pdf" -request: - method: "GET" - path: "/api/v1/products/report" - body: '{"username":"user","password":"123"}' -response: - status: 200 - headers: - Content-Type: "application/pdf" - Content-Disposition: "attachment; filename=report.pdf" - # 智能协议:引擎会自动识别前缀并异步读取磁盘文件 - body: "file://./storage/reports/annual_2024.pdf" \ No newline at end of file diff --git a/mocks/v1/system/health.yaml b/mocks/v1/system/health.yaml deleted file mode 100644 index a1a2d4b..0000000 --- a/mocks/v1/system/health.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# 使用 YAML 数组语法定义多个规则 -- id: "sys_ping" - request: - method: "GET" - path: "/api/v1/ping" - response: - status: 200 - body: "pong" - -- id: "sys_version" - request: - method: "GET" - path: "/api/v1/version" - response: - status: 200 - body: '{"version": "1.2.0-smart"}' \ No newline at end of file diff --git a/mocks/v1/upload.yaml b/mocks/v1/upload.yaml deleted file mode 100644 index 8a69423..0000000 --- a/mocks/v1/upload.yaml +++ /dev/null @@ -1,23 +0,0 @@ -id: "upload_file" -request: - method: "POST" - path: "/api/v1/upload" - headers: - Content-Type: "multipart/form-data" -response: - status: 200 - headers: - Content-Type: "application/json" - body: > - { - "code": 0, - "data": { - "filename": "example.txt", - "path": "storage/2024-01-15/example.txt", - "size": 1024, - "url": "/storage/2024-01-15/example.txt" - }, - "msg": "upload success" - } -settings: - delay_ms: 100 diff --git a/mocks/v1/user/search.yaml b/mocks/v1/user/search.yaml deleted file mode 100644 index d4848fb..0000000 --- a/mocks/v1/user/search.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: "user_search_admin" -request: - method: "GET" - path: "/api/v1/users" - # 请求中必须包含 role=admin 且 status=active - query_params: - role: "admin" - status: "active" -response: - status: 200 - body: '{"users": [{"id": 1, "name": "SuperAdmin"}]}' \ No newline at end of file diff --git a/src/router.rs b/src/router.rs index c68dbcd..132a94c 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use crate::config::MockRule; +use crate::models::{MockRule, Payload}; pub struct MockRouter { // 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则 @@ -18,7 +18,7 @@ impl MockRouter { path: &str, queries: &HashMap, headers: &HashMap, - incoming_body: &Option, // 修改 1: 增加参数 + payload: &Payload, ) -> Option<&MockRule> { // 1. 提取请求路径的首段作为索引 Key let key = self.extract_first_segment(path); @@ -27,7 +27,7 @@ impl MockRouter { if let Some(rules) = self.index.get(&key) { // 3. 在候选集中进行线性深度匹配 for rule in rules { - if self.is_match(rule, method, path, queries, headers,incoming_body) { + if self.is_match(rule, method, path, queries, headers, payload) { return Some(rule); } } @@ -44,18 +44,19 @@ impl MockRouter { path: &str, queries: &HashMap, headers: &HashMap, - incoming_body: &Option, // 修改 3: 增加参数 + payload: &Payload, ) -> bool { // A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠) if rule.request.method.to_uppercase() != method.to_uppercase() { - println!("DEBUG: [ID:{}] Method Mismatch: YAML={}, Req={}", rule.id, rule.request.method, method); + println!("DEBUG: [NAME:{}] Method Mismatch: YAML={}, Req={}", rule.name, rule.request.method, method); return false; } if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') { - println!("DEBUG: [ID:{}] Path Mismatch: YAML='{}', Req='{}'", rule.id, rule.request.path, path); + println!("DEBUG: [NAME:{}] Path Mismatch: YAML='{}', Req='{}'", rule.name, rule.request.path, path); return false; } - println!("DEBUG: [ID:{}] Method and Path matched! Checking headers...", rule.id); + println!("DEBUG: [NAME:{}] Method and Path matched! Checking headers...", rule.name); + // B. Query 参数校验 (子集匹配原则) if let Some(ref required_queries) = rule.request.query_params { for (key, val) in required_queries { @@ -64,47 +65,72 @@ impl MockRouter { } } } - - // C. Header 校验 (优化版) + + // C. Header 校验 (大小写不敏感,Content-Type 使用前缀匹配) if let Some(ref required_headers) = rule.request.headers { for (key, val) in required_headers { - println!("{}:{}",key.clone(), val.clone()); - // 方案:将 Key 统一转为小写比较,并检查请求头是否“包含”期望的值 + let key_lower = key.to_lowercase(); let matched = headers.iter().any(|(k, v)| { - // k.to_lowercase() == key.to_lowercase() && v.contains(val) - k.to_lowercase() == key.to_lowercase() && v==val + if k.to_lowercase() != key_lower { + return false; + } + // Content-Type 使用前缀匹配(因为可能包含 boundary 等参数) + if key_lower == "content-type" { + v.to_lowercase().starts_with(&val.to_lowercase()) + } else { + v == val + } }); - if !matched { return false; } } } - // D. 智能 Body 全量比对逻辑 - if let Some(ref required_val) = rule.request.body { - match incoming_body { - Some(actual_val) => { - // 实现你的想法:尝试将 YAML 中的 String 转换为 Object 再对比 - let final_required = if let Some(s) = required_val.as_str() { - // 如果能解析成 JSON,就用解析后的对象,否则用原始字符串 Value - serde_json::from_str::(s).unwrap_or_else(|_| required_val.clone()) - } else { - required_val.clone() - }; - // 执行全量相等比对 - if final_required != *actual_val { - println!("DEBUG: [ID:{}] Body Mismatch", rule.id); - return false; - } - } - None => return false, // YAML 要求有 Body 但请求为空 - } + // D. Body 匹配(根据 Payload 类型智能比较) + if let Some(ref yaml_body) = rule.request.body { + return self.match_body(yaml_body, payload); } true } + /// Body 匹配逻辑 + fn match_body(&self, yaml_body: &serde_json::Value, payload: &Payload) -> bool { + // 将 YAML body 规范化:如果是字符串,尝试解析为 JSON + let normalized_body = normalize_yaml_body(yaml_body); + + match payload { + Payload::Json(actual) => { + // JSON 对象比较 + &normalized_body == actual + } + Payload::Xml(actual) => { + // XML 字符串比较(规范化后比较) + normalized_body.as_str() + .map(|expected| normalize_xml(expected) == normalize_xml(actual)) + .unwrap_or(false) + } + Payload::Form(actual) => { + // Form 键值对比较(子集匹配) + compare_form_with_yaml(&normalized_body, actual) + } + Payload::Multipart(actual_data) => { + // Multipart 匹配:支持键值对或字段名列表 + compare_multipart_with_yaml(&normalized_body, actual_data) + } + Payload::Text(actual) => { + // 字符串比较(去掉首尾空白) + normalized_body.as_str() + .map(|expected| expected.trim() == actual.trim()) + .unwrap_or_else(|| normalized_body.to_string().trim() == actual.trim()) + } + Payload::None => { + false // YAML 配置了 body,但请求没有 body + } + } + } + /// 与 Loader 保持一致的 Key 提取算法 fn extract_first_segment(&self, path: &str) -> String { path.trim_start_matches('/') @@ -113,4 +139,87 @@ impl MockRouter { .unwrap_or("root") .to_string() } -} \ No newline at end of file +} + +/// 规范化 YAML body:如果是字符串,尝试解析为 JSON +fn normalize_yaml_body(yaml_body: &serde_json::Value) -> serde_json::Value { + if let Some(s) = yaml_body.as_str() { + // 先 trim 去掉前导/尾随空格,再尝试解析为 JSON + let trimmed = s.trim(); + if let Ok(parsed) = serde_json::from_str::(trimmed) { + return parsed; + } + } + yaml_body.clone() +} + +/// 规范化 XML 字符串:去掉声明、多余空白、格式化为紧凑形式 +fn normalize_xml(xml: &str) -> String { + let mut result = xml.to_string(); + + // 去掉 XML 声明 + if let Some(pos) = result.find("?>") { + if result[..pos].contains(">() + .chunks(1) + .map(|c| c[0]) + .collect::(); + + // 分割成行,去掉每行首尾空白,过滤空行 + result = result + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::(); + + result +} + +/// Form 比较:YAML 中的键值对必须是请求的子集 +fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap) -> bool { + let yaml_map = match yaml_body.as_object() { + Some(obj) => obj, + None => return false, + }; + + for (key, yaml_val) in yaml_map { + let expected = yaml_val.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_val.to_string()); + if actual.get(key) != Some(&expected) { + return false; + } + } + true +} + +/// Multipart 比较:对象和数组形式都只匹配字段名是否存在 +fn compare_multipart_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap) -> bool { + // 方式 1:对象形式 - 只匹配键名是否存在(忽略值) + if let Some(yaml_map) = yaml_body.as_object() { + for key in yaml_map.keys() { + if !actual.contains_key(key) { + return false; + } + } + return true; + } + + // 方式 2:数组形式 - 只匹配字段名是否存在 + if let Some(yaml_array) = yaml_body.as_array() { + for yaml_field in yaml_array { + let field_name = yaml_field.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_field.to_string()); + if !actual.contains_key(&field_name) { + return false; + } + } + return true; + } + + false +}