feat: mock配置迁移至JSON格式并修复body匹配
- 将mock配置从YAML格式迁移到JSON格式 - 修复JSON字符串格式body匹配失败问题 - 添加MCP功能模块 - 更新mock-spec.md规范文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
---
|
---
|
||||||
paths:
|
paths:
|
||||||
- "mocks/**/*.{yml,yaml}"
|
- "mocks/**/*.json"
|
||||||
---
|
---
|
||||||
|
|
||||||
## YAML 配置规范
|
## JSON 配置规范
|
||||||
|
|
||||||
AI 在生成 Mock 规则时必须遵循以下格式。
|
AI 在生成 Mock 规则时必须遵循以下格式。
|
||||||
|
|
||||||
### 字段说明
|
### 字段说明
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|------|------|:----:|------|
|
|----------------------|--------|:--:|----------------------------------|
|
||||||
| id | string | 是 | 规则唯一标识 |
|
| name | string | 是 | 规则唯一标识 |
|
||||||
| request.method | string | 是 | HTTP 方法 (GET/POST/PUT/DELETE...) |
|
| request.method | string | 是 | HTTP 方法 (GET/POST/PUT/DELETE...) |
|
||||||
| request.path | string | 是 | 请求路径,精确匹配 |
|
| request.path | string | 是 | 请求路径,精确匹配 |
|
||||||
| request.query_params | object | 否 | 查询参数匹配 |
|
| request.query_params | object | 否 | 查询参数匹配 |
|
||||||
@@ -30,58 +30,91 @@ AI 在生成 Mock 规则时必须遵循以下格式。
|
|||||||
|
|
||||||
### 配置示例
|
### 配置示例
|
||||||
|
|
||||||
**单接口模式:**
|
**基础示例:**
|
||||||
|
|
||||||
```yaml
|
```json
|
||||||
id: "login"
|
{
|
||||||
request:
|
"name": "login",
|
||||||
method: "POST"
|
"request": {
|
||||||
path: "/api/login"
|
"method": "POST",
|
||||||
body: { "username": "test" }
|
"path": "/v1/auth/login",
|
||||||
response:
|
"body": {
|
||||||
status: 200
|
"username": "user001",
|
||||||
body: '{"token": "xxx"}'
|
"password": "password123"
|
||||||
settings:
|
}
|
||||||
delay_ms: 100
|
},
|
||||||
```
|
"response": {
|
||||||
|
"status": 200,
|
||||||
**多接口模式(数组):**
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
```yaml
|
},
|
||||||
- id: "get-users"
|
"body": "{\"token\": \"xxx\"}"
|
||||||
request: { method: "GET", path: "/api/users" }
|
},
|
||||||
response: { status: 200, body: '{"users": []}' }
|
"settings": {
|
||||||
|
"delay_ms": 100
|
||||||
- id: "create-user"
|
}
|
||||||
request: { method: "POST", path: "/api/users" }
|
}
|
||||||
response: { status: 201, body: '{"id": 1}' }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**带查询参数和请求头:**
|
**带查询参数和请求头:**
|
||||||
|
|
||||||
```yaml
|
```json
|
||||||
id: "search-users"
|
{
|
||||||
request:
|
"name": "search_users",
|
||||||
method: "GET"
|
"request": {
|
||||||
path: "/api/users"
|
"method": "GET",
|
||||||
query_params: { "role": "admin" }
|
"path": "/v1/users",
|
||||||
headers: { "Authorization": "Bearer token" }
|
"query_params": {
|
||||||
response:
|
"role": "admin"
|
||||||
status: 200
|
},
|
||||||
headers: { "X-Total-Count": "100" }
|
"headers": {
|
||||||
body: '{"users": []}'
|
"Authorization": "Bearer token"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"X-Total-Count": "100"
|
||||||
|
},
|
||||||
|
"body": "{\"users\": []}"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**文件响应:**
|
**文件响应:**
|
||||||
|
|
||||||
```yaml
|
```json
|
||||||
id: "download-pdf"
|
{
|
||||||
request:
|
"name": "download_pdf",
|
||||||
method: "GET"
|
"request": {
|
||||||
path: "/api/download"
|
"method": "GET",
|
||||||
response:
|
"path": "/v1/download"
|
||||||
status: 200
|
},
|
||||||
body: "file://./data/large-file.pdf"
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/pdf"
|
||||||
|
},
|
||||||
|
"body": "file://./storage/data/report.pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字符串格式 Body(兼容格式):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string_body_example",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/api/test",
|
||||||
|
"body": "{\"username\":\"user001\",\"password\":\"password123\"}"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"body": "{\"success\": true}"
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> `file://` 支持两种路径:
|
> `file://` 支持两种路径:
|
||||||
|
|||||||
380
Cargo.lock
generated
380
Cargo.lock
generated
@@ -2,12 +2,27 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
@@ -60,6 +75,12 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -72,12 +93,28 @@ version = "2.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -85,10 +122,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "chrono"
|
||||||
version = "1.0.2"
|
version = "0.4.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
@@ -106,6 +163,12 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -124,6 +187,21 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -131,6 +209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -139,6 +218,23 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -168,9 +264,13 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -188,12 +288,6 @@ dependencies = [
|
|||||||
"wasip2",
|
"wasip2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.16.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -277,13 +371,27 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "iana-time-zone"
|
||||||
version = "2.12.1"
|
version = "0.1.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"android_system_properties",
|
||||||
"hashbrown",
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -312,6 +420,16 @@ 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 = "js-sys"
|
||||||
|
version = "0.3.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kqueue"
|
name = "kqueue"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -403,9 +521,10 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"notify",
|
"notify",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
|
"rmcp",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -459,6 +578,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -488,6 +616,12 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -539,6 +673,38 @@ dependencies = [
|
|||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmcp"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33a0110d28bd076f39e14bfd5b0340216dd18effeb5d02b43215944cc3e5c751"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"paste",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rmcp-macros",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmcp-macros"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6e2b2fd7497540489fa2db285edd43b7ed14c49157157438664278da6e42a7a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -552,6 +718,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
@@ -567,6 +739,30 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"schemars_derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars_derive"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_derive_internals",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -603,6 +799,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive_internals"
|
||||||
|
version = "0.29.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.147"
|
version = "1.0.147"
|
||||||
@@ -639,19 +846,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_yaml"
|
|
||||||
version = "0.9.34+deprecated"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
"unsafe-libyaml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -661,6 +855,12 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.7"
|
version = "1.4.7"
|
||||||
@@ -722,6 +922,26 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -864,12 +1084,6 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unsafe-libyaml"
|
|
||||||
version = "0.2.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -901,6 +1115,51 @@ dependencies = [
|
|||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@@ -910,12 +1169,65 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.60.2"
|
version = "0.60.2"
|
||||||
|
|||||||
15
Cargo.toml
15
Cargo.toml
@@ -3,6 +3,10 @@ name = "mock_server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
mcp = ["rmcp", "schemars"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# 核心 Web 框架
|
# 核心 Web 框架
|
||||||
axum = "0.8.8"
|
axum = "0.8.8"
|
||||||
@@ -12,9 +16,8 @@ tokio={version = "1.48.0",features = ["full"]}
|
|||||||
tokio-util = {version = "0.7.17",features = ["io"]}
|
tokio-util = {version = "0.7.17",features = ["io"]}
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
|
||||||
# 序列化与 YAML 解析
|
# 序列化
|
||||||
serde = {version = "1.0.228",features = ["derive"]}
|
serde = {version = "1.0.228",features = ["derive"]}
|
||||||
serde_yaml = "0.9.34+deprecated"
|
|
||||||
serde_json = "1.0.147"
|
serde_json = "1.0.147"
|
||||||
|
|
||||||
# 物理目录递归扫描工具
|
# 物理目录递归扫描工具
|
||||||
@@ -23,13 +26,13 @@ walkdir = "2.5.0"
|
|||||||
tracing="0.1.44"
|
tracing="0.1.44"
|
||||||
tracing-subscriber = "0.3.22"
|
tracing-subscriber = "0.3.22"
|
||||||
|
|
||||||
# 性能优化:快速哈希(可选,用于路由匹配)
|
|
||||||
#dashmap = "7.0.0-rc2"
|
|
||||||
# 热加载支持(扩展功能)
|
# 热加载支持(扩展功能)
|
||||||
notify = "8.2.0"
|
notify = "8.2.0"
|
||||||
notify-debouncer-mini = "0.6.0"
|
notify-debouncer-mini = "0.6.0"
|
||||||
# 路径处理
|
|
||||||
#pathdiff = "0.2.3"
|
# MCP Server 支持(可选)
|
||||||
|
rmcp = { version = "0.1", features = ["server"], optional = true }
|
||||||
|
schemars = { version = "0.8", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.24.0"
|
tempfile = "3.24.0"
|
||||||
26
mocks/v1/auth/login_001.json
Normal file
26
mocks/v1/auth/login_001.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "user_login_001",
|
||||||
|
"request": {
|
||||||
|
"path": "/v1/auth/login",
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6",
|
||||||
|
"host": "127.0.0.1:8080"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"username": "user001",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"登录成功\",\"data\":{\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6\",\"userId\":10001,\"username\":\"user001\",\"role\":\"administrator\"}}"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"delay_ms": 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
26
mocks/v1/auth/login_002.json
Normal file
26
mocks/v1/auth/login_002.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
26
mocks/v1/auth/login_003.json
Normal file
26
mocks/v1/auth/login_003.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
23
mocks/v1/auth/login_004.json
Normal file
23
mocks/v1/auth/login_004.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
mocks/v1/auth/login_005.json
Normal file
23
mocks/v1/auth/login_005.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# 用户登录 - 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
mocks/v1/auth/register.json
Normal file
22
mocks/v1/auth/register.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "user_register",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/auth/register",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"username": "newuser",
|
||||||
|
"password": "newpass123",
|
||||||
|
"email": "newuser@example.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 201,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"注册成功\",\"data\":{\"userId\":10002,\"username\":\"newuser\",\"email\":\"newuser@example.com\",\"createdAt\":\"2026-03-27T10:00:00Z\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 用户注册 - JSON 格式
|
|
||||||
name: "user_register"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/auth/register"
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body:
|
|
||||||
username: "newuser"
|
|
||||||
password: "newpass123"
|
|
||||||
email: "newuser@example.com"
|
|
||||||
response:
|
|
||||||
status: 201
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "注册成功",
|
|
||||||
"data": {
|
|
||||||
"userId": 10002,
|
|
||||||
"username": "newuser",
|
|
||||||
"email": "newuser@example.com",
|
|
||||||
"createdAt": "2026-03-27T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 用户登录 - JSON 格式
|
|
||||||
name: "user_login_001"
|
|
||||||
request:
|
|
||||||
path: "/v1/auth/login"
|
|
||||||
method: "POST"
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body:
|
|
||||||
username: "user001"
|
|
||||||
password: "password123"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "登录成功",
|
|
||||||
"data": {
|
|
||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6",
|
|
||||||
"userId": 10001,
|
|
||||||
"username": "user001",
|
|
||||||
"role": "administrator"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 2000 # 模拟真实网络延迟
|
|
||||||
18
mocks/v1/data/export.json
Normal file
18
mocks/v1/data/export.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "data_export",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/data/export",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><userId>10001</userId><format>xml</format></request>"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><response><code>0</code><message>导出成功</message><data><user><id>10001</id><name>管理员</name><email>admin@example.com</email></user></data></response>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# XML 数据导出 - XML 格式
|
|
||||||
name: "data_export"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/data/export"
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/xml"
|
|
||||||
body: |
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<request>
|
|
||||||
<userId>10001</userId>
|
|
||||||
<format>xml</format>
|
|
||||||
</request>
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/xml"
|
|
||||||
body: |
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<response>
|
|
||||||
<code>0</code>
|
|
||||||
<message>导出成功</message>
|
|
||||||
<data>
|
|
||||||
<user>
|
|
||||||
<id>10001</id>
|
|
||||||
<name>管理员</name>
|
|
||||||
<email>admin@example.com</email>
|
|
||||||
</user>
|
|
||||||
</data>
|
|
||||||
</response>
|
|
||||||
14
mocks/v1/health.json
Normal file
14
mocks/v1/health.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "health_check",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/health"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"status\":\"healthy\",\"version\":\"2.0.0\",\"timestamp\":\"2026-03-27T10:00:00Z\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# 健康检查 - GET 无 Body
|
|
||||||
name: "health_check"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/v1/health"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"status": "healthy",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"timestamp": "2026-03-27T10:00:00Z"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
name: "prod_export_pdf"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/v1/products/report"
|
|
||||||
body: '{"username":"user001","password":"password123"}'
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/pdf"
|
|
||||||
Content-Disposition: "attachment; filename=report.pdf"
|
|
||||||
# 智能协议:引擎会自动识别前缀并异步读取磁盘文件
|
|
||||||
body: "file://./storage/v1/hello.pdf"
|
|
||||||
16
mocks/v1/products/export_pdf.json
Normal file
16
mocks/v1/products/export_pdf.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "prod_export_pdf",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/products/report",
|
||||||
|
"body": "{\"username\":\"user001\",\"password\":\"password123\"}"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/pdf",
|
||||||
|
"Content-Disposition": "attachment; filename=report.pdf"
|
||||||
|
},
|
||||||
|
"body": "file://./storage/v1/hello.pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# 上传头像 - Multipart 格式(数组形式,只匹配字段名)
|
|
||||||
- name: "user_upload_avatar_001"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/user/avatar"
|
|
||||||
headers:
|
|
||||||
Content-Type: "multipart/form-data"
|
|
||||||
body:
|
|
||||||
- "avatar1"
|
|
||||||
- "description1"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "头像上传成功",
|
|
||||||
"data": {
|
|
||||||
"url": "https://cdn.example.com/v1/avatars/10001.jpg",
|
|
||||||
"size": 204800,
|
|
||||||
"filename": "avatar.jpg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: "user_upload_avatar_002"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/user/avatar"
|
|
||||||
headers:
|
|
||||||
Content-Type: "multipart/form-data"
|
|
||||||
body:
|
|
||||||
avatar2: "avatar"
|
|
||||||
description2: "description"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "头像上传成功",
|
|
||||||
"data": {
|
|
||||||
"url": "https://cdn.example.com/v1/avatars/10002.jpg",
|
|
||||||
"size": 204800,
|
|
||||||
"filename": "avatar.jpg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
- name: "user_upload_avatar_003"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/user/avatar"
|
|
||||||
headers:
|
|
||||||
Content-Type: "multipart/form-data"
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"avatar3": "avatar"
|
|
||||||
"description3": "description"
|
|
||||||
}
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "头像上传成功",
|
|
||||||
"data": {
|
|
||||||
"url": "https://cdn.example.com/v1/avatars/10003.jpg",
|
|
||||||
"size": 204800,
|
|
||||||
"filename": "avatar.jpg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
mocks/v1/user/avatar_001.json
Normal file
18
mocks/v1/user/avatar_001.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "user_upload_avatar_001",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/user/avatar",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
"body": ["avatar1", "description1"]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"头像上传成功\",\"data\":{\"url\":\"https://cdn.example.com/v1/avatars/10001.jpg\",\"size\":204800,\"filename\":\"avatar.jpg\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
mocks/v1/user/avatar_002.json
Normal file
21
mocks/v1/user/avatar_002.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "user_upload_avatar_002",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/user/avatar",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"avatar2": "avatar",
|
||||||
|
"description2": "description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"头像上传成功\",\"data\":{\"url\":\"https://cdn.example.com/v1/avatars/10002.jpg\",\"size\":204800,\"filename\":\"avatar.jpg\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
mocks/v1/user/avatar_003.json
Normal file
21
mocks/v1/user/avatar_003.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "user_upload_avatar_003",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/user/avatar",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"avatar3": "avatar",
|
||||||
|
"description3": "description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"头像上传成功\",\"data\":{\"url\":\"https://cdn.example.com/v1/avatars/10003.jpg\",\"size\":204800,\"filename\":\"avatar.jpg\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
mocks/v1/user/download.json
Normal file
18
mocks/v1/user/download.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "user_download",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/user/download",
|
||||||
|
"query_params": {
|
||||||
|
"format": "json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Content-Disposition": "attachment; filename=user_data.json"
|
||||||
|
},
|
||||||
|
"body": "file://./storage/v1/user_data.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# 下载用户数据文件 - file:// 协议
|
|
||||||
name: "user_download"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/v1/user/download"
|
|
||||||
query_params:
|
|
||||||
format: "json"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/octet-stream"
|
|
||||||
Content-Disposition: "attachment; filename=user_data.json"
|
|
||||||
body: "file://./storage/v1/user_data.json"
|
|
||||||
18
mocks/v1/user/echo.json
Normal file
18
mocks/v1/user/echo.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "user_echo",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/user/echo",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
},
|
||||||
|
"body": "Hello V1 Mock Server"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
},
|
||||||
|
"body": "Echo from V1: Hello V1 Mock Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# 文本回显 - Text 格式
|
|
||||||
name: "user_echo"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/user/echo"
|
|
||||||
headers:
|
|
||||||
Content-Type: "text/plain"
|
|
||||||
body: "Hello V1 Mock Server"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "text/plain"
|
|
||||||
body: "Echo from V1: Hello V1 Mock Server"
|
|
||||||
21
mocks/v1/user/login_form.json
Normal file
21
mocks/v1/user/login_form.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "_user_login_form",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/user/login/form",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"username": "formuser",
|
||||||
|
"password": "formpass"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"表单登录成功\",\"data\":{\"token\":\"v2_form_token_xyz\",\"userId\":20001,\"username\":\"formuser\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 表单登录 - Form 格式
|
|
||||||
name: "_user_login_form"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/v1/user/login/form"
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/x-www-form-urlencoded"
|
|
||||||
body:
|
|
||||||
username: "formuser"
|
|
||||||
password: "formpass"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "表单登录成功",
|
|
||||||
"data": {
|
|
||||||
"token": "v2_form_token_xyz",
|
|
||||||
"userId": 20001,
|
|
||||||
"username": "formuser"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
mocks/v1/user/profile.json
Normal file
17
mocks/v1/user/profile.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "user_profile",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/user/profile",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer v1_test_token"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"code\":0,\"message\":\"获取成功\",\"data\":{\"userId\":10001,\"username\":\"admin\",\"email\":\"admin@example.com\",\"nickname\":\"管理员\",\"avatar\":\"https://example.com/avatars/admin.jpg\",\"createdAt\":\"2025-01-01T00:00:00Z\"}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 获取用户信息 - GET 无 Body,需要 Authorization Header
|
|
||||||
name: "user_profile"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/v1/user/profile"
|
|
||||||
headers:
|
|
||||||
Authorization: "Bearer v1_test_token"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: |
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "获取成功",
|
|
||||||
"data": {
|
|
||||||
"userId": 10001,
|
|
||||||
"username": "admin",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"nickname": "管理员",
|
|
||||||
"avatar": "https://example.com/avatars/admin.jpg",
|
|
||||||
"createdAt": "2025-01-01T00:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, RwLock}; // 必须引入 RwLock
|
use std::sync::{Arc, RwLock};
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::models::Payload;
|
use crate::models::Payload;
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ pub mod models;
|
|||||||
pub mod loader;
|
pub mod loader;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
|
pub mod manager;
|
||||||
|
|
||||||
|
#[cfg(feature = "mcp")]
|
||||||
|
pub mod mcp;
|
||||||
@@ -1,25 +1,29 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use walkdir::WalkDir; // 需在 Cargo.toml 添加 walkdir 依赖
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::models::{MockRule, MockSource};
|
use crate::models::MockRule;
|
||||||
|
|
||||||
pub struct MockLoader;
|
pub struct MockLoader;
|
||||||
|
|
||||||
impl MockLoader {
|
impl MockLoader {
|
||||||
/// 递归扫描指定目录并构建索引表
|
/// 递归扫描指定目录并构建索引表
|
||||||
|
/// 目录结构:mocks/{group}/{rule}.json
|
||||||
pub fn load_all_from_dir(dir: &Path) -> HashMap<String, Vec<MockRule>> {
|
pub fn load_all_from_dir(dir: &Path) -> HashMap<String, Vec<MockRule>> {
|
||||||
let mut index: HashMap<String, Vec<MockRule>> = HashMap::new();
|
let mut index: HashMap<String, Vec<MockRule>> = HashMap::new();
|
||||||
|
|
||||||
// 1. 使用 walkdir 递归遍历目录,不限层级
|
if !dir.exists() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 使用 walkdir 递归遍历目录,寻找 JSON 文件
|
||||||
for entry in WalkDir::new(dir)
|
for entry in WalkDir::new(dir)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| e.path().extension().map_or(false, |ext| ext == "yaml" || ext == "yml"))
|
.filter(|e| e.path().extension().map_or(false, |ext| ext == "json"))
|
||||||
{
|
{
|
||||||
if let Some(rules) = Self::parse_yaml_file(entry.path()) {
|
if let Some(rule) = Self::parse_json_file(entry.path()) {
|
||||||
for rule in rules {
|
|
||||||
// 2. 提取路径首段作为索引 Key
|
// 2. 提取路径首段作为索引 Key
|
||||||
let key = Self::extract_first_segment(&rule.request.path);
|
let key = Self::extract_first_segment(&rule.request.path);
|
||||||
|
|
||||||
@@ -27,21 +31,25 @@ impl MockLoader {
|
|||||||
index.entry(key).or_insert_with(Vec::new).push(rule);
|
index.entry(key).or_insert_with(Vec::new).push(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
println!("Successfully loaded {} segments from {:?}", index.len(), dir);
|
println!("Successfully loaded {} segments from {:?}", index.len(), dir);
|
||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析单个 YAML 文件,支持单接口和多接口模式
|
/// 解析单个 JSON 文件
|
||||||
fn parse_yaml_file(path: &Path) -> Option<Vec<MockRule>> {
|
fn parse_json_file(path: &Path) -> Option<MockRule> {
|
||||||
let content = fs::read_to_string(path).ok()?;
|
match fs::read_to_string(path) {
|
||||||
|
Ok(content) => {
|
||||||
// 利用 serde_yaml 的反序列化能力处理 MockSource 枚举
|
match serde_json::from_str::<MockRule>(&content) {
|
||||||
match serde_yaml::from_str::<MockSource>(&content) {
|
Ok(rule) => Some(rule),
|
||||||
Ok(source) => Some(source.flatten()), // 统一打平为 Vec<MockRule>
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to parse YAML at {:?}: {}", path, e);
|
eprintln!("Failed to parse JSON at {:?}: {}", path, e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to read file {:?}: {}", path, e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/main.rs
120
src/main.rs
@@ -1,47 +1,136 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::{routing::any, Router};
|
use axum::{routing::any, Router};
|
||||||
use notify_debouncer_mini::{new_debouncer, notify::*};
|
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
|
||||||
|
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
use mock_server::router::MockRouter;
|
use mock_server::router::MockRouter;
|
||||||
use mock_server::handler::{mock_handler, AppState};
|
use mock_server::handler::{mock_handler, AppState};
|
||||||
|
|
||||||
|
fn print_usage() {
|
||||||
|
println!("Mock Server - A mock API server with hot-reload support");
|
||||||
|
println!();
|
||||||
|
println!("Usage: mock_server [OPTIONS]");
|
||||||
|
println!();
|
||||||
|
println!("Options:");
|
||||||
|
println!(" --mcp Run as MCP server (stdio transport)");
|
||||||
|
println!(" --mocks <DIR> Mocks directory path (default: ./mocks)");
|
||||||
|
println!(" --port <PORT> Server port (default: 8080)");
|
||||||
|
println!(" --help Show this help message");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let mocks_dir = Path::new("./mocks");
|
// 解析命令行参数
|
||||||
if !mocks_dir.exists() {
|
let args: Vec<String> = std::env::args().collect();
|
||||||
std::fs::create_dir_all(mocks_dir).unwrap();
|
let mut mocks_dir = PathBuf::from("./mocks");
|
||||||
|
let mut port: u16 = 8080;
|
||||||
|
|
||||||
|
#[cfg(feature = "mcp")]
|
||||||
|
let mut mcp_mode = false;
|
||||||
|
|
||||||
|
let mut i = 1;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--help" | "-h" => {
|
||||||
|
print_usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
"--mcp" => {
|
||||||
|
#[cfg(feature = "mcp")]
|
||||||
|
{
|
||||||
|
mcp_mode = true;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "mcp"))]
|
||||||
|
{
|
||||||
|
eprintln!("MCP feature not enabled. Rebuild with --features mcp");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--mocks" => {
|
||||||
|
if i + 1 < args.len() {
|
||||||
|
mocks_dir = PathBuf::from(&args[i + 1]);
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
eprintln!("--mocks requires a directory path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--port" => {
|
||||||
|
if i + 1 < args.len() {
|
||||||
|
match args[i + 1].parse::<u16>() {
|
||||||
|
Ok(p) => port = p,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Invalid port number: {}", args[i + 1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
eprintln!("--port requires a port number");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("Unknown option: {}", args[i]);
|
||||||
|
print_usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 初始加载
|
if !mocks_dir.exists() {
|
||||||
|
std::fs::create_dir_all(&mocks_dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mcp")]
|
||||||
|
if mcp_mode {
|
||||||
|
run_mcp_server(mocks_dir).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run_http_server(mocks_dir, port).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mcp")]
|
||||||
|
async fn run_mcp_server(mocks_dir: PathBuf) {
|
||||||
|
println!("Starting MCP server...");
|
||||||
|
let manager = Arc::new(mock_server::manager::MockManager::new(mocks_dir));
|
||||||
|
println!("Loaded {} groups", manager.list_groups().len());
|
||||||
|
|
||||||
|
match mock_server::mcp::run_mcp_server(manager).await {
|
||||||
|
Ok(_) => println!("MCP server stopped"),
|
||||||
|
Err(e) => eprintln!("MCP server error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_http_server(mocks_dir: PathBuf, port: u16) {
|
||||||
println!("Scanning mocks directory...");
|
println!("Scanning mocks directory...");
|
||||||
let index = MockLoader::load_all_from_dir(mocks_dir);
|
let index = MockLoader::load_all_from_dir(&mocks_dir);
|
||||||
let shared_state = Arc::new(AppState {
|
let shared_state = Arc::new(AppState {
|
||||||
router: RwLock::new(MockRouter::new(index)),
|
router: std::sync::RwLock::new(MockRouter::new(index)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 设置热加载监听器
|
// 设置热加载监听器
|
||||||
let state_for_watcher = shared_state.clone();
|
let state_for_watcher = shared_state.clone();
|
||||||
let watch_path = mocks_dir.to_path_buf();
|
let watch_path = mocks_dir.clone();
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
// 200ms 防抖,防止编辑器保存文件时产生多次干扰
|
|
||||||
let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap();
|
let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap();
|
||||||
debouncer.watcher().watch(&watch_path, RecursiveMode::Recursive).unwrap();
|
debouncer.watcher().watch(&watch_path, RecursiveMode::Recursive).unwrap();
|
||||||
|
|
||||||
// 启动异步任务监听文件变动
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok(res) = rx.recv() {
|
while let Ok(res) = rx.recv() {
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("🔄 Detecting changes in mocks/, reloading...");
|
println!("🔄 Detecting changes in mocks/, reloading...");
|
||||||
let new_index = MockLoader::load_all_from_dir(&watch_path);
|
let new_index = MockLoader::load_all_from_dir(&watch_path);
|
||||||
// 获取写锁 (Write Lock) 更新索引
|
|
||||||
let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock");
|
let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock");
|
||||||
*writer = MockRouter::new(new_index);
|
*writer = MockRouter::new(new_index);
|
||||||
println!("✅ Mocks reloaded successfully.");
|
println!("✅ Mocks reloaded successfully.");
|
||||||
@@ -51,12 +140,11 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. 配置 Axum 路由
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.fallback(any(mock_handler))
|
.fallback(any(mock_handler))
|
||||||
.with_state(shared_state);
|
.with_state(shared_state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||||
println!("🚀 Server running at http://{}", addr);
|
println!("🚀 Server running at http://{}", addr);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
|||||||
208
src/manager.rs
Normal file
208
src/manager.rs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::models::MockRule;
|
||||||
|
use crate::loader::MockLoader;
|
||||||
|
use crate::router::MockRouter;
|
||||||
|
|
||||||
|
/// Mock 规则管理器
|
||||||
|
/// 提供统一的 CRUD 操作,同时管理内存和文件持久化
|
||||||
|
pub struct MockManager {
|
||||||
|
/// mocks 目录的基础路径
|
||||||
|
base_path: PathBuf,
|
||||||
|
/// 内存中的规则分组:group -> Vec<MockRule>
|
||||||
|
groups: RwLock<HashMap<String, Vec<MockRule>>>,
|
||||||
|
/// 路由索引
|
||||||
|
router: RwLock<MockRouter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockManager {
|
||||||
|
/// 创建新的 MockManager
|
||||||
|
pub fn new(base_path: PathBuf) -> Self {
|
||||||
|
let groups = MockLoader::load_all_from_dir(&base_path);
|
||||||
|
let index = Self::build_index(&groups);
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base_path,
|
||||||
|
groups: RwLock::new(groups),
|
||||||
|
router: RwLock::new(router),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从分组构建路由索引
|
||||||
|
fn build_index(groups: &HashMap<String, Vec<MockRule>>) -> HashMap<String, Vec<MockRule>> {
|
||||||
|
let mut index: HashMap<String, Vec<MockRule>> = HashMap::new();
|
||||||
|
|
||||||
|
for rules in groups.values() {
|
||||||
|
for rule in rules {
|
||||||
|
let key = Self::extract_first_segment(&rule.request.path);
|
||||||
|
index.entry(key).or_default().push(rule.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提取路径首段
|
||||||
|
fn extract_first_segment(path: &str) -> String {
|
||||||
|
path.trim_start_matches('/')
|
||||||
|
.split('/')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("root")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出所有规则(可按分组过滤)
|
||||||
|
pub fn list(&self, group: Option<&str>) -> Vec<(String, MockRule)> {
|
||||||
|
let groups = self.groups.read().unwrap();
|
||||||
|
match group {
|
||||||
|
Some(g) => groups
|
||||||
|
.get(g)
|
||||||
|
.map(|rules| rules.iter().map(|r| (g.to_string(), r.clone())).collect())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
None => groups
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(g, rules)| rules.iter().map(|r| (g.clone(), r.clone())))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取单个规则
|
||||||
|
pub fn get(&self, group: &str, name: &str) -> Option<MockRule> {
|
||||||
|
let groups = self.groups.read().unwrap();
|
||||||
|
groups
|
||||||
|
.get(group)?
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.name == name)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建规则
|
||||||
|
pub fn create(&self, group: &str, rule: MockRule) -> Result<(), String> {
|
||||||
|
// 1. 写入文件
|
||||||
|
let dir = self.base_path.join(group);
|
||||||
|
fs::create_dir_all(&dir).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||||
|
|
||||||
|
let file_path = dir.join(format!("{}.json", rule.name));
|
||||||
|
let content =
|
||||||
|
serde_json::to_string_pretty(&rule).map_err(|e| format!("序列化失败: {}", e))?;
|
||||||
|
fs::write(&file_path, content).map_err(|e| format!("写入文件失败: {}", e))?;
|
||||||
|
|
||||||
|
// 2. 更新内存
|
||||||
|
{
|
||||||
|
let mut groups = self.groups.write().unwrap();
|
||||||
|
groups
|
||||||
|
.entry(group.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(rule.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 重建 router
|
||||||
|
self.rebuild_router();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新规则
|
||||||
|
pub fn update(&self, group: &str, name: &str, rule: MockRule) -> Result<(), String> {
|
||||||
|
// 1. 更新文件
|
||||||
|
let file_path = self.base_path.join(group).join(format!("{}.json", name));
|
||||||
|
|
||||||
|
// 如果 name 变化,需要删除旧文件
|
||||||
|
if name != rule.name {
|
||||||
|
if file_path.exists() {
|
||||||
|
fs::remove_file(&file_path).map_err(|e| format!("删除旧文件失败: {}", e))?;
|
||||||
|
}
|
||||||
|
let new_path = self.base_path.join(group).join(format!("{}.json", rule.name));
|
||||||
|
let content =
|
||||||
|
serde_json::to_string_pretty(&rule).map_err(|e| format!("序列化失败: {}", e))?;
|
||||||
|
fs::write(&new_path, content).map_err(|e| format!("写入文件失败: {}", e))?;
|
||||||
|
} else {
|
||||||
|
let content =
|
||||||
|
serde_json::to_string_pretty(&rule).map_err(|e| format!("序列化失败: {}", e))?;
|
||||||
|
fs::write(&file_path, content).map_err(|e| format!("写入文件失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新内存
|
||||||
|
{
|
||||||
|
let mut groups = self.groups.write().unwrap();
|
||||||
|
if let Some(rules) = groups.get_mut(group) {
|
||||||
|
if let Some(pos) = rules.iter().position(|r| r.name == name) {
|
||||||
|
rules[pos] = rule.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 重建 router
|
||||||
|
self.rebuild_router();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除规则
|
||||||
|
pub fn delete(&self, group: &str, name: &str) -> Result<(), String> {
|
||||||
|
// 1. 删除文件
|
||||||
|
let file_path = self.base_path.join(group).join(format!("{}.json", name));
|
||||||
|
if file_path.exists() {
|
||||||
|
fs::remove_file(&file_path).map_err(|e| format!("删除文件失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新内存
|
||||||
|
{
|
||||||
|
let mut groups = self.groups.write().unwrap();
|
||||||
|
if let Some(rules) = groups.get_mut(group) {
|
||||||
|
rules.retain(|r| r.name != name);
|
||||||
|
if rules.is_empty() {
|
||||||
|
groups.remove(group);
|
||||||
|
// 尝试删除空目录
|
||||||
|
let _ = fs::remove_dir(self.base_path.join(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 重建 router
|
||||||
|
self.rebuild_router();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重载所有规则(从磁盘重新加载)
|
||||||
|
pub fn reload(&self) {
|
||||||
|
let new_groups = MockLoader::load_all_from_dir(&self.base_path);
|
||||||
|
let new_index = Self::build_index(&new_groups);
|
||||||
|
let new_router = MockRouter::new(new_index);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut groups = self.groups.write().unwrap();
|
||||||
|
*groups = new_groups;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut router = self.router.write().unwrap();
|
||||||
|
*router = new_router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重建 router 索引
|
||||||
|
fn rebuild_router(&self) {
|
||||||
|
let groups = self.groups.read().unwrap();
|
||||||
|
let index = Self::build_index(&groups);
|
||||||
|
let new_router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut router = self.router.write().unwrap();
|
||||||
|
*router = new_router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 router 的读锁引用(用于 handler)
|
||||||
|
pub fn get_router(&self) -> &RwLock<MockRouter> {
|
||||||
|
&self.router
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出所有分组
|
||||||
|
pub fn list_groups(&self) -> Vec<String> {
|
||||||
|
let groups = self.groups.read().unwrap();
|
||||||
|
groups.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/mcp/mod.rs
Normal file
3
src/mcp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod server;
|
||||||
|
|
||||||
|
pub use server::run_mcp_server;
|
||||||
159
src/mcp/server.rs
Normal file
159
src/mcp/server.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use rmcp::{
|
||||||
|
handler::server::tool::ToolRouter,
|
||||||
|
model::*,
|
||||||
|
tool, tool_handler, tool_router,
|
||||||
|
ServerHandler, ServiceExt,
|
||||||
|
transport::stdio,
|
||||||
|
};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::manager::MockManager;
|
||||||
|
use crate::models::MockRule;
|
||||||
|
|
||||||
|
/// MCP Server for Mock Server management
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MockMcpServer {
|
||||||
|
manager: Arc<MockManager>,
|
||||||
|
tool_router: ToolRouter<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_router]
|
||||||
|
impl MockMcpServer {
|
||||||
|
pub fn new(manager: Arc<MockManager>) -> Self {
|
||||||
|
Self {
|
||||||
|
manager,
|
||||||
|
tool_router: Self::tool_router(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "List all mock rules, optionally filtered by group")]
|
||||||
|
async fn list_mock_rules(
|
||||||
|
&self,
|
||||||
|
#[schemars(description = "Optional group name to filter by")]
|
||||||
|
group: Option<String>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
let rules = self.manager.list(group.as_deref());
|
||||||
|
let result = serde_json::to_string_pretty(&rules)
|
||||||
|
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(result)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Get a specific mock rule by group and name")]
|
||||||
|
async fn get_mock_rule(
|
||||||
|
&self,
|
||||||
|
#[schemars(description = "Group name (directory name)")]
|
||||||
|
group: String,
|
||||||
|
#[schemars(description = "Rule name")]
|
||||||
|
name: String,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
match self.manager.get(&group, &name) {
|
||||||
|
Some(rule) => {
|
||||||
|
let result = serde_json::to_string_pretty(&rule)
|
||||||
|
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(result)]))
|
||||||
|
}
|
||||||
|
None => Ok(CallToolResult::error(vec![Content::text(
|
||||||
|
format!("Rule not found: {}/{}", group, name),
|
||||||
|
)])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Create a new mock rule")]
|
||||||
|
async fn create_mock_rule(
|
||||||
|
&self,
|
||||||
|
#[schemars(description = "Group name (directory name)")]
|
||||||
|
group: String,
|
||||||
|
#[schemars(description = "Mock rule definition (JSON object)")]
|
||||||
|
rule_json: String,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
let rule: MockRule = serde_json::from_str(&rule_json)
|
||||||
|
.map_err(|e| McpError::invalid_params(format!("Invalid rule JSON: {}", e), None))?;
|
||||||
|
|
||||||
|
match self.manager.create(&group, rule) {
|
||||||
|
Ok(_) => Ok(CallToolResult::success(vec![Content::text(
|
||||||
|
format!("Created rule in group '{}'", group),
|
||||||
|
)])),
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Update an existing mock rule")]
|
||||||
|
async fn update_mock_rule(
|
||||||
|
&self,
|
||||||
|
#[schemars(description = "Group name")]
|
||||||
|
group: String,
|
||||||
|
#[schemars(description = "Current rule name")]
|
||||||
|
name: String,
|
||||||
|
#[schemars(description = "Updated rule definition (JSON object)")]
|
||||||
|
rule_json: String,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
let rule: MockRule = serde_json::from_str(&rule_json)
|
||||||
|
.map_err(|e| McpError::invalid_params(format!("Invalid rule JSON: {}", e), None))?;
|
||||||
|
|
||||||
|
match self.manager.update(&group, &name, rule) {
|
||||||
|
Ok(_) => Ok(CallToolResult::success(vec![Content::text(
|
||||||
|
format!("Updated rule {}/{}", group, name),
|
||||||
|
)])),
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Delete a mock rule")]
|
||||||
|
async fn delete_mock_rule(
|
||||||
|
&self,
|
||||||
|
#[schemars(description = "Group name")]
|
||||||
|
group: String,
|
||||||
|
#[schemars(description = "Rule name")]
|
||||||
|
name: String,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
match self.manager.delete(&group, &name) {
|
||||||
|
Ok(_) => Ok(CallToolResult::success(vec![Content::text(
|
||||||
|
format!("Deleted rule {}/{}", group, name),
|
||||||
|
)])),
|
||||||
|
Err(e) => Ok(CallToolResult::error(vec![Content::text(e)])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Reload all mock rules from disk")]
|
||||||
|
async fn reload_mock_rules(&self) -> Result<CallToolResult, McpError> {
|
||||||
|
self.manager.reload();
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(
|
||||||
|
"Reloaded all rules from disk",
|
||||||
|
)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(description = "List all groups (directories)")]
|
||||||
|
async fn list_groups(&self) -> Result<CallToolResult, McpError> {
|
||||||
|
let groups = self.manager.list_groups();
|
||||||
|
let result = serde_json::to_string_pretty(&groups)
|
||||||
|
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(result)]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_handler]
|
||||||
|
impl ServerHandler for MockMcpServer {
|
||||||
|
fn get_info(&self) -> ServerInfo {
|
||||||
|
ServerInfo {
|
||||||
|
protocol_version: ProtocolVersion::V_2024_11_05,
|
||||||
|
capabilities: ServerCapabilities::builder()
|
||||||
|
.enable_tools()
|
||||||
|
.build(),
|
||||||
|
server_info: Implementation {
|
||||||
|
name: "mock-server".to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
},
|
||||||
|
instructions: Some("Mock Server MCP - Manage mock API rules for development.\n\nUse list_mock_rules to see all rules, create_mock_rule to add new ones, and delete_mock_rule to remove them.".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run MCP server with stdio transport
|
||||||
|
pub async fn run_mcp_server(manager: Arc<MockManager>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let server = MockMcpServer::new(manager);
|
||||||
|
let service = server.serve(stdio()).await?;
|
||||||
|
service.waiting().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -46,26 +46,6 @@ impl Payload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 顶层包装:支持单对象或数组,自动打平
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum MockSource {
|
|
||||||
/// 对应"一个接口一个文件"模式
|
|
||||||
Single(MockRule),
|
|
||||||
/// 对应"一个文件多个接口"模式
|
|
||||||
Multiple(Vec<MockRule>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MockSource {
|
|
||||||
/// 将不同的解析模式统一转化为列表,供 Loader 构建索引
|
|
||||||
pub fn flatten(self) -> Vec<MockRule> {
|
|
||||||
match self {
|
|
||||||
Self::Single(rule) => vec![rule],
|
|
||||||
Self::Multiple(rules) => rules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 核心 Mock 规则定义
|
/// 核心 Mock 规则定义
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
pub struct MockRule {
|
pub struct MockRule {
|
||||||
|
|||||||
@@ -88,45 +88,49 @@ impl MockRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// D. Body 匹配(根据 Payload 类型智能比较)
|
// D. Body 匹配(根据 Payload 类型智能比较)
|
||||||
if let Some(ref yaml_body) = rule.request.body {
|
if let Some(ref rule_body) = rule.request.body {
|
||||||
return self.match_body(yaml_body, payload);
|
return self.match_body(rule_body, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Body 匹配逻辑
|
/// Body 匹配逻辑
|
||||||
fn match_body(&self, yaml_body: &serde_json::Value, payload: &Payload) -> bool {
|
fn match_body(&self, rule_body: &serde_json::Value, payload: &Payload) -> bool {
|
||||||
// 将 YAML body 规范化:如果是字符串,尝试解析为 JSON
|
|
||||||
let normalized_body = normalize_yaml_body(yaml_body);
|
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
Payload::Json(actual) => {
|
Payload::Json(actual) => {
|
||||||
// JSON 对象比较
|
// 如果 rule_body 是字符串,尝试解析为 JSON 后比较
|
||||||
&normalized_body == actual
|
if let Some(rule_str) = rule_body.as_str() {
|
||||||
|
// 尝试将字符串解析为 JSON
|
||||||
|
if let Ok(parsed_rule) = serde_json::from_str::<serde_json::Value>(rule_str) {
|
||||||
|
return &parsed_rule == actual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 直接比较
|
||||||
|
rule_body == actual
|
||||||
}
|
}
|
||||||
Payload::Xml(actual) => {
|
Payload::Xml(actual) => {
|
||||||
// XML 字符串比较(规范化后比较)
|
// XML 字符串比较(规范化后比较)
|
||||||
normalized_body.as_str()
|
rule_body.as_str()
|
||||||
.map(|expected| normalize_xml(expected) == normalize_xml(actual))
|
.map(|expected| normalize_xml(expected) == normalize_xml(actual))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
Payload::Form(actual) => {
|
Payload::Form(actual) => {
|
||||||
// Form 键值对比较(子集匹配)
|
// Form 键值对比较(子集匹配)
|
||||||
compare_form_with_yaml(&normalized_body, actual)
|
compare_form_with_json(rule_body, actual)
|
||||||
}
|
}
|
||||||
Payload::Multipart(actual_data) => {
|
Payload::Multipart(actual_data) => {
|
||||||
// Multipart 匹配:支持键值对或字段名列表
|
// Multipart 匹配:支持键值对或字段名列表
|
||||||
compare_multipart_with_yaml(&normalized_body, actual_data)
|
compare_multipart_with_json(rule_body, actual_data)
|
||||||
}
|
}
|
||||||
Payload::Text(actual) => {
|
Payload::Text(actual) => {
|
||||||
// 字符串比较(去掉首尾空白)
|
// 字符串比较(去掉首尾空白)
|
||||||
normalized_body.as_str()
|
rule_body.as_str()
|
||||||
.map(|expected| expected.trim() == actual.trim())
|
.map(|expected| expected.trim() == actual.trim())
|
||||||
.unwrap_or_else(|| normalized_body.to_string().trim() == actual.trim())
|
.unwrap_or_else(|| rule_body.to_string().trim() == actual.trim())
|
||||||
}
|
}
|
||||||
Payload::None => {
|
Payload::None => {
|
||||||
false // YAML 配置了 body,但请求没有 body
|
false // 配置了 body,但请求没有 body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,18 +145,6 @@ impl MockRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 规范化 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::<serde_json::Value>(trimmed) {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yaml_body.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 规范化 XML 字符串:去掉声明、多余空白、格式化为紧凑形式
|
/// 规范化 XML 字符串:去掉声明、多余空白、格式化为紧凑形式
|
||||||
fn normalize_xml(xml: &str) -> String {
|
fn normalize_xml(xml: &str) -> String {
|
||||||
let mut result = xml.to_string();
|
let mut result = xml.to_string();
|
||||||
@@ -182,15 +174,15 @@ fn normalize_xml(xml: &str) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Form 比较:YAML 中的键值对必须是请求的子集
|
/// Form 比较:JSON 中的键值对必须是请求的子集
|
||||||
fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
fn compare_form_with_json(rule_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
||||||
let yaml_map = match yaml_body.as_object() {
|
let rule_map = match rule_body.as_object() {
|
||||||
Some(obj) => obj,
|
Some(obj) => obj,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (key, yaml_val) in yaml_map {
|
for (key, rule_val) in rule_map {
|
||||||
let expected = yaml_val.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_val.to_string());
|
let expected = rule_val.as_str().map(|s| s.to_string()).unwrap_or_else(|| rule_val.to_string());
|
||||||
if actual.get(key) != Some(&expected) {
|
if actual.get(key) != Some(&expected) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -199,10 +191,10 @@ fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Multipart 比较:对象和数组形式都只匹配字段名是否存在
|
/// Multipart 比较:对象和数组形式都只匹配字段名是否存在
|
||||||
fn compare_multipart_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
fn compare_multipart_with_json(rule_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
||||||
// 方式 1:对象形式 - 只匹配键名是否存在(忽略值)
|
// 方式 1:对象形式 - 只匹配键名是否存在(忽略值)
|
||||||
if let Some(yaml_map) = yaml_body.as_object() {
|
if let Some(rule_map) = rule_body.as_object() {
|
||||||
for key in yaml_map.keys() {
|
for key in rule_map.keys() {
|
||||||
if !actual.contains_key(key) {
|
if !actual.contains_key(key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -211,9 +203,9 @@ fn compare_multipart_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 方式 2:数组形式 - 只匹配字段名是否存在
|
// 方式 2:数组形式 - 只匹配字段名是否存在
|
||||||
if let Some(yaml_array) = yaml_body.as_array() {
|
if let Some(rule_array) = rule_body.as_array() {
|
||||||
for yaml_field in yaml_array {
|
for rule_field in rule_array {
|
||||||
let field_name = yaml_field.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_field.to_string());
|
let field_name = rule_field.as_str().map(|s| s.to_string()).unwrap_or_else(|| rule_field.to_string());
|
||||||
if !actual.contains_key(&field_name) {
|
if !actual.contains_key(&field_name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,25 @@
|
|||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use mock_server::models::MockSource;
|
use mock_server::models::MockRule;
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_deserialization() {
|
fn test_config_deserialization() {
|
||||||
// 测试 1:验证单接口 YAML 解析
|
// 测试 1:验证 JSON 解析
|
||||||
let single_yaml = r#"
|
let single_json = r#"{
|
||||||
name: "auth_v1"
|
"name": "auth_v1",
|
||||||
request:
|
"request": {
|
||||||
method: "POST"
|
"method": "POST",
|
||||||
path: "/api/v1/login"
|
"path": "/api/v1/login"
|
||||||
response:
|
},
|
||||||
status: 200
|
"response": {
|
||||||
body: "inline_content"
|
"status": 200,
|
||||||
"#;
|
"body": "inline_content"
|
||||||
let res: MockSource = serde_yaml::from_str(single_yaml).expect("应该成功解析单接口");
|
}
|
||||||
assert_eq!(res.flatten().len(), 1);
|
}"#;
|
||||||
|
let res: MockRule = serde_json::from_str(single_json).expect("应该成功解析 JSON");
|
||||||
// 测试 2:验证多接口 YAML 数组解析
|
assert_eq!(res.name, "auth_v1");
|
||||||
let multi_yaml = r#"
|
|
||||||
- name: "api_1"
|
|
||||||
request: { method: "GET", path: "/1" }
|
|
||||||
response: { status: 200, body: "b1" }
|
|
||||||
- name: "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]
|
#[test]
|
||||||
@@ -42,34 +32,39 @@ fn test_recursive_loading_logic() {
|
|||||||
let user_dir = root_path.join("v1/user");
|
let user_dir = root_path.join("v1/user");
|
||||||
fs::create_dir_all(&user_dir).unwrap();
|
fs::create_dir_all(&user_dir).unwrap();
|
||||||
|
|
||||||
// 在深层目录创建单接口文件
|
// 在深层目录创建单接口 JSON 文件
|
||||||
let mut file1 = File::create(user_dir.join("get_profile.yaml")).unwrap();
|
let mut file1 = File::create(user_dir.join("get_profile.json")).unwrap();
|
||||||
writeln!(
|
writeln!(
|
||||||
file1,
|
file1,
|
||||||
r#"
|
r#"{{
|
||||||
name: "user_profile"
|
"name": "user_profile",
|
||||||
request:
|
"request": {{
|
||||||
method: "GET"
|
"method": "GET",
|
||||||
path: "/api/v1/user/profile"
|
"path": "/api/v1/user/profile"
|
||||||
response:
|
}},
|
||||||
status: 200
|
"response": {{
|
||||||
body: "profile_data"
|
"status": 200,
|
||||||
"#
|
"body": "profile_data"
|
||||||
|
}}
|
||||||
|
}}"#
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// 在根目录创建多接口文件
|
// 在根目录创建另一个 JSON 文件
|
||||||
let mut file2 = File::create(root_path.join("system.yaml")).unwrap();
|
let mut file2 = File::create(root_path.join("health.json")).unwrap();
|
||||||
writeln!(
|
writeln!(
|
||||||
file2,
|
file2,
|
||||||
r#"
|
r#"{{
|
||||||
- name: "sys_health"
|
"name": "sys_health",
|
||||||
request: {{ method: "GET", path: "/health" }}
|
"request": {{
|
||||||
response: {{ status: 200, body: "ok" }}
|
"method": "GET",
|
||||||
- name: "sys_version"
|
"path": "/health"
|
||||||
request: {{ method: "GET", path: "/version" }}
|
}},
|
||||||
response: {{ status: 200, body: "1.0.0" }}
|
"response": {{
|
||||||
"#
|
"status": 200,
|
||||||
|
"body": "ok"
|
||||||
|
}}
|
||||||
|
}}"#
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -80,11 +75,10 @@ fn test_recursive_loading_logic() {
|
|||||||
// 1. 检查 Key 是否根据路径首段正确提取
|
// 1. 检查 Key 是否根据路径首段正确提取
|
||||||
assert!(index.contains_key("api"), "索引应包含 'api' 键");
|
assert!(index.contains_key("api"), "索引应包含 'api' 键");
|
||||||
assert!(index.contains_key("health"), "索引应包含 'health' 键");
|
assert!(index.contains_key("health"), "索引应包含 'health' 键");
|
||||||
assert!(index.contains_key("version"), "索引应包含 'version' 键");
|
|
||||||
|
|
||||||
// 2. 检查规则总数
|
// 2. 检查规则总数
|
||||||
let total_rules: usize = index.values().map(|v| v.len()).sum();
|
let total_rules: usize = index.values().map(|v| v.len()).sum();
|
||||||
assert_eq!(total_rules, 3, "总规则数应为 3");
|
assert_eq!(total_rules, 2, "总规则数应为 2");
|
||||||
|
|
||||||
// 3. 检查深层文件是否被正确读取
|
// 3. 检查深层文件是否被正确读取
|
||||||
let api_rules = &index["api"];
|
let api_rules = &index["api"];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::fs::{self, File};
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use mock_server::models::{MockRule, MockSource, Payload};
|
use mock_server::models::{MockRule, Payload};
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
use mock_server::router::MockRouter;
|
use mock_server::router::MockRouter;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -11,42 +11,27 @@ use std::collections::HashMap;
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_config_parsing_scenarios() {
|
fn test_config_parsing_scenarios() {
|
||||||
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
||||||
let yaml_single = r#"
|
let json_single = r#"{
|
||||||
name: "auth_v1"
|
"name": "auth_v1",
|
||||||
request:
|
"request": {
|
||||||
method: "POST"
|
"method": "POST",
|
||||||
path: "/api/v1/login"
|
"path": "/api/v1/login",
|
||||||
body: { "user": "admin" }
|
"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 rules = source_s.flatten();
|
let rule: MockRule = serde_json::from_str(json_single).expect("解析单接口失败");
|
||||||
assert_eq!(rules.len(), 1);
|
assert!(rule.request.body.is_some());
|
||||||
// 验证 body 是否被成功解析为 Value::Object
|
assert_eq!(rule.request.body.as_ref().unwrap()["user"], "admin");
|
||||||
assert!(rules[0].request.body.is_some());
|
|
||||||
assert_eq!(rules[0].request.body.as_ref().unwrap()["user"], "admin");
|
|
||||||
|
|
||||||
// 场景 B: 验证多接口配置
|
// 场景 B: 验证 Smart Body 的 file:// 协议字符串解析
|
||||||
let yaml_multi = r#"
|
let json_file = r#"{
|
||||||
- name: "api_1"
|
"name": "export_api",
|
||||||
request: { method: "GET", path: "/health" }
|
"request": { "method": "GET", "path": "/download" },
|
||||||
response: { status: 200, body: "ok" }
|
"response": { "status": 200, "body": "file://./storage/data.zip" }
|
||||||
- name: "api_2"
|
}"#;
|
||||||
request: { method: "GET", path: "/version" }
|
let rule_file: MockRule = serde_json::from_str(json_file).unwrap();
|
||||||
response: { status: 200, body: "1.0" }
|
assert!(rule_file.response.body.starts_with("file://"));
|
||||||
"#;
|
|
||||||
let source_m: MockSource = serde_yaml::from_str(yaml_multi).expect("解析多接口失败");
|
|
||||||
assert_eq!(source_m.flatten().len(), 2);
|
|
||||||
|
|
||||||
// 场景 C: 验证 Smart Body 的 file:// 协议字符串解析
|
|
||||||
let yaml_file = r#"
|
|
||||||
name: "export_api"
|
|
||||||
request: { method: "GET", path: "/download" }
|
|
||||||
response: { status: 200, body: "file://./storage/data.zip" }
|
|
||||||
"#;
|
|
||||||
let source_f: MockSource = serde_yaml::from_str(yaml_file).unwrap();
|
|
||||||
let rule = &source_f.flatten()[0];
|
|
||||||
assert!(rule.response.body.starts_with("file://"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 模块二:验证 Loader 递归扫描与索引构建
|
/// 模块二:验证 Loader 递归扫描与索引构建
|
||||||
@@ -58,11 +43,11 @@ fn test_loader_recursive_indexing() {
|
|||||||
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();
|
||||||
|
|
||||||
let mut f1 = File::create(auth_path.join("single_login.yaml")).unwrap();
|
let mut f1 = File::create(auth_path.join("login.json")).unwrap();
|
||||||
writeln!(f1, "name: 'l1'\nrequest: {{ method: 'POST', path: '/api/v1/login' }}\nresponse: {{ status: 200, body: 'ok' }}").unwrap();
|
writeln!(f1, r#"{{"name":"l1","request":{{"method":"POST","path":"/api/v1/login"}},"response":{{"status":200,"body":"ok"}}}}"#).unwrap();
|
||||||
|
|
||||||
let mut f2 = File::create(root_path.join("sys.yaml")).unwrap();
|
let mut f2 = File::create(root_path.join("health.json")).unwrap();
|
||||||
writeln!(f2, "- name: 's1'\n request: {{ method: 'GET', path: '/health' }}\n response: {{ status: 200, body: 'up' }}").unwrap();
|
writeln!(f2, r#"{{"name":"s1","request":{{"method":"GET","path":"/health"}},"response":{{"status":200,"body":"up"}}}}"#).unwrap();
|
||||||
|
|
||||||
let index = MockLoader::load_all_from_dir(root_path);
|
let index = MockLoader::load_all_from_dir(root_path);
|
||||||
|
|
||||||
@@ -78,21 +63,19 @@ fn test_router_matching_logic() {
|
|||||||
let mut index = HashMap::new();
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
// 1. 准备带有 Body 的规则
|
// 1. 准备带有 Body 的规则
|
||||||
let rule_auth = serde_yaml::from_str::<MockSource>(
|
let rule_json = r#"{
|
||||||
r#"
|
"name": "auth_v1",
|
||||||
name: "auth_v1"
|
"request": {
|
||||||
request:
|
"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 }
|
||||||
body: { "code": 123 }
|
},
|
||||||
response: { status: 200, body: "token_123" }
|
"response": { "status": 200, "body": "token_123" }
|
||||||
"#,
|
}"#;
|
||||||
)
|
let rule_auth: MockRule = serde_json::from_str(rule_json).unwrap();
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule_auth[0].clone()]);
|
index.insert("api".to_string(), vec![rule_auth.clone()]);
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
// 2. 测试场景 A:JSON 完全匹配
|
// 2. 测试场景 A:JSON 完全匹配
|
||||||
@@ -139,48 +122,39 @@ fn test_payload_type_matching() {
|
|||||||
let mut index = HashMap::new();
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
// XML 规则
|
// XML 规则
|
||||||
let xml_rule = serde_yaml::from_str::<MockSource>(
|
let xml_rule: MockRule = serde_json::from_str(r#"{
|
||||||
r#"
|
"name": "xml_api",
|
||||||
name: "xml_api"
|
"request": {
|
||||||
request:
|
"method": "POST",
|
||||||
method: "POST"
|
"path": "/api/xml",
|
||||||
path: "/api/xml"
|
"body": "<root><name>test</name></root>"
|
||||||
body: "<root><name>test</name></root>"
|
},
|
||||||
response: { status: 200, body: "ok" }
|
"response": { "status": 200, "body": "ok" }
|
||||||
"#,
|
}"#).unwrap();
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
// Form 规则
|
// Form 规则
|
||||||
let form_rule = serde_yaml::from_str::<MockSource>(
|
let form_rule: MockRule = serde_json::from_str(r#"{
|
||||||
r#"
|
"name": "form_api",
|
||||||
name: "form_api"
|
"request": {
|
||||||
request:
|
"method": "POST",
|
||||||
method: "POST"
|
"path": "/api/form",
|
||||||
path: "/api/form"
|
"body": { "username": "admin", "password": "123456" }
|
||||||
body: { "username": "admin", "password": "123456" }
|
},
|
||||||
response: { status: 200, body: "login_ok" }
|
"response": { "status": 200, "body": "login_ok" }
|
||||||
"#,
|
}"#).unwrap();
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
// Text 规则
|
// Text 规则
|
||||||
let text_rule = serde_yaml::from_str::<MockSource>(
|
let text_rule: MockRule = serde_json::from_str(r#"{
|
||||||
r#"
|
"name": "text_api",
|
||||||
name: "text_api"
|
"request": {
|
||||||
request:
|
"method": "POST",
|
||||||
method: "POST"
|
"path": "/api/text",
|
||||||
path: "/api/text"
|
"body": "plain text content"
|
||||||
body: "plain text content"
|
},
|
||||||
response: { status: 200, body: "text_ok" }
|
"response": { "status": 200, "body": "text_ok" }
|
||||||
"#,
|
}"#).unwrap();
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![xml_rule[0].clone(), form_rule[0].clone(), text_rule[0].clone()]);
|
index.insert("api".to_string(), vec![xml_rule, form_rule, text_rule]);
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
// 测试 XML 匹配
|
// 测试 XML 匹配
|
||||||
@@ -252,34 +226,28 @@ fn test_multipart_matching() {
|
|||||||
let mut index = HashMap::new();
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
// 数组形式:只匹配字段名
|
// 数组形式:只匹配字段名
|
||||||
let array_rule = serde_yaml::from_str::<MockSource>(
|
let array_rule: MockRule = serde_json::from_str(r#"{
|
||||||
r#"
|
"name": "upload_array",
|
||||||
name: "upload_array"
|
"request": {
|
||||||
request:
|
"method": "POST",
|
||||||
method: "POST"
|
"path": "/api/upload/array",
|
||||||
path: "/api/upload/array"
|
"body": ["file", "description"]
|
||||||
body: ["file", "description"]
|
},
|
||||||
response: { status: 200, body: "ok" }
|
"response": { "status": 200, "body": "ok" }
|
||||||
"#,
|
}"#).unwrap();
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
// 对象形式:匹配键值对
|
// 对象形式:匹配键名
|
||||||
let object_rule = serde_yaml::from_str::<MockSource>(
|
let object_rule: MockRule = serde_json::from_str(r#"{
|
||||||
r#"
|
"name": "upload_object",
|
||||||
name: "upload_object"
|
"request": {
|
||||||
request:
|
"method": "POST",
|
||||||
method: "POST"
|
"path": "/api/upload/object",
|
||||||
path: "/api/upload/object"
|
"body": { "username": "admin", "role": "user" }
|
||||||
body: { "username": "admin", "role": "user" }
|
},
|
||||||
response: { status: 200, body: "ok" }
|
"response": { "status": 200, "body": "ok" }
|
||||||
"#,
|
}"#).unwrap();
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![array_rule[0].clone(), object_rule[0].clone()]);
|
index.insert("api".to_string(), vec![array_rule, object_rule]);
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
// 测试数组形式:匹配字段名
|
// 测试数组形式:匹配字段名
|
||||||
@@ -299,7 +267,7 @@ response: { status: 200, body: "ok" }
|
|||||||
|
|
||||||
// 测试对象形式:键名匹配(值被忽略)
|
// 测试对象形式:键名匹配(值被忽略)
|
||||||
let mut correct_data = HashMap::new();
|
let mut correct_data = HashMap::new();
|
||||||
correct_data.insert("username".to_string(), "any_value".to_string()); // 值不重要
|
correct_data.insert("username".to_string(), "any_value".to_string());
|
||||||
correct_data.insert("role".to_string(), "any_role".to_string());
|
correct_data.insert("role".to_string(), "any_role".to_string());
|
||||||
let correct_payload = Payload::Multipart(correct_data);
|
let correct_payload = Payload::Multipart(correct_data);
|
||||||
let object_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &correct_payload);
|
let object_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &correct_payload);
|
||||||
@@ -308,118 +276,43 @@ response: { status: 200, body: "ok" }
|
|||||||
// 测试对象形式:键名不匹配
|
// 测试对象形式:键名不匹配
|
||||||
let mut wrong_data = HashMap::new();
|
let mut wrong_data = HashMap::new();
|
||||||
wrong_data.insert("username".to_string(), "admin".to_string());
|
wrong_data.insert("username".to_string(), "admin".to_string());
|
||||||
wrong_data.insert("other_field".to_string(), "value".to_string()); // 缺少 role 字段
|
wrong_data.insert("other_field".to_string(), "value".to_string());
|
||||||
let wrong_payload = Payload::Multipart(wrong_data);
|
let wrong_payload = Payload::Multipart(wrong_data);
|
||||||
let wrong_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &wrong_payload);
|
let wrong_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &wrong_payload);
|
||||||
assert!(wrong_matched.is_none(), "缺少键名不应该成功");
|
assert!(wrong_matched.is_none(), "缺少键名不应该成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 模块七:验证 YAML 块语法(折叠 `>` 和字面量 `|`)
|
/// 模块七:验证 XML 格式化匹配
|
||||||
#[test]
|
|
||||||
fn test_yaml_block_syntax() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
// 使用折叠语法 `>` 的规则
|
|
||||||
let folded_rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
name: "folded_api"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/folded"
|
|
||||||
body: >
|
|
||||||
{"username":"admin","password":"123456"}
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "ok"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
// 使用字面量语法 `|` 的规则
|
|
||||||
let literal_rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
name: "literal_api"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/literal"
|
|
||||||
body: |
|
|
||||||
{"username":"test","password":"abcdef"}
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "ok"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![folded_rule[0].clone(), literal_rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试折叠语法:YAML 中 `>` 会把内容合并成一行,但仍是字符串
|
|
||||||
// 程序应该能将字符串解析为 JSON 后匹配
|
|
||||||
let folded_payload = Payload::Json(json!({"username":"admin","password":"123456"}));
|
|
||||||
let folded_matched = router.match_rule("POST", "/api/folded", &HashMap::new(), &HashMap::new(), &folded_payload);
|
|
||||||
assert!(folded_matched.is_some(), "折叠语法应该匹配 JSON 请求");
|
|
||||||
assert_eq!(folded_matched.unwrap().name, "folded_api");
|
|
||||||
|
|
||||||
// 测试字面量语法:YAML 中 `|` 保留换行
|
|
||||||
let literal_payload = Payload::Json(json!({"username":"test","password":"abcdef"}));
|
|
||||||
let literal_matched = router.match_rule("POST", "/api/literal", &HashMap::new(), &HashMap::new(), &literal_payload);
|
|
||||||
assert!(literal_matched.is_some(), "字面量语法应该匹配 JSON 请求");
|
|
||||||
assert_eq!(literal_matched.unwrap().name, "literal_api");
|
|
||||||
|
|
||||||
// 测试不匹配的情况
|
|
||||||
let wrong_payload = Payload::Json(json!({"username":"wrong","password":"wrong"}));
|
|
||||||
let wrong_matched = router.match_rule("POST", "/api/folded", &HashMap::new(), &HashMap::new(), &wrong_payload);
|
|
||||||
assert!(wrong_matched.is_none(), "错误的 body 不应该匹配");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 模块八:验证 XML 格式化匹配
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_xml_normalized_matching() {
|
fn test_xml_normalized_matching() {
|
||||||
let mut index = HashMap::new();
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
// YAML 中的 XML(带格式化)
|
// JSON 中的 XML(紧凑格式)
|
||||||
let xml_rule = serde_yaml::from_str::<MockSource>(
|
let xml_rule: MockRule = serde_json::from_str(r#"{
|
||||||
r#"
|
"name": "xml_api",
|
||||||
name: "xml_api"
|
"request": {
|
||||||
request:
|
"method": "POST",
|
||||||
method: "POST"
|
"path": "/api/xml",
|
||||||
path: "/api/xml"
|
"body": "<user><id>1001</id><name>张三</name></user>"
|
||||||
body: |
|
},
|
||||||
<user>
|
"response": { "status": 200, "body": "ok" }
|
||||||
<id>1001</id>
|
}"#).unwrap();
|
||||||
<name>张三</name>
|
|
||||||
</user>
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "ok"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![xml_rule[0].clone()]);
|
index.insert("api".to_string(), vec![xml_rule]);
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
// 测试 1:完全相同的格式化 XML
|
// 测试 1:紧凑格式的 XML
|
||||||
let formatted_xml = Payload::Xml("<user>\n <id>1001</id>\n <name>张三</name>\n</user>".to_string());
|
|
||||||
let matched1 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &formatted_xml);
|
|
||||||
assert!(matched1.is_some(), "格式化 XML 应该匹配");
|
|
||||||
|
|
||||||
// 测试 2:紧凑格式的 XML
|
|
||||||
let compact_xml = Payload::Xml("<user><id>1001</id><name>张三</name></user>".to_string());
|
let compact_xml = Payload::Xml("<user><id>1001</id><name>张三</name></user>".to_string());
|
||||||
let matched2 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &compact_xml);
|
let matched1 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &compact_xml);
|
||||||
assert!(matched2.is_some(), "紧凑格式 XML 应该匹配");
|
assert!(matched1.is_some(), "紧凑格式 XML 应该匹配");
|
||||||
|
|
||||||
// 测试 3:带 XML 声明的请求
|
// 测试 2:带 XML 声明的请求
|
||||||
let xml_with_decl = Payload::Xml("<?xml version=\"1.0\" encoding=\"UTF-8\"?><user><id>1001</id><name>张三</name></user>".to_string());
|
let xml_with_decl = Payload::Xml("<?xml version=\"1.0\" encoding=\"UTF-8\"?><user><id>1001</id><name>张三</name></user>".to_string());
|
||||||
let matched3 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_with_decl);
|
let matched2 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_with_decl);
|
||||||
assert!(matched3.is_some(), "带声明的 XML 应该匹配");
|
assert!(matched2.is_some(), "带声明的 XML 应该匹配");
|
||||||
|
|
||||||
// 测试 4:内容不同,不应该匹配
|
// 测试 3:内容不同,不应该匹配
|
||||||
let wrong_xml = Payload::Xml("<user><id>1002</id><name>李四</name></user>".to_string());
|
let wrong_xml = Payload::Xml("<user><id>1002</id><name>李四</name></user>".to_string());
|
||||||
let matched4 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &wrong_xml);
|
let matched3 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &wrong_xml);
|
||||||
assert!(matched4.is_none(), "内容不同的 XML 不应该匹配");
|
assert!(matched3.is_none(), "内容不同的 XML 不应该匹配");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,37 +6,39 @@ use mock_server::loader::MockLoader;
|
|||||||
use mock_server::router::MockRouter;
|
use mock_server::router::MockRouter;
|
||||||
|
|
||||||
/// 加载 v1 目录的 mock 规则
|
/// 加载 v1 目录的 mock 规则
|
||||||
fn load_v2_mocks() -> HashMap<String, Vec<mock_server::models::MockRule>> {
|
fn load_v1_mocks() -> HashMap<String, Vec<mock_server::models::MockRule>> {
|
||||||
let v2_path = Path::new("../mocks/v1");
|
let v1_path = Path::new("./mocks/v1");
|
||||||
MockLoader::load_all_from_dir(v2_path)
|
MockLoader::load_all_from_dir(v1_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块一:验证所有 YAML 文件正确加载 ==========
|
// ========== 模块一:验证所有 JSON 文件正确加载 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_load_all_mocks() {
|
fn test_v1_load_all_mocks() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
|
|
||||||
// 验证索引键存在
|
// 验证索引键存在
|
||||||
assert!(index.contains_key("v1"), "应包含 'v2' 索引键");
|
assert!(index.contains_key("v1"), "应包含 'v1' 索引键");
|
||||||
|
|
||||||
// 验证规则总数
|
// 验证规则总数
|
||||||
let total: usize = index.values().map(|v| v.len()).sum();
|
let total: usize = index.values().map(|v| v.len()).sum();
|
||||||
assert_eq!(total, 9, "v2 目录应有 9 个 mock 规则");
|
assert!(total >= 10, "v1 目录应有至少 10 个 mock 规则");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块二:JSON Payload 测试 ==========
|
// ========== 模块二:JSON Payload 测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_json_login() {
|
fn test_v1_json_login() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
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());
|
||||||
|
headers.insert("Authorization".to_string(), "eyJhbGciOiJIUzI1NiIsInR5cCI6".to_string());
|
||||||
|
headers.insert("host".to_string(), "127.0.0.1:8080".to_string());
|
||||||
|
|
||||||
let payload = Payload::Json(json!({
|
let payload = Payload::Json(json!({
|
||||||
"username": "admin",
|
"username": "user001",
|
||||||
"password": "password123"
|
"password": "password123"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -44,13 +46,13 @@ fn test_v2_json_login() {
|
|||||||
|
|
||||||
assert!(matched.is_some(), "JSON 登录应匹配成功");
|
assert!(matched.is_some(), "JSON 登录应匹配成功");
|
||||||
let rule = matched.unwrap();
|
let rule = matched.unwrap();
|
||||||
assert_eq!(rule.name, "v2_user_login");
|
assert_eq!(rule.name, "user_login_001");
|
||||||
assert_eq!(rule.response.status, 200);
|
assert_eq!(rule.response.status, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_json_register() {
|
fn test_v1_json_register() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
@@ -66,15 +68,15 @@ fn test_v2_json_register() {
|
|||||||
|
|
||||||
assert!(matched.is_some(), "JSON 注册应匹配成功");
|
assert!(matched.is_some(), "JSON 注册应匹配成功");
|
||||||
let rule = matched.unwrap();
|
let rule = matched.unwrap();
|
||||||
assert_eq!(rule.name, "v2_user_register");
|
assert_eq!(rule.name, "user_register");
|
||||||
assert_eq!(rule.response.status, 201);
|
assert_eq!(rule.response.status, 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块三:Form Payload 测试 ==========
|
// ========== 模块三:Form Payload 测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_form_login() {
|
fn test_v1_form_login() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
@@ -88,74 +90,74 @@ fn test_v2_form_login() {
|
|||||||
let matched = router.match_rule("POST", "/v1/user/login/form", &HashMap::new(), &headers, &payload);
|
let matched = router.match_rule("POST", "/v1/user/login/form", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
assert!(matched.is_some(), "Form 登录应匹配成功");
|
assert!(matched.is_some(), "Form 登录应匹配成功");
|
||||||
assert_eq!(matched.unwrap().name, "v2_user_login_form");
|
assert_eq!(matched.unwrap().name, "_user_login_form");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块四:Text Payload 测试 ==========
|
// ========== 模块四:Text Payload 测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_text_echo() {
|
fn test_v1_text_echo() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("Content-Type".to_string(), "text/plain".to_string());
|
headers.insert("Content-Type".to_string(), "text/plain".to_string());
|
||||||
|
|
||||||
let payload = Payload::Text("Hello V2 Mock Server".to_string());
|
let payload = Payload::Text("Hello V1 Mock Server".to_string());
|
||||||
|
|
||||||
let matched = router.match_rule("POST", "/v1/user/echo", &HashMap::new(), &headers, &payload);
|
let matched = router.match_rule("POST", "/v1/user/echo", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
assert!(matched.is_some(), "Text 回显应匹配成功");
|
assert!(matched.is_some(), "Text 回显应匹配成功");
|
||||||
let rule = matched.unwrap();
|
let rule = matched.unwrap();
|
||||||
assert_eq!(rule.name, "v2_user_echo");
|
assert_eq!(rule.name, "user_echo");
|
||||||
assert!(rule.response.body.contains("Echo from V2"));
|
assert!(rule.response.body.contains("Echo from V1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块五:XML Payload 测试 ==========
|
// ========== 模块五:XML Payload 测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_xml_export() {
|
fn test_v1_xml_export() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("Content-Type".to_string(), "application/xml".to_string());
|
headers.insert("Content-Type".to_string(), "application/xml".to_string());
|
||||||
|
|
||||||
let xml_body = r#"<request><userId>10001</userId><format>xml</format></request>"#;
|
let xml_body = r#"<?xml version="1.0" encoding="UTF-8"?><request><userId>10001</userId><format>xml</format></request>"#;
|
||||||
let payload = Payload::Xml(xml_body.to_string());
|
let payload = Payload::Xml(xml_body.to_string());
|
||||||
|
|
||||||
let matched = router.match_rule("POST", "/v1/data/export", &HashMap::new(), &headers, &payload);
|
let matched = router.match_rule("POST", "/v1/data/export", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
assert!(matched.is_some(), "XML 导出应匹配成功");
|
assert!(matched.is_some(), "XML 导出应匹配成功");
|
||||||
assert_eq!(matched.unwrap().name, "v2_data_export");
|
assert_eq!(matched.unwrap().name, "data_export");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块六:Multipart Payload 测试 ==========
|
// ========== 模块六:Multipart Payload 测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_multipart_upload() {
|
fn test_v1_multipart_upload() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("Content-Type".to_string(), "multipart/form-data".to_string());
|
headers.insert("Content-Type".to_string(), "multipart/form-data".to_string());
|
||||||
|
|
||||||
let mut multipart_data = HashMap::new();
|
let mut multipart_data = HashMap::new();
|
||||||
multipart_data.insert("avatar".to_string(), "file_content".to_string());
|
multipart_data.insert("avatar1".to_string(), "file_content".to_string());
|
||||||
multipart_data.insert("description".to_string(), "user avatar".to_string());
|
multipart_data.insert("description1".to_string(), "user avatar".to_string());
|
||||||
let payload = Payload::Multipart(multipart_data);
|
let payload = Payload::Multipart(multipart_data);
|
||||||
|
|
||||||
let matched = router.match_rule("POST", "/v1/user/avatar", &HashMap::new(), &headers, &payload);
|
let matched = router.match_rule("POST", "/v1/user/avatar", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
assert!(matched.is_some(), "Multipart 上传应匹配成功");
|
assert!(matched.is_some(), "Multipart 上传应匹配成功");
|
||||||
assert_eq!(matched.unwrap().name, "v2_user_upload_avatar");
|
assert_eq!(matched.unwrap().name, "user_upload_avatar_001");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块七:None Payload 测试 (GET 无 Body) ==========
|
// ========== 模块七:None Payload 测试 (GET 无 Body) ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_health_check() {
|
fn test_v1_health_check() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let payload = Payload::None;
|
let payload = Payload::None;
|
||||||
@@ -164,31 +166,31 @@ fn test_v2_health_check() {
|
|||||||
|
|
||||||
assert!(matched.is_some(), "健康检查应匹配成功");
|
assert!(matched.is_some(), "健康检查应匹配成功");
|
||||||
let rule = matched.unwrap();
|
let rule = matched.unwrap();
|
||||||
assert_eq!(rule.name, "v2_health_check");
|
assert_eq!(rule.name, "health_check");
|
||||||
assert_eq!(rule.response.status, 200);
|
assert_eq!(rule.response.status, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_get_profile() {
|
fn test_v1_get_profile() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("Authorization".to_string(), "Bearer v2_test_token".to_string());
|
headers.insert("Authorization".to_string(), "Bearer v1_test_token".to_string());
|
||||||
|
|
||||||
let payload = Payload::None;
|
let payload = Payload::None;
|
||||||
|
|
||||||
let matched = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &headers, &payload);
|
let matched = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
assert!(matched.is_some(), "获取用户信息应匹配成功");
|
assert!(matched.is_some(), "获取用户信息应匹配成功");
|
||||||
assert_eq!(matched.unwrap().name, "v2_user_profile");
|
assert_eq!(matched.unwrap().name, "user_profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块八:Query Params 测试 ==========
|
// ========== 模块八:Query Params 测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_query_params() {
|
fn test_v1_query_params() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
let mut query = HashMap::new();
|
let mut query = HashMap::new();
|
||||||
@@ -200,15 +202,15 @@ fn test_v2_query_params() {
|
|||||||
|
|
||||||
assert!(matched.is_some(), "带 query params 的下载应匹配成功");
|
assert!(matched.is_some(), "带 query params 的下载应匹配成功");
|
||||||
let rule = matched.unwrap();
|
let rule = matched.unwrap();
|
||||||
assert_eq!(rule.name, "v2_user_download");
|
assert_eq!(rule.name, "user_download");
|
||||||
assert!(rule.response.body.starts_with("file://"));
|
assert!(rule.response.body.starts_with("file://"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块九:Header 匹配测试 ==========
|
// ========== 模块九:Header 匹配测试 ==========
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_v2_header_required() {
|
fn test_v1_header_required() {
|
||||||
let index = load_v2_mocks();
|
let index = load_v1_mocks();
|
||||||
let router = MockRouter::new(index);
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
// 无 Authorization header 不应匹配
|
// 无 Authorization header 不应匹配
|
||||||
@@ -218,21 +220,7 @@ fn test_v2_header_required() {
|
|||||||
|
|
||||||
// 有正确的 Authorization header 应匹配
|
// 有正确的 Authorization header 应匹配
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("Authorization".to_string(), "Bearer v2_test_token".to_string());
|
headers.insert("Authorization".to_string(), "Bearer v1_test_token".to_string());
|
||||||
let matched = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &headers, &payload);
|
let matched = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &headers, &payload);
|
||||||
assert!(matched.is_some(), "有 Authorization 应匹配");
|
assert!(matched.is_some(), "有 Authorization 应匹配");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 模块十:name 字段验证 ==========
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_v2_all_rules_have_name() {
|
|
||||||
let index = load_v2_mocks();
|
|
||||||
|
|
||||||
for (key, rules) in &index {
|
|
||||||
for rule in rules {
|
|
||||||
assert!(!rule.name.is_empty(), "规则 name 不能为空");
|
|
||||||
assert!(rule.name.starts_with("v2_"), "v2 规则 name 应以 'v2_' 开头");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user