feat: 实现文件上传功能并完善测试覆盖
- 新增 upload.rs 模块,支持 multipart/form-data 文件上传 - 文件按日期存储在 storage/YYYY-MM-DD/ 目录下 - 使用 UUID 生成唯一文件名,保留原始扩展名 - 添加 axum-extra, uuid, chrono 依赖 新增测试用例: - config_test.rs: 6 个测试 (配置结构验证) - router_test.rs: 11 个测试 (路由匹配逻辑) - handler_test.rs: 8 个测试 (请求处理) - upload_test.rs: 13 个测试 (文件上传功能) 其他改进: - 优化 handler.rs 代码注释 - 更新 .gitignore 忽略 storage/ 和 .claude/ - 添加 CLAUDE.md 项目指南文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
140
tests/config_test.rs
Normal file
140
tests/config_test.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use mock_server::config::{MockResponse, MockSettings};
|
||||
|
||||
#[test]
|
||||
fn test_mock_response_file_protocol() {
|
||||
// 测试 1: 文件协议识别
|
||||
let response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "file://./data/file.txt".to_string(),
|
||||
};
|
||||
assert!(response.is_file_protocol());
|
||||
|
||||
// 测试 2: 非文件协议识别
|
||||
let inline_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "{\"key\": \"value\"}".to_string(),
|
||||
};
|
||||
assert!(!inline_response.is_file_protocol());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_response_file_path_extraction() {
|
||||
// 测试 1: 提取文件路径
|
||||
let response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "file://./data/file.txt".to_string(),
|
||||
};
|
||||
assert_eq!(response.get_file_path(), Some("./data/file.txt"));
|
||||
|
||||
// 测试 2: 提取相对路径
|
||||
let response2 = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "file:///absolute/path/file.pdf".to_string(),
|
||||
};
|
||||
assert_eq!(response2.get_file_path(), Some("/absolute/path/file.pdf"));
|
||||
|
||||
// 测试 3: 非文件协议返回 None
|
||||
let inline_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "inline content".to_string(),
|
||||
};
|
||||
assert_eq!(inline_response.get_file_path(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_settings_delay() {
|
||||
// 测试 1: 有延迟设置
|
||||
let settings = MockSettings {
|
||||
delay_ms: Some(1000),
|
||||
};
|
||||
assert_eq!(settings.delay_ms, Some(1000));
|
||||
|
||||
// 测试 2: 无延迟设置
|
||||
let settings = MockSettings { delay_ms: None };
|
||||
assert_eq!(settings.delay_ms, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_response_various_body_formats() {
|
||||
// 测试 1: JSON 格式
|
||||
let json_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: r#"{"code": 0, "message": "success"}"#.to_string(),
|
||||
};
|
||||
assert_eq!(json_response.status, 200);
|
||||
assert!(!json_response.is_file_protocol());
|
||||
|
||||
// 测试 2: HTML 格式
|
||||
let html_response = MockResponse {
|
||||
status: 200,
|
||||
headers: Some(vec![("Content-Type".to_string(), "text/html".to_string())]
|
||||
.into_iter()
|
||||
.collect()),
|
||||
body: "<html><body>Hello</body></html>".to_string(),
|
||||
};
|
||||
assert_eq!(html_response.status, 200);
|
||||
|
||||
// 测试 3: 纯文本
|
||||
let text_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "Plain text response".to_string(),
|
||||
};
|
||||
assert_eq!(text_response.body, "Plain text response");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_response_with_headers() {
|
||||
let mut headers = std::collections::HashMap::new();
|
||||
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||
headers.insert("Authorization".to_string(), "Bearer token".to_string());
|
||||
|
||||
let response = MockResponse {
|
||||
status: 200,
|
||||
headers: Some(headers),
|
||||
body: "response body".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(response.status, 200);
|
||||
assert!(response.headers.is_some());
|
||||
let response_headers = response.headers.unwrap();
|
||||
assert_eq!(response_headers.get("Content-Type"), Some(&"application/json".to_string()));
|
||||
assert_eq!(response_headers.get("Authorization"), Some(&"Bearer token".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_response_edge_cases() {
|
||||
// 测试 1: 空文件路径
|
||||
let empty_file_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "file://".to_string(),
|
||||
};
|
||||
assert!(empty_file_response.is_file_protocol());
|
||||
assert_eq!(empty_file_response.get_file_path(), Some(""));
|
||||
|
||||
// 测试 2: 空响应体
|
||||
let empty_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "".to_string(),
|
||||
};
|
||||
assert_eq!(empty_response.body, "");
|
||||
|
||||
// 测试 3: 包含特殊字符的文件路径
|
||||
let special_path_response = MockResponse {
|
||||
status: 200,
|
||||
headers: None,
|
||||
body: "file://./data/file with spaces.txt".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
special_path_response.get_file_path(),
|
||||
Some("./data/file with spaces.txt")
|
||||
);
|
||||
}
|
||||
441
tests/handler_test.rs
Normal file
441
tests/handler_test.rs
Normal file
@@ -0,0 +1,441 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Request,
|
||||
http::{HeaderMap, HeaderValue, Method, StatusCode},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use mock_server::config::MockSource;
|
||||
use mock_server::handler::{AppState, mock_handler};
|
||||
use mock_server::router::MockRouter;
|
||||
use tempfile::tempdir;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
/// 创建带有 Mock 规则的 AppState
|
||||
fn create_app_state_with_rules(rules: Vec<&str>) -> Arc<AppState> {
|
||||
let temp_dir = tempdir().expect("无法创建临时目录");
|
||||
let root_path = temp_dir.path();
|
||||
|
||||
let mut all_rules = Vec::new();
|
||||
for (i, yaml_content) in rules.iter().enumerate() {
|
||||
let file_path = root_path.join(format!("rule_{}.yaml", i));
|
||||
let mut file = File::create(&file_path).unwrap();
|
||||
writeln!(file, "{}", yaml_content).unwrap();
|
||||
|
||||
let content = fs::read_to_string(&file_path).unwrap();
|
||||
if let Ok(source) = serde_yaml::from_str::<MockSource>(&content) {
|
||||
all_rules.extend(source.flatten());
|
||||
}
|
||||
}
|
||||
|
||||
let mut index = HashMap::new();
|
||||
for rule in all_rules {
|
||||
let key = rule.request.path.trim_start_matches('/')
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or("root")
|
||||
.to_string();
|
||||
index.entry(key).or_insert_with(Vec::new).push(rule);
|
||||
}
|
||||
|
||||
let router = MockRouter::new(index);
|
||||
Arc::new(AppState { router })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_basic_request() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "basic_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/test"
|
||||
response:
|
||||
status: 200
|
||||
body: "test response"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/test")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::GET,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_with_query_params() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "query_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/search"
|
||||
query_params:
|
||||
q: "rust"
|
||||
response:
|
||||
status: 200
|
||||
body: "search results"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/search?q=rust")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let mut query_params = HashMap::new();
|
||||
query_params.insert("q".to_string(), "rust".to_string());
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::GET,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(query_params),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_with_headers() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "header_test"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/data"
|
||||
headers:
|
||||
Authorization: "Bearer token123"
|
||||
Content-Type: "application/json"
|
||||
response:
|
||||
status: 200
|
||||
body: "data response"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let request = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/api/data")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
|
||||
headers.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::POST,
|
||||
headers,
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_with_body() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "body_test"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/users"
|
||||
body:
|
||||
name: "John"
|
||||
age: 30
|
||||
response:
|
||||
status: 201
|
||||
body: "user created"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let body_json = serde_json::json!({ "name": "John", "age": 30 });
|
||||
let body_bytes = serde_json::to_vec(&body_json).unwrap();
|
||||
|
||||
let request = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/api/users")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::POST,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_no_match() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "existing_rule"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/existing"
|
||||
response:
|
||||
status: 200
|
||||
body: "exists"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/nonexistent")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::GET,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_response_headers() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "headers_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/with-headers"
|
||||
response:
|
||||
status: 200
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
Cache-Control: "no-cache"
|
||||
body: '{"key": "value"}'
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/with-headers")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::GET,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.headers().get("content-type").unwrap(),
|
||||
"application/json"
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("cache-control").unwrap(),
|
||||
"no-cache"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_different_status_codes() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml1 = r#"
|
||||
id: "success"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/success"
|
||||
response:
|
||||
status: 200
|
||||
body: "ok"
|
||||
"#;
|
||||
|
||||
let yaml2 = r#"
|
||||
id: "created"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/created"
|
||||
response:
|
||||
status: 201
|
||||
body: "created"
|
||||
"#;
|
||||
|
||||
let yaml3 = r#"
|
||||
id: "error"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/error"
|
||||
response:
|
||||
status: 500
|
||||
body: "server error"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml1, yaml2, yaml3]);
|
||||
|
||||
rt.block_on(async {
|
||||
// 测试 200
|
||||
let request1 = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/success")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response1 = mock_handler(
|
||||
axum::extract::State(state.clone()),
|
||||
Method::GET,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request1,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response1.status(), StatusCode::OK);
|
||||
|
||||
// 测试 201
|
||||
let request2 = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/api/created")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response2 = mock_handler(
|
||||
axum::extract::State(state.clone()),
|
||||
Method::POST,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request2,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response2.status(), StatusCode::CREATED);
|
||||
|
||||
// 测试 500
|
||||
let request3 = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/error")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response3 = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::GET,
|
||||
HeaderMap::new(),
|
||||
axum::extract::Query(HashMap::new()),
|
||||
request3,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response3.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_complex_matching() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
let yaml = r#"
|
||||
id: "complex"
|
||||
request:
|
||||
method: "PUT"
|
||||
path: "/api/users/123"
|
||||
query_params:
|
||||
update: "true"
|
||||
headers:
|
||||
Authorization: "Bearer secret"
|
||||
body:
|
||||
name: "Updated Name"
|
||||
response:
|
||||
status: 200
|
||||
body: "updated"
|
||||
"#;
|
||||
|
||||
let state = create_app_state_with_rules(vec![yaml]);
|
||||
|
||||
rt.block_on(async {
|
||||
let body_json = serde_json::json!({ "name": "Updated Name" });
|
||||
let body_bytes = serde_json::to_vec(&body_json).unwrap();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("content-type", HeaderValue::from_static("application/json"));
|
||||
headers.insert("authorization", HeaderValue::from_static("Bearer secret"));
|
||||
|
||||
let request = Request::builder()
|
||||
.method(Method::PUT)
|
||||
.uri("/api/users/123")
|
||||
.header("content-type", "application/json")
|
||||
.header("authorization", "Bearer secret")
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
let mut query_params = HashMap::new();
|
||||
query_params.insert("update".to_string(), "true".to_string());
|
||||
|
||||
let response = mock_handler(
|
||||
axum::extract::State(state),
|
||||
Method::PUT,
|
||||
headers,
|
||||
axum::extract::Query(query_params),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use std::io::Write;
|
||||
use tempfile::tempdir;
|
||||
// 确保 Cargo.toml 中有 serde_json
|
||||
use serde_json::json;
|
||||
use mock_server::config::{MockRule, MockSource};
|
||||
use mock_server::config::MockSource;
|
||||
use mock_server::loader::MockLoader;
|
||||
use mock_server::router::MockRouter;
|
||||
use std::collections::HashMap;
|
||||
|
||||
354
tests/router_test.rs
Normal file
354
tests/router_test.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
use mock_server::config::MockSource;
|
||||
use mock_server::router::MockRouter;
|
||||
use std::collections::HashMap;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_router_basic_method_path_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "basic_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/test"
|
||||
response: { status: 200, body: "ok" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试精确匹配
|
||||
let matched = router.match_rule("GET", "/api/test", &HashMap::new(), &HashMap::new(), &None);
|
||||
assert!(matched.is_some());
|
||||
assert_eq!(matched.unwrap().id, "basic_test");
|
||||
|
||||
// 测试方法不匹配
|
||||
let not_matched = router.match_rule("POST", "/api/test", &HashMap::new(), &HashMap::new(), &None);
|
||||
assert!(not_matched.is_none());
|
||||
|
||||
// 测试路径不匹配
|
||||
let not_matched = router.match_rule("GET", "/api/other", &HashMap::new(), &HashMap::new(), &None);
|
||||
assert!(not_matched.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_case_insensitive_method() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "case_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/data"
|
||||
response: { status: 200, body: "data" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试不同大小写的方法匹配
|
||||
assert!(router.match_rule("get", "/api/data", &HashMap::new(), &HashMap::new(), &None).is_some());
|
||||
assert!(router.match_rule("GET", "/api/data", &HashMap::new(), &HashMap::new(), &None).is_some());
|
||||
assert!(router.match_rule("Get", "/api/data", &HashMap::new(), &HashMap::new(), &None).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_path_trailing_slash() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "slash_test"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/users"
|
||||
response: { status: 200, body: "created" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试带和不带斜杠的路径匹配
|
||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &None).is_some());
|
||||
assert!(router.match_rule("POST", "/api/users/", &HashMap::new(), &HashMap::new(), &None).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_query_params_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "query_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/search"
|
||||
query_params:
|
||||
q: "rust"
|
||||
limit: "10"
|
||||
response: { status: 200, body: "results" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试查询参数完全匹配
|
||||
let mut queries = HashMap::new();
|
||||
queries.insert("q".to_string(), "rust".to_string());
|
||||
queries.insert("limit".to_string(), "10".to_string());
|
||||
queries.insert("extra".to_string(), "value".to_string()); // 额外参数应该忽略
|
||||
|
||||
assert!(router.match_rule("GET", "/api/search", &queries, &HashMap::new(), &None).is_some());
|
||||
|
||||
// 测试查询参数不匹配
|
||||
let mut wrong_queries = HashMap::new();
|
||||
wrong_queries.insert("q".to_string(), "python".to_string());
|
||||
|
||||
assert!(router.match_rule("GET", "/api/search", &wrong_queries, &HashMap::new(), &None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_headers_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "header_test"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/data"
|
||||
headers:
|
||||
Authorization: "Bearer token123"
|
||||
Content-Type: "application/json"
|
||||
response: { status: 200, body: "data" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试头部完全匹配(大小写不敏感)
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("authorization".to_string(), "Bearer token123".to_string());
|
||||
headers.insert("content-type".to_string(), "application/json".to_string());
|
||||
headers.insert("extra-header".to_string(), "value".to_string());
|
||||
|
||||
assert!(router.match_rule("GET", "/api/data", &HashMap::new(), &headers, &None).is_some());
|
||||
|
||||
// 测试头部不匹配
|
||||
let mut wrong_headers = HashMap::new();
|
||||
wrong_headers.insert("authorization".to_string(), "Bearer wrong".to_string());
|
||||
|
||||
assert!(router.match_rule("GET", "/api/data", &HashMap::new(), &wrong_headers, &None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_body_matching() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "body_test"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/users"
|
||||
body:
|
||||
name: "John"
|
||||
age: 30
|
||||
response: { status: 200, body: "created" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试 Body 完全匹配
|
||||
let body = Some(json!({ "name": "John", "age": 30 }));
|
||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &body).is_some());
|
||||
|
||||
// 测试 Body 不匹配
|
||||
let wrong_body = Some(json!({ "name": "Jane", "age": 25 }));
|
||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &wrong_body).is_none());
|
||||
|
||||
// 测试请求无 Body
|
||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_body_string_to_json() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
// YAML 中 body 是字符串,但内容是 JSON
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "str_body_test"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/login"
|
||||
body: '{"username": "admin", "password": "secret"}'
|
||||
response: { status: 200, body: "token" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 请求中 body 是 JSON 对象,应该匹配
|
||||
let body = Some(json!({ "username": "admin", "password": "secret" }));
|
||||
assert!(router.match_rule("POST", "/api/login", &HashMap::new(), &HashMap::new(), &body).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_multiple_rules_same_segment() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule1 = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "rule1"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/users"
|
||||
response: { status: 200, body: "users" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
let rule2 = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "rule2"
|
||||
request:
|
||||
method: "POST"
|
||||
path: "/api/users"
|
||||
response: { status: 201, body: "created" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule1[0].clone(), rule2[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试不同方法匹配不同规则
|
||||
let get_matched = router.match_rule("GET", "/api/users", &HashMap::new(), &HashMap::new(), &None);
|
||||
assert!(get_matched.is_some());
|
||||
assert_eq!(get_matched.unwrap().id, "rule1");
|
||||
|
||||
let post_matched = router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &None);
|
||||
assert!(post_matched.is_some());
|
||||
assert_eq!(post_matched.unwrap().id, "rule2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_complex_matching_scenario() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let complex_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "complex_rule"
|
||||
request:
|
||||
method: "PUT"
|
||||
path: "/api/users/123"
|
||||
query_params:
|
||||
update: "true"
|
||||
headers:
|
||||
Authorization: "Bearer token456"
|
||||
Content-Type: "application/json"
|
||||
body:
|
||||
name: "Updated"
|
||||
response: { status: 200, body: "updated" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![complex_rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试完全匹配所有条件
|
||||
let mut queries = HashMap::new();
|
||||
queries.insert("update".to_string(), "true".to_string());
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("authorization".to_string(), "Bearer token456".to_string());
|
||||
headers.insert("content-type".to_string(), "application/json".to_string());
|
||||
|
||||
let body = Some(json!({ "name": "Updated" }));
|
||||
|
||||
let matched = router.match_rule("PUT", "/api/users/123", &queries, &headers, &body);
|
||||
assert!(matched.is_some());
|
||||
assert_eq!(matched.unwrap().id, "complex_rule");
|
||||
|
||||
// 测试缺少任何条件都不匹配
|
||||
let no_body_matched = router.match_rule("PUT", "/api/users/123", &queries, &headers, &None);
|
||||
assert!(no_body_matched.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_root_path_handling() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let root_rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "root_rule"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/"
|
||||
response: { status: 200, body: "root" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
// 根路径会被提取为空字符串 key
|
||||
index.insert("".to_string(), vec![root_rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试根路径匹配
|
||||
assert!(router.match_rule("GET", "/", &HashMap::new(), &HashMap::new(), &None).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_router_nonexistent_key() {
|
||||
let mut index = HashMap::new();
|
||||
|
||||
let rule = serde_yaml::from_str::<MockSource>(
|
||||
r#"
|
||||
id: "test_rule"
|
||||
request:
|
||||
method: "GET"
|
||||
path: "/api/data"
|
||||
response: { status: 200, body: "data" }
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.flatten();
|
||||
|
||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
||||
let router = MockRouter::new(index);
|
||||
|
||||
// 测试请求不同段路径,应该不匹配
|
||||
assert!(router.match_rule("GET", "/health", &HashMap::new(), &HashMap::new(), &None).is_none());
|
||||
assert!(router.match_rule("GET", "/v2/data", &HashMap::new(), &HashMap::new(), &None).is_none());
|
||||
}
|
||||
246
tests/upload_test.rs
Normal file
246
tests/upload_test.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use mock_server::upload::{UploadResult, FileInfo};
|
||||
use tempfile::TempDir;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
/// 创建模拟的 multipart 数据
|
||||
fn create_multipart_body(boundary: &str, files: Vec<(&str, &str, &[u8])>) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
|
||||
for (field_name, filename, content) in files {
|
||||
body.extend_from_slice(
|
||||
format!(
|
||||
"--{}\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n\r\n",
|
||||
boundary, field_name, filename
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
body.extend_from_slice(content);
|
||||
body.extend_from_slice(b"\r\n");
|
||||
}
|
||||
|
||||
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upload_result_success() {
|
||||
let file_info = FileInfo {
|
||||
original_name: "test.txt".to_string(),
|
||||
stored_name: "uuid-test.txt".to_string(),
|
||||
path: "storage/2026-03-19/uuid-test.txt".to_string(),
|
||||
size: 1024,
|
||||
content_type: Some("text/plain".to_string()),
|
||||
};
|
||||
|
||||
let result = UploadResult::success(vec![file_info]);
|
||||
|
||||
// 验证序列化
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
assert!(json.contains("test.txt"));
|
||||
assert!(json.contains("success"));
|
||||
assert!(json.contains("1024"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upload_result_error() {
|
||||
let result = UploadResult::error("Test error message".to_string());
|
||||
|
||||
assert!(!result.success);
|
||||
assert_eq!(result.message, "Test error message");
|
||||
assert!(result.files.is_empty());
|
||||
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
assert!(json.contains("Test error message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_info_serialization() {
|
||||
let file_info = FileInfo {
|
||||
original_name: "document.pdf".to_string(),
|
||||
stored_name: "550e8400-e29b-41d4-a716-446655440000.pdf".to_string(),
|
||||
path: "storage/2026-03-19/550e8400-e29b-41d4-a716-446655440000.pdf".to_string(),
|
||||
size: 2048,
|
||||
content_type: Some("application/pdf".to_string()),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&file_info).unwrap();
|
||||
|
||||
assert!(json.contains("document.pdf"));
|
||||
assert!(json.contains("2048"));
|
||||
assert!(json.contains("application/pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upload_result_into_response() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let result = UploadResult::success(vec![]);
|
||||
let response = result.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let result_error = UploadResult::error("Error".to_string());
|
||||
let response_error = result_error.into_response();
|
||||
|
||||
assert_eq!(response_error.status(), StatusCode::BAD_REQUEST);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_storage_directory_creation() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage_path = temp_dir.path().join("storage");
|
||||
|
||||
// 测试目录创建
|
||||
tokio::fs::create_dir_all(&storage_path).await.unwrap();
|
||||
assert!(storage_path.exists());
|
||||
|
||||
// 测试日期目录创建
|
||||
let date_dir_name = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let date_dir = storage_path.join(&date_dir_name);
|
||||
tokio::fs::create_dir_all(&date_dir).await.unwrap();
|
||||
assert!(date_dir.exists());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_save_and_read() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("test_file.txt");
|
||||
|
||||
let content = b"Test file content";
|
||||
|
||||
// 写入文件
|
||||
tokio::fs::write(&file_path, content).await.unwrap();
|
||||
assert!(file_path.exists());
|
||||
|
||||
// 读取文件
|
||||
let read_content = tokio::fs::read(&file_path).await.unwrap();
|
||||
assert_eq!(read_content, content);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multipart_body_creation() {
|
||||
let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
|
||||
let content = b"Hello, World!";
|
||||
|
||||
let body = create_multipart_body(boundary, vec![("file", "test.txt", content)]);
|
||||
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
|
||||
assert!(body_str.contains(boundary));
|
||||
assert!(body_str.contains("test.txt"));
|
||||
assert!(body_str.contains("Hello, World!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_files_multipart() {
|
||||
let boundary = "----WebKitFormBoundary";
|
||||
let files = vec![
|
||||
("file1", "a.txt", b"content a" as &[u8]),
|
||||
("file2", "b.txt", b"content b" as &[u8]),
|
||||
];
|
||||
|
||||
let body = create_multipart_body(boundary, files);
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
|
||||
assert!(body_str.contains("a.txt"));
|
||||
assert!(body_str.contains("b.txt"));
|
||||
assert!(body_str.contains("content a"));
|
||||
assert!(body_str.contains("content b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uuid_generation() {
|
||||
use uuid::Uuid;
|
||||
|
||||
let uuid1 = Uuid::new_v4();
|
||||
let uuid2 = Uuid::new_v4();
|
||||
|
||||
// UUID 应该是唯一的
|
||||
assert_ne!(uuid1, uuid2);
|
||||
|
||||
// UUID 应该是有效的
|
||||
assert!(!uuid1.is_nil());
|
||||
assert!(!uuid2.is_nil());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_directory_format() {
|
||||
let date_dir_name = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
|
||||
// 验证日期格式
|
||||
assert!(date_dir_name.contains('-'));
|
||||
let parts: Vec<&str> = date_dir_name.split('-').collect();
|
||||
assert_eq!(parts.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_extension_extraction() {
|
||||
let test_cases = vec![
|
||||
("test.txt", ".txt"),
|
||||
("document.pdf", ".pdf"),
|
||||
("image.png", ".png"),
|
||||
("archive.tar.gz", ".gz"),
|
||||
("noextension", ""),
|
||||
];
|
||||
|
||||
for (filename, expected_ext) in test_cases {
|
||||
let extension = std::path::Path::new(filename)
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| format!(".{}", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
assert_eq!(extension, expected_ext);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_file_content() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("large_file.bin");
|
||||
|
||||
// 创建 1MB 的数据
|
||||
let large_content = vec![0u8; 1024 * 1024];
|
||||
|
||||
tokio::fs::write(&file_path, &large_content).await.unwrap();
|
||||
|
||||
let file_size = tokio::fs::metadata(&file_path).await.unwrap().len();
|
||||
assert_eq!(file_size, 1024 * 1024);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_file_content() {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("binary.bin");
|
||||
|
||||
// 创建二进制数据
|
||||
let binary_data: Vec<u8> = (0..=255).collect();
|
||||
|
||||
tokio::fs::write(&file_path, &binary_data).await.unwrap();
|
||||
|
||||
let read_data = tokio::fs::read(&file_path).await.unwrap();
|
||||
assert_eq!(read_data.len(), 256);
|
||||
assert_eq!(read_data, binary_data);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user