feat: 增加request对body字段的支持
- 实现body字段对json语法,yaml语法的支持的支持 - 新增测试用例login_out.yaml
This commit is contained in:
@@ -7,6 +7,11 @@ request:
|
|||||||
Content-Type: "application/json"
|
Content-Type: "application/json"
|
||||||
Authorization: "111"
|
Authorization: "111"
|
||||||
host: "127.0.0.1:8080"
|
host: "127.0.0.1:8080"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"username":"user",
|
||||||
|
"password":"123"
|
||||||
|
}
|
||||||
response:
|
response:
|
||||||
status: 200
|
status: 200
|
||||||
headers:
|
headers:
|
||||||
|
|||||||
50
mocks/v1/auth/login_out.yaml
Normal file
50
mocks/v1/auth/login_out.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
- 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 # 模拟真实网络延迟
|
||||||
@@ -2,6 +2,7 @@ id: "prod_export_pdf"
|
|||||||
request:
|
request:
|
||||||
method: "GET"
|
method: "GET"
|
||||||
path: "/api/v1/products/report"
|
path: "/api/v1/products/report"
|
||||||
|
body: '{"username":"user","password":"123"}'
|
||||||
response:
|
response:
|
||||||
status: 200
|
status: 200
|
||||||
headers:
|
headers:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ use std::collections::HashMap;
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum MockSource {
|
pub enum MockSource {
|
||||||
/// 对应“一接口一文件”模式
|
/// 对应“一个接口一个文件”模式
|
||||||
Single(MockRule),
|
Single(MockRule),
|
||||||
/// 对应“一文件多接口”模式
|
/// 对应“一个文件多个接口”模式
|
||||||
Multiple(Vec<MockRule>),
|
Multiple(Vec<MockRule>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +39,8 @@ pub struct RequestMatcher {
|
|||||||
pub query_params: Option<HashMap<String, String>>,
|
pub query_params: Option<HashMap<String, String>>,
|
||||||
/// 选填:只有请求包含这些 Header 时才匹配
|
/// 选填:只有请求包含这些 Header 时才匹配
|
||||||
pub headers: Option<HashMap<String, String>>,
|
pub headers: Option<HashMap<String, String>>,
|
||||||
|
// 修改点:从 String 改为 Option<serde_json::Value>
|
||||||
|
pub body: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 响应内容定义
|
/// 响应内容定义
|
||||||
|
|||||||
@@ -23,19 +23,37 @@ pub async fn mock_handler(
|
|||||||
Query(params): Query<HashMap<String, String>>,
|
Query(params): Query<HashMap<String, String>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let path = req.uri().path();
|
// let path = req.uri().path();
|
||||||
|
// 1. 【关键】将需要的数据克隆出来,断开与 req 的借用关系
|
||||||
|
let path = req.uri().path().to_string();
|
||||||
|
let method_str = method.as_str().to_string();
|
||||||
// 1. 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用
|
// 1. 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用
|
||||||
|
|
||||||
|
// 2. 现在可以安全地消耗 req 了,因为上面没有指向 req 内部的引用了
|
||||||
|
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(_) => {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body(Body::from("Read body error"))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let incoming_json: Option<serde_json::Value> = serde_json::from_slice(&body_bytes).ok();
|
||||||
|
|
||||||
let mut req_headers = HashMap::new();
|
let mut req_headers = HashMap::new();
|
||||||
for (name, value) in headers.iter() {
|
for (name, value) in headers.iter() {
|
||||||
if let Ok(v) = value.to_str() {
|
if let Ok(v) = value.to_str() {
|
||||||
req_headers.insert(name.as_str().to_string(), v.to_string());
|
req_headers.insert(name.as_str().to_string(), v.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 执行匹配逻辑
|
// 2. 执行匹配逻辑
|
||||||
if let Some(rule) = state.router.match_rule(method.as_str(), path, ¶ms, &req_headers) {
|
if let Some(rule) =
|
||||||
|
state
|
||||||
|
.router
|
||||||
|
.match_rule(&method_str, &path, ¶ms, &req_headers, &incoming_json)
|
||||||
|
{
|
||||||
// 3. 处理模拟延迟
|
// 3. 处理模拟延迟
|
||||||
if let Some(ref settings) = rule.settings {
|
if let Some(ref settings) = rule.settings {
|
||||||
if let Some(delay) = settings.delay_ms {
|
if let Some(delay) = settings.delay_ms {
|
||||||
@@ -49,7 +67,6 @@ pub async fn mock_handler(
|
|||||||
|
|
||||||
// 注入 YAML 定义的 Header
|
// 注入 YAML 定义的 Header
|
||||||
if let Some(ref h) = rule.response.headers {
|
if let Some(ref h) = rule.response.headers {
|
||||||
|
|
||||||
for (k, v) in h {
|
for (k, v) in h {
|
||||||
// println!("{}:{}",k.clone(), v.clone());
|
// println!("{}:{}",k.clone(), v.clone());
|
||||||
response_builder = response_builder.header(k, v);
|
response_builder = response_builder.header(k, v);
|
||||||
@@ -65,23 +82,26 @@ pub async fn mock_handler(
|
|||||||
let body = Body::from_stream(stream);
|
let body = Body::from_stream(stream);
|
||||||
response_builder.body(body).unwrap()
|
response_builder.body(body).unwrap()
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => Response::builder()
|
||||||
Response::builder()
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.body(Body::from(format!(
|
||||||
.body(Body::from(format!("Mock Error: File not found at {}", file_path)))
|
"Mock Error: File not found at {}",
|
||||||
.unwrap()
|
file_path
|
||||||
}
|
)))
|
||||||
|
.unwrap(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// B. 内联模式:直接返回字符串内容
|
// B. 内联模式:直接返回字符串内容
|
||||||
response_builder.body(Body::from(rule.response.body.clone())).unwrap()
|
response_builder
|
||||||
|
.body(Body::from(rule.response.body.clone()))
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("请求头{:?}",req_headers.clone());
|
println!("请求头{:?}", req_headers.clone());
|
||||||
// 匹配失败返回 404
|
// 匹配失败返回 404
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.body(Body::from("No mock rule matched this request"))
|
.body(Body::from("No mock rule matched this request"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ impl MockRouter {
|
|||||||
path: &str,
|
path: &str,
|
||||||
queries: &HashMap<String, String>,
|
queries: &HashMap<String, String>,
|
||||||
headers: &HashMap<String, String>,
|
headers: &HashMap<String, String>,
|
||||||
|
incoming_body: &Option<serde_json::Value>, // 修改 1: 增加参数
|
||||||
) -> Option<&MockRule> {
|
) -> Option<&MockRule> {
|
||||||
// 1. 提取请求路径的首段作为索引 Key
|
// 1. 提取请求路径的首段作为索引 Key
|
||||||
let key = self.extract_first_segment(path);
|
let key = self.extract_first_segment(path);
|
||||||
@@ -26,7 +27,7 @@ impl MockRouter {
|
|||||||
if let Some(rules) = self.index.get(&key) {
|
if let Some(rules) = self.index.get(&key) {
|
||||||
// 3. 在候选集中进行线性深度匹配
|
// 3. 在候选集中进行线性深度匹配
|
||||||
for rule in rules {
|
for rule in rules {
|
||||||
if self.is_match(rule, method, path, queries, headers) {
|
if self.is_match(rule, method, path, queries, headers,incoming_body) {
|
||||||
return Some(rule);
|
return Some(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ impl MockRouter {
|
|||||||
path: &str,
|
path: &str,
|
||||||
queries: &HashMap<String, String>,
|
queries: &HashMap<String, String>,
|
||||||
headers: &HashMap<String, String>,
|
headers: &HashMap<String, String>,
|
||||||
|
incoming_body: &Option<serde_json::Value>, // 修改 3: 增加参数
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠)
|
// A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠)
|
||||||
if rule.request.method.to_uppercase() != method.to_uppercase() {
|
if rule.request.method.to_uppercase() != method.to_uppercase() {
|
||||||
@@ -78,6 +80,27 @@ impl MockRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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::<serde_json::Value>(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 但请求为空
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
// 替换为你的项目实际名称
|
// 确保 Cargo.toml 中有 serde_json
|
||||||
|
use serde_json::json;
|
||||||
use mock_server::config::{MockRule, MockSource};
|
use mock_server::config::{MockRule, MockSource};
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
use mock_server::router::MockRouter;
|
use mock_server::router::MockRouter;
|
||||||
@@ -10,18 +11,23 @@ use std::collections::HashMap;
|
|||||||
/// 模块一:验证 Config 反序列化逻辑
|
/// 模块一:验证 Config 反序列化逻辑
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_parsing_scenarios() {
|
fn test_config_parsing_scenarios() {
|
||||||
// 场景 A: 验证单接口配置 (Inline 模式)
|
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
||||||
let yaml_single = r#"
|
let yaml_single = r#"
|
||||||
id: "auth_v1"
|
id: "auth_v1"
|
||||||
request: { method: "POST", path: "/api/v1/login" }
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/v1/login"
|
||||||
|
body: { "user": "admin" }
|
||||||
response: { status: 200, body: "welcome" }
|
response: { status: 200, body: "welcome" }
|
||||||
"#;
|
"#;
|
||||||
let source_s: MockSource = serde_yaml::from_str(yaml_single).expect("解析单接口失败");
|
let source_s: MockSource = serde_yaml::from_str(yaml_single).expect("解析单接口失败");
|
||||||
let rules = source_s.flatten();
|
let rules = source_s.flatten();
|
||||||
assert_eq!(rules.len(), 1);
|
assert_eq!(rules.len(), 1);
|
||||||
assert_eq!(rules[0].request.path, "/api/v1/login");
|
// 验证 body 是否被成功解析为 Value::Object
|
||||||
|
assert!(rules[0].request.body.is_some());
|
||||||
|
assert_eq!(rules[0].request.body.as_ref().unwrap()["user"], "admin");
|
||||||
|
|
||||||
// 场景 B: 验证多接口配置 (Collection 模式)
|
// 场景 B: 验证多接口配置 (原有逻辑不变)
|
||||||
let yaml_multi = r#"
|
let yaml_multi = r#"
|
||||||
- id: "api_1"
|
- id: "api_1"
|
||||||
request: { method: "GET", path: "/health" }
|
request: { method: "GET", path: "/health" }
|
||||||
@@ -33,7 +39,7 @@ fn test_config_parsing_scenarios() {
|
|||||||
let source_m: MockSource = serde_yaml::from_str(yaml_multi).expect("解析多接口失败");
|
let source_m: MockSource = serde_yaml::from_str(yaml_multi).expect("解析多接口失败");
|
||||||
assert_eq!(source_m.flatten().len(), 2);
|
assert_eq!(source_m.flatten().len(), 2);
|
||||||
|
|
||||||
// 场景 C: 验证 Smart Body 的 file:// 协议字符串解析
|
// 场景 C: 验证 Smart Body 的 file:// 协议字符串解析 (原有逻辑不变)
|
||||||
let yaml_file = r#"
|
let yaml_file = r#"
|
||||||
id: "export_api"
|
id: "export_api"
|
||||||
request: { method: "GET", path: "/download" }
|
request: { method: "GET", path: "/download" }
|
||||||
@@ -44,48 +50,34 @@ fn test_config_parsing_scenarios() {
|
|||||||
assert!(rule.response.body.starts_with("file://"));
|
assert!(rule.response.body.starts_with("file://"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 模块二:验证 Loader 递归扫描与索引构建
|
/// 模块二:验证 Loader 递归扫描与索引构建 (不涉及 Matcher 逻辑,基本保持不变)
|
||||||
#[test]
|
#[test]
|
||||||
fn test_loader_recursive_indexing() {
|
fn test_loader_recursive_indexing() {
|
||||||
let temp_root = tempdir().expect("无法创建临时目录");
|
let temp_root = tempdir().expect("无法创建临时目录");
|
||||||
let root_path = temp_root.path();
|
let root_path = temp_root.path();
|
||||||
|
|
||||||
// 创建多级目录结构
|
|
||||||
let auth_path = root_path.join("v1/auth");
|
let auth_path = root_path.join("v1/auth");
|
||||||
fs::create_dir_all(&auth_path).unwrap();
|
fs::create_dir_all(&auth_path).unwrap();
|
||||||
|
|
||||||
// 1. 在深层目录写入一个单接口文件
|
|
||||||
let mut f1 = File::create(auth_path.join("login.yaml")).unwrap();
|
let mut f1 = File::create(auth_path.join("login.yaml")).unwrap();
|
||||||
writeln!(f1, "id: 'l1'\nrequest: {{ method: 'POST', path: '/api/v1/login' }}\nresponse: {{ status: 200, body: 'ok' }}").unwrap();
|
writeln!(f1, "id: 'l1'\nrequest: {{ method: 'POST', path: '/api/v1/login' }}\nresponse: {{ status: 200, body: 'ok' }}").unwrap();
|
||||||
|
|
||||||
// 2. 在根目录写入一个多接口文件
|
|
||||||
let mut f2 = File::create(root_path.join("sys.yaml")).unwrap();
|
let mut f2 = File::create(root_path.join("sys.yaml")).unwrap();
|
||||||
writeln!(f2, "- id: 's1'\n request: {{ method: 'GET', path: '/health' }}\n response: {{ status: 200, body: 'up' }}").unwrap();
|
writeln!(f2, "- id: 's1'\n request: {{ method: 'GET', path: '/health' }}\n response: {{ status: 200, body: 'up' }}").unwrap();
|
||||||
|
|
||||||
// 执行加载
|
|
||||||
let index = MockLoader::load_all_from_dir(root_path);
|
let index = MockLoader::load_all_from_dir(root_path);
|
||||||
|
|
||||||
// 验证逻辑:
|
assert!(index.contains_key("api"));
|
||||||
// 即使物理路径很深,索引 Key 必须是逻辑路径的首段
|
assert!(index.contains_key("health"));
|
||||||
assert!(
|
|
||||||
index.contains_key("api"),
|
|
||||||
"必须通过 /api/v1/login 提取出 'api' 键"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
index.contains_key("health"),
|
|
||||||
"必须通过 /health 提取出 'health' 键"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 验证扁平化后的总数
|
|
||||||
let total: usize = index.values().map(|v| v.len()).sum();
|
let total: usize = index.values().map(|v| v.len()).sum();
|
||||||
assert_eq!(total, 2);
|
assert_eq!(total, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_router_matching_logic() {
|
fn test_router_matching_logic() {
|
||||||
// 1. 准备模拟数据(模拟 Loader 的输出)
|
|
||||||
let mut index = HashMap::new();
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
|
// 1. 准备带有 Body 的规则
|
||||||
let rule_auth = serde_yaml::from_str::<MockSource>(
|
let rule_auth = serde_yaml::from_str::<MockSource>(
|
||||||
r#"
|
r#"
|
||||||
id: "auth_v1"
|
id: "auth_v1"
|
||||||
@@ -93,55 +85,76 @@ fn test_router_matching_logic() {
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
path: "/api/v1/login"
|
path: "/api/v1/login"
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
|
body: { "code": 123 }
|
||||||
response: { status: 200, body: "token_123" }
|
response: { status: 200, body: "token_123" }
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let rule_user = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "get_user"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/user/info"
|
|
||||||
query_params: { "id": "100" }
|
|
||||||
response: { status: 200, body: "user_info" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
// 构建 HashMap 索引(模仿 Loader 的行为)
|
|
||||||
index.insert(
|
|
||||||
"api".to_string(),
|
|
||||||
vec![rule_auth[0].clone(), rule_user[0].clone()],
|
|
||||||
);
|
|
||||||
|
|
||||||
|
index.insert("api".to_string(), vec![rule_auth[0].clone()]);
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
// 2. 测试场景 A:完全匹配
|
// 2. 测试场景 A:完全匹配 (包括 Body)
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
let matched = router.match_rule("POST", "/api/v1/login", &HashMap::new(), &headers);
|
// 构造请求 Body
|
||||||
|
let incoming_body = Some(json!({ "code": 123 }));
|
||||||
|
|
||||||
|
let matched = router.match_rule(
|
||||||
|
"POST",
|
||||||
|
"/api/v1/login",
|
||||||
|
&HashMap::new(),
|
||||||
|
&headers,
|
||||||
|
&incoming_body // 传入新参数
|
||||||
|
);
|
||||||
assert!(matched.is_some());
|
assert!(matched.is_some());
|
||||||
assert_eq!(matched.unwrap().id, "auth_v1");
|
assert_eq!(matched.unwrap().id, "auth_v1");
|
||||||
|
|
||||||
// 3. 测试场景 B:路径末尾斜杠归一化 (Trailing Slash)
|
// 3. 测试场景 B:Body 不匹配
|
||||||
let matched_slash = router.match_rule("POST", "/api/v1/login/", &HashMap::new(), &headers);
|
let wrong_body = Some(json!({ "code": 456 }));
|
||||||
assert!(matched_slash.is_some(), "应该忽略路径末尾的斜杠");
|
let matched_fail = router.match_rule(
|
||||||
|
"POST",
|
||||||
|
"/api/v1/login",
|
||||||
|
&HashMap::new(),
|
||||||
|
&headers,
|
||||||
|
&wrong_body
|
||||||
|
);
|
||||||
|
assert!(matched_fail.is_none(), "Body 不一致时不应匹配成功");
|
||||||
|
|
||||||
// 4. 测试场景 C:Query 参数子集匹配
|
// 4. 测试场景 C:智能字符串转换验证 (YAML 是字符串,请求是对象)
|
||||||
let mut queries = HashMap::new();
|
let rule_str_body = serde_yaml::from_str::<MockSource>(
|
||||||
queries.insert("id".to_string(), "100".to_string());
|
r#"
|
||||||
queries.insert("extra".to_string(), "unused".to_string()); // 额外的参数不应影响匹配
|
id: "str_match"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/str"
|
||||||
|
body: '{"type": "json_in_string"}' # 这里 YAML 解析为 Value::String
|
||||||
|
response: { status: 200, body: "ok" }
|
||||||
|
"#).unwrap().flatten();
|
||||||
|
|
||||||
let matched_query = router.match_rule("GET", "/api/user/info", &queries, &HashMap::new());
|
let mut index2 = HashMap::new();
|
||||||
assert!(matched_query.is_some());
|
index2.insert("api".to_string(), vec![rule_str_body[0].clone()]);
|
||||||
assert_eq!(matched_query.unwrap().id, "get_user");
|
let router2 = MockRouter::new(index2);
|
||||||
|
|
||||||
// 5. 测试场景 D:匹配失败(Method 错误)
|
let incoming_obj = Some(json!({ "type": "json_in_string" })); // 请求是 JSON 对象
|
||||||
let fail_method = router.match_rule("GET", "/api/v1/login", &HashMap::new(), &headers);
|
let matched_str = router2.match_rule(
|
||||||
assert!(fail_method.is_none());
|
"POST",
|
||||||
}
|
"/api/str",
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
|
&incoming_obj
|
||||||
|
);
|
||||||
|
assert!(matched_str.is_some(), "应该支持将 YAML 字符串 body 转换为对象进行匹配");
|
||||||
|
|
||||||
|
// 5. 测试场景 D:末尾斜杠兼容性测试
|
||||||
|
let matched_slash = router.match_rule(
|
||||||
|
"POST",
|
||||||
|
"/api/v1/login/",
|
||||||
|
&HashMap::new(),
|
||||||
|
&headers,
|
||||||
|
&incoming_body
|
||||||
|
);
|
||||||
|
assert!(matched_slash.is_some());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user