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:
2026-03-19 22:11:42 +08:00
parent 5f3269bad5
commit ab368ead1b
14 changed files with 2113 additions and 15 deletions

354
tests/router_test.rs Normal file
View 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());
}