feat: 实现核心匹配引擎与请求处理器并修复解析逻辑
- 新增 router 模块:实现基于路径首段索引子集匹配。 - 新增 handler 模块:集成 Axum 处理器,支持 Smart Body 协议与延迟模拟。 - 修复解析与匹配故障:修正 YAML 字段类型解析错误。
This commit is contained in:
79
Cargo.lock
generated
79
Cargo.lock
generated
@@ -277,6 +277,12 @@ version = "1.0.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.178"
|
version = "0.2.178"
|
||||||
@@ -345,9 +351,20 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -541,6 +558,15 @@ dependencies = [
|
|||||||
"unsafe-libyaml",
|
"unsafe-libyaml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.7"
|
version = "1.4.7"
|
||||||
@@ -602,6 +628,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.48.0"
|
version = "1.48.0"
|
||||||
@@ -679,9 +714,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
@@ -689,6 +736,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||||
|
dependencies = [
|
||||||
|
"nu-ansi-term",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -703,6 +776,12 @@ version = "0.2.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ serde_json = "1.0.147"
|
|||||||
# 物理目录递归扫描工具
|
# 物理目录递归扫描工具
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
|
||||||
|
tracing="0.1.44"
|
||||||
|
tracing-subscriber = "0.3.22"
|
||||||
|
|
||||||
# 性能优化:快速哈希(可选,用于路由匹配)
|
# 性能优化:快速哈希(可选,用于路由匹配)
|
||||||
#dashmap = "7.0.0-rc2"
|
#dashmap = "7.0.0-rc2"
|
||||||
# 热加载支持(扩展功能)
|
# 热加载支持(扩展功能)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ request:
|
|||||||
# 必须包含此 Header 才会匹配
|
# 必须包含此 Header 才会匹配
|
||||||
headers:
|
headers:
|
||||||
Content-Type: "application/json"
|
Content-Type: "application/json"
|
||||||
|
Authorization: "111"
|
||||||
|
host: "127.0.0.1:8080"
|
||||||
response:
|
response:
|
||||||
status: 200
|
status: 200
|
||||||
headers:
|
headers:
|
||||||
|
|||||||
87
src/handler.rs
Normal file
87
src/handler.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{Query, State},
|
||||||
|
http::{HeaderMap, Method, Request, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio_util::io::ReaderStream; // 需在 Cargo.toml 确认有 tokio-util
|
||||||
|
|
||||||
|
use crate::router::MockRouter;
|
||||||
|
|
||||||
|
/// 共享的应用状态
|
||||||
|
pub struct AppState {
|
||||||
|
pub router: MockRouter,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 全局统一请求处理函数
|
||||||
|
pub async fn mock_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
method: Method,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let path = req.uri().path();
|
||||||
|
|
||||||
|
// 1. 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用
|
||||||
|
let mut req_headers = HashMap::new();
|
||||||
|
for (name, value) in headers.iter() {
|
||||||
|
if let Ok(v) = value.to_str() {
|
||||||
|
req_headers.insert(name.as_str().to_string(), v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 执行匹配逻辑
|
||||||
|
if let Some(rule) = state.router.match_rule(method.as_str(), path, ¶ms, &req_headers) {
|
||||||
|
|
||||||
|
// 3. 处理模拟延迟
|
||||||
|
if let Some(ref settings) = rule.settings {
|
||||||
|
if let Some(delay) = settings.delay_ms {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 构建响应基础信息
|
||||||
|
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
|
||||||
|
let mut response_builder = Response::builder().status(status);
|
||||||
|
|
||||||
|
// 注入 YAML 定义的 Header
|
||||||
|
if let Some(ref h) = rule.response.headers {
|
||||||
|
|
||||||
|
for (k, v) in h {
|
||||||
|
// println!("{}:{}",k.clone(), v.clone());
|
||||||
|
response_builder = response_builder.header(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 执行 Smart Body 协议逻辑
|
||||||
|
if let Some(file_path) = rule.response.get_file_path() {
|
||||||
|
// A. 文件模式:异步打开文件并转换为流,实现低内存占用
|
||||||
|
match tokio::fs::File::open(file_path).await {
|
||||||
|
Ok(file) => {
|
||||||
|
let stream = ReaderStream::new(file);
|
||||||
|
let body = Body::from_stream(stream);
|
||||||
|
response_builder.body(body).unwrap()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Body::from(format!("Mock Error: File not found at {}", file_path)))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// B. 内联模式:直接返回字符串内容
|
||||||
|
response_builder.body(Body::from(rule.response.body.clone())).unwrap()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("请求头{:?}",req_headers.clone());
|
||||||
|
// 匹配失败返回 404
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::from("No mock rule matched this request"))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
// 声明模块并设为 pub,这样 tests/ 目录才能看到它们
|
// 声明模块并设为 pub,这样 tests/ 目录才能看到它们
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
|
pub mod router;
|
||||||
|
pub mod handler;
|
||||||
43
src/main.rs
43
src/main.rs
@@ -1,9 +1,44 @@
|
|||||||
// 使用项目名(下划线形式)引用 lib 中的内容
|
use std::net::SocketAddr;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{routing::any, Router};
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
|
use mock_server::router::MockRouter;
|
||||||
|
use mock_server::handler::{mock_handler, AppState};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// 你的启动逻辑...
|
// 1. 初始化日志(建议添加,方便观察加载情况)
|
||||||
let index = MockLoader::load_all_from_dir(std::path::Path::new("./mocks"));
|
// 需要在 Cargo.toml 添加 tracing-subscriber = "0.3"
|
||||||
println!("服务启动中...");
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// 2. 递归加载所有的 YAML 配置文件
|
||||||
|
let mocks_dir = Path::new("./mocks");
|
||||||
|
if !mocks_dir.exists() {
|
||||||
|
println!("Warning: 'mocks/' directory not found. Creating it...");
|
||||||
|
std::fs::create_dir_all(mocks_dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Scanning mocks directory...");
|
||||||
|
let index = MockLoader::load_all_from_dir(mocks_dir);
|
||||||
|
|
||||||
|
// 3. 构建路由引擎并包装为共享状态
|
||||||
|
let router_engine = MockRouter::new(index);
|
||||||
|
let shared_state = Arc::new(AppState {
|
||||||
|
router: router_engine,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 配置 Axum 路由
|
||||||
|
// 使用 any(mock_handler) 意味着它会接管所有 HTTP 方法和所有路径的请求
|
||||||
|
let app = Router::new()
|
||||||
|
.fallback(any(mock_handler))
|
||||||
|
.with_state(shared_state);
|
||||||
|
|
||||||
|
// 5. 启动服务
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
|
println!("🚀 Rust Mock Server is running on http://{}", addr);
|
||||||
|
println!("Ready to handle requests based on your YAML definitions.");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
93
src/router.rs
Normal file
93
src/router.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::config::MockRule;
|
||||||
|
|
||||||
|
pub struct MockRouter {
|
||||||
|
// 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则
|
||||||
|
pub index: HashMap<String, Vec<MockRule>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockRouter {
|
||||||
|
pub fn new(index: HashMap<String, Vec<MockRule>>) -> Self {
|
||||||
|
Self { index }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 核心匹配函数:根据请求信息寻找匹配的 Mock 规则
|
||||||
|
pub fn match_rule(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
path: &str,
|
||||||
|
queries: &HashMap<String, String>,
|
||||||
|
headers: &HashMap<String, String>,
|
||||||
|
) -> Option<&MockRule> {
|
||||||
|
// 1. 提取请求路径的首段作为索引 Key
|
||||||
|
let key = self.extract_first_segment(path);
|
||||||
|
println!("DEBUG: Request Key: '{}', Available Keys: {:?}", key, self.index.keys());
|
||||||
|
// 2. 从 HashMap 中快速定位候选规则列表 (O(k) 复杂度)
|
||||||
|
if let Some(rules) = self.index.get(&key) {
|
||||||
|
// 3. 在候选集中进行线性深度匹配
|
||||||
|
for rule in rules {
|
||||||
|
if self.is_match(rule, method, path, queries, headers) {
|
||||||
|
return Some(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 细粒度匹配逻辑
|
||||||
|
fn is_match(
|
||||||
|
&self,
|
||||||
|
rule: &MockRule,
|
||||||
|
method: &str,
|
||||||
|
path: &str,
|
||||||
|
queries: &HashMap<String, String>,
|
||||||
|
headers: &HashMap<String, String>,
|
||||||
|
) -> 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);
|
||||||
|
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);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
println!("DEBUG: [ID:{}] Method and Path matched! Checking headers...", rule.id);
|
||||||
|
// B. Query 参数校验 (子集匹配原则)
|
||||||
|
if let Some(ref required_queries) = rule.request.query_params {
|
||||||
|
for (key, val) in required_queries {
|
||||||
|
if queries.get(key) != Some(val) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C. Header 校验 (优化版)
|
||||||
|
if let Some(ref required_headers) = rule.request.headers {
|
||||||
|
for (key, val) in required_headers {
|
||||||
|
println!("{}:{}",key.clone(), val.clone());
|
||||||
|
// 方案:将 Key 统一转为小写比较,并检查请求头是否“包含”期望的值
|
||||||
|
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 !matched {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 与 Loader 保持一致的 Key 提取算法
|
||||||
|
fn extract_first_segment(&self, path: &str) -> String {
|
||||||
|
path.trim_start_matches('/')
|
||||||
|
.split('/')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("root")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ use std::fs::{self, File};
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
// 替换为你的项目实际名称
|
// 替换为你的项目实际名称
|
||||||
use mock_server::config::{MockSource, MockRule};
|
use mock_server::config::{MockRule, MockSource};
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
|
use mock_server::router::MockRouter;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// 模块一:验证 Config 反序列化逻辑
|
/// 模块一:验证 Config 反序列化逻辑
|
||||||
#[test]
|
#[test]
|
||||||
@@ -65,10 +67,81 @@ fn test_loader_recursive_indexing() {
|
|||||||
|
|
||||||
// 验证逻辑:
|
// 验证逻辑:
|
||||||
// 即使物理路径很深,索引 Key 必须是逻辑路径的首段
|
// 即使物理路径很深,索引 Key 必须是逻辑路径的首段
|
||||||
assert!(index.contains_key("api"), "必须通过 /api/v1/login 提取出 'api' 键");
|
assert!(
|
||||||
assert!(index.contains_key("health"), "必须通过 /health 提取出 'health' 键");
|
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]
|
||||||
|
fn test_router_matching_logic() {
|
||||||
|
// 1. 准备模拟数据(模拟 Loader 的输出)
|
||||||
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
|
let rule_auth = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
id: "auth_v1"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/v1/login"
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
response: { status: 200, body: "token_123" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.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()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
// 2. 测试场景 A:完全匹配
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/api/v1/login", &HashMap::new(), &headers);
|
||||||
|
assert!(matched.is_some());
|
||||||
|
assert_eq!(matched.unwrap().id, "auth_v1");
|
||||||
|
|
||||||
|
// 3. 测试场景 B:路径末尾斜杠归一化 (Trailing Slash)
|
||||||
|
let matched_slash = router.match_rule("POST", "/api/v1/login/", &HashMap::new(), &headers);
|
||||||
|
assert!(matched_slash.is_some(), "应该忽略路径末尾的斜杠");
|
||||||
|
|
||||||
|
// 4. 测试场景 C:Query 参数子集匹配
|
||||||
|
let mut queries = HashMap::new();
|
||||||
|
queries.insert("id".to_string(), "100".to_string());
|
||||||
|
queries.insert("extra".to_string(), "unused".to_string()); // 额外的参数不应影响匹配
|
||||||
|
|
||||||
|
let matched_query = router.match_rule("GET", "/api/user/info", &queries, &HashMap::new());
|
||||||
|
assert!(matched_query.is_some());
|
||||||
|
assert_eq!(matched_query.unwrap().id, "get_user");
|
||||||
|
|
||||||
|
// 5. 测试场景 D:匹配失败(Method 错误)
|
||||||
|
let fail_method = router.match_rule("GET", "/api/v1/login", &HashMap::new(), &headers);
|
||||||
|
assert!(fail_method.is_none());
|
||||||
|
}
|
||||||
|
|||||||
114
tests/loader_test.rs
Normal file
114
tests/loader_test.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
// 假设你的项目名在 Cargo.toml 中叫 mock_server
|
||||||
|
use mock_server::config::MockSource;
|
||||||
|
use mock_server::loader::MockLoader;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_deserialization() {
|
||||||
|
// 测试 1:验证单接口 YAML 解析
|
||||||
|
let single_yaml = r#"
|
||||||
|
id: "auth_v1"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/v1/login"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body: "inline_content"
|
||||||
|
"#;
|
||||||
|
let res: MockSource = serde_yaml::from_str(single_yaml).expect("应该成功解析单接口");
|
||||||
|
assert_eq!(res.flatten().len(), 1);
|
||||||
|
// assert_eq!(res.flatten()[0].id, "auth_v1");
|
||||||
|
|
||||||
|
// 测试 2:验证多接口 YAML 数组解析
|
||||||
|
let multi_yaml = r#"
|
||||||
|
- id: "api_1"
|
||||||
|
request: { method: "GET", path: "/1" }
|
||||||
|
response: { status: 200, body: "b1" }
|
||||||
|
- id: "api_2"
|
||||||
|
request: { method: "GET", path: "/2" }
|
||||||
|
response: { status: 200, body: "b2" }
|
||||||
|
"#;
|
||||||
|
let res_multi: MockSource = serde_yaml::from_str(multi_yaml).expect("应该成功解析接口数组");
|
||||||
|
assert_eq!(res_multi.flatten().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_recursive_loading_logic() {
|
||||||
|
// 创建临时 Mock 目录结构
|
||||||
|
let root_dir = tempdir().expect("创建临时目录失败");
|
||||||
|
let root_path = root_dir.path();
|
||||||
|
|
||||||
|
// 构造物理层级:mocks/v1/user/
|
||||||
|
let user_dir = root_path.join("v1/user");
|
||||||
|
fs::create_dir_all(&user_dir).unwrap();
|
||||||
|
|
||||||
|
// 在深层目录创建单接口文件
|
||||||
|
let mut file1 = File::create(user_dir.join("get_profile.yaml")).unwrap();
|
||||||
|
writeln!(
|
||||||
|
file1,
|
||||||
|
r#"
|
||||||
|
id: "user_profile"
|
||||||
|
request:
|
||||||
|
method: "GET"
|
||||||
|
path: "/api/v1/user/profile"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body: "profile_data"
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 在根目录创建多接口文件
|
||||||
|
let mut file2 = File::create(root_path.join("system.yaml")).unwrap();
|
||||||
|
writeln!(
|
||||||
|
file2,
|
||||||
|
r#"
|
||||||
|
- id: "sys_health"
|
||||||
|
request: {{ method: "GET", path: "/health" }}
|
||||||
|
response: {{ status: 200, body: "ok" }}
|
||||||
|
- id: "sys_version"
|
||||||
|
request: {{ method: "GET", path: "/version" }}
|
||||||
|
response: {{ status: 200, body: "1.0.0" }}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// writeln!(
|
||||||
|
// file2,
|
||||||
|
// r#"
|
||||||
|
// - id: "sys_health"
|
||||||
|
// request:
|
||||||
|
// method: "GET"
|
||||||
|
// path: "/health"
|
||||||
|
// response:
|
||||||
|
// status: 200
|
||||||
|
// body: "ok"
|
||||||
|
// - id: "sys_version"
|
||||||
|
// request:
|
||||||
|
// method: "GET"
|
||||||
|
// path: "/version"
|
||||||
|
// response:
|
||||||
|
// status: 200
|
||||||
|
// body: "1.0.0"
|
||||||
|
// "#
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
// 执行加载
|
||||||
|
let index = MockLoader::load_all_from_dir(root_path);
|
||||||
|
|
||||||
|
// 断言结果:
|
||||||
|
// 1. 检查 Key 是否根据路径首段正确提取(/api/v1/... -> api, /health -> health)
|
||||||
|
assert!(index.contains_key("api"), "索引应包含 'api' 键");
|
||||||
|
assert!(index.contains_key("health"), "索引应包含 'health' 键");
|
||||||
|
assert!(index.contains_key("version"), "索引应包含 'version' 键");
|
||||||
|
|
||||||
|
// 2. 检查规则总数
|
||||||
|
let total_rules: usize = index.values().map(|v| v.len()).sum();
|
||||||
|
assert_eq!(total_rules, 3, "总规则数应为 3");
|
||||||
|
|
||||||
|
// 3. 检查深层文件是否被正确读取
|
||||||
|
let api_rules = &index["api"];
|
||||||
|
assert_eq!(api_rules[0].id, "user_profile");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user