Compare commits
5 Commits
feature-ms
...
feature-ms
| Author | SHA1 | Date | |
|---|---|---|---|
| 061ceff4b8 | |||
| 9c1d0e16b4 | |||
| b579a835de | |||
| e218ab04fe | |||
| 18e0ccad58 |
@@ -2,7 +2,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(cargo search:*)"
|
"Bash(cargo search:*)",
|
||||||
|
"Bash(cd:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,3 @@
|
|||||||
# 日志文件
|
# 日志文件
|
||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# 上传文件存储目录
|
|
||||||
storage/
|
|
||||||
|
|||||||
507
Cargo.lock
generated
507
Cargo.lock
generated
@@ -2,33 +2,12 @@
|
|||||||
# 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]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.102"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
||||||
|
|
||||||
[[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"
|
||||||
@@ -81,30 +60,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-extra"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
|
|
||||||
dependencies = [
|
|
||||||
"axum",
|
|
||||||
"axum-core",
|
|
||||||
"bytes",
|
|
||||||
"fastrand",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"http-body-util",
|
|
||||||
"mime",
|
|
||||||
"multer",
|
|
||||||
"pin-project-lite",
|
|
||||||
"rustversion",
|
|
||||||
"serde_core",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -117,62 +72,18 @@ 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.57"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
|
||||||
dependencies = [
|
|
||||||
"find-msvc-tools",
|
|
||||||
"shlex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chrono"
|
|
||||||
version = "0.4.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
|
||||||
dependencies = [
|
|
||||||
"iana-time-zone",
|
|
||||||
"js-sys",
|
|
||||||
"num-traits",
|
|
||||||
"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 = "encoding_rs"
|
|
||||||
version = "0.8.35"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -195,18 +106,6 @@ 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]]
|
|
||||||
name = "foldhash"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -285,44 +184,16 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"r-efi 6.0.0",
|
|
||||||
"wasip2",
|
|
||||||
"wasip3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.15.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
|
||||||
dependencies = [
|
|
||||||
"foldhash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heck"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -405,36 +276,6 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iana-time-zone"
|
|
||||||
version = "0.1.65"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
|
||||||
dependencies = [
|
|
||||||
"android_system_properties",
|
|
||||||
"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]]
|
|
||||||
name = "id-arena"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.12.1"
|
version = "2.12.1"
|
||||||
@@ -442,9 +283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown",
|
||||||
"serde",
|
|
||||||
"serde_core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -473,16 +312,6 @@ 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"
|
||||||
@@ -509,12 +338,6 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "leb128fmt"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.178"
|
version = "0.2.178"
|
||||||
@@ -577,8 +400,6 @@ name = "mock_server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
|
||||||
"chrono",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"notify",
|
"notify",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
@@ -590,27 +411,9 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "multer"
|
|
||||||
version = "3.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"encoding_rs",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"httparse",
|
|
||||||
"memchr",
|
|
||||||
"mime",
|
|
||||||
"spin",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "8.2.0"
|
version = "8.2.0"
|
||||||
@@ -656,15 +459,6 @@ 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"
|
||||||
@@ -712,16 +506,6 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "prettyplease"
|
|
||||||
version = "0.2.37"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.103"
|
version = "1.0.103"
|
||||||
@@ -746,12 +530,6 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r-efi"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -774,12 +552,6 @@ 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"
|
||||||
@@ -801,12 +573,6 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -895,12 +661,6 @@ 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"
|
||||||
@@ -932,12 +692,6 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.9.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.111"
|
version = "2.0.111"
|
||||||
@@ -962,7 +716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -1110,42 +864,18 @@ 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 = "unicode-xid"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uuid"
|
|
||||||
version = "1.22.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.4.2",
|
|
||||||
"js-sys",
|
|
||||||
"serde_core",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -1168,95 +898,7 @@ version = "1.0.1+wasi-0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen 0.46.0",
|
"wit-bindgen",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasip3"
|
|
||||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen 0.51.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
|
||||||
name = "wasm-encoder"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
|
||||||
dependencies = [
|
|
||||||
"leb128fmt",
|
|
||||||
"wasmparser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-metadata"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"indexmap",
|
|
||||||
"wasm-encoder",
|
|
||||||
"wasmparser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasmparser"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"hashbrown 0.15.5",
|
|
||||||
"indexmap",
|
|
||||||
"semver",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1268,65 +910,12 @@ 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"
|
||||||
@@ -1416,94 +1005,6 @@ version = "0.46.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen-rust-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-core"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"heck",
|
|
||||||
"wit-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-rust"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"heck",
|
|
||||||
"indexmap",
|
|
||||||
"prettyplease",
|
|
||||||
"syn",
|
|
||||||
"wasm-metadata",
|
|
||||||
"wit-bindgen-core",
|
|
||||||
"wit-component",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-rust-macro"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"prettyplease",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wit-bindgen-core",
|
|
||||||
"wit-bindgen-rust",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-component"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"indexmap",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-encoder",
|
|
||||||
"wasm-metadata",
|
|
||||||
"wasmparser",
|
|
||||||
"wit-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-parser"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"id-arena",
|
|
||||||
"indexmap",
|
|
||||||
"log",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"unicode-xid",
|
|
||||||
"wasmparser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# 核心 Web 框架
|
# 核心 Web 框架
|
||||||
axum = "0.8.8"
|
axum = "0.8.8"
|
||||||
axum-extra = { version = "0.10", features = ["multipart"] }
|
|
||||||
# 异步运行时
|
# 异步运行时
|
||||||
tokio={version = "1.48.0",features = ["full"]}
|
tokio={version = "1.48.0",features = ["full"]}
|
||||||
# 异步文件操作与流处理工具
|
# 异步文件操作与流处理工具
|
||||||
@@ -21,12 +20,6 @@ serde_json = "1.0.147"
|
|||||||
# 物理目录递归扫描工具
|
# 物理目录递归扫描工具
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
|
||||||
# UUID 生成(用于唯一文件名)
|
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
|
||||||
|
|
||||||
# 日期时间处理
|
|
||||||
chrono = "0.4"
|
|
||||||
|
|
||||||
tracing="0.1.44"
|
tracing="0.1.44"
|
||||||
tracing-subscriber = "0.3.22"
|
tracing-subscriber = "0.3.22"
|
||||||
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -8,7 +8,7 @@
|
|||||||
- **热重载**:`mocks/*.yaml` 变更自动生效,无需重启服务
|
- **热重载**:`mocks/*.yaml` 变更自动生效,无需重启服务
|
||||||
- **高性能**:基于 Rust 异步运行时,路径首段哈希索引 O(1) 匹配
|
- **高性能**:基于 Rust 异步运行时,路径首段哈希索引 O(1) 匹配
|
||||||
- **低内存**:大响应体支持 `file://` 协议从磁盘流式读取
|
- **低内存**:大响应体支持 `file://` 协议从磁盘流式读取
|
||||||
- **文件上传**:内置 `/api/upload` 端点,按日期分目录存储
|
- **请求匹配**:支持 method、path、headers、query_params、body 多维度匹配
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -39,38 +39,37 @@ src/
|
|||||||
├── config.rs # 数据结构定义(MockRule, RequestMatcher 等)
|
├── config.rs # 数据结构定义(MockRule, RequestMatcher 等)
|
||||||
├── loader.rs # YAML 解析,目录扫描
|
├── loader.rs # YAML 解析,目录扫描
|
||||||
├── router.rs # 路径首段索引,匹配逻辑
|
├── router.rs # 路径首段索引,匹配逻辑
|
||||||
├── handler.rs # 统一请求处理器,文件流式响应
|
└── handler.rs # 统一请求处理器,文件流式响应
|
||||||
└── upload.rs # Multipart 文件上传处理
|
|
||||||
|
|
||||||
tests/ # 集成测试(每个模块一个测试文件)
|
tests/ # 集成测试(每个模块一个测试文件)
|
||||||
mocks/ # YAML Mock 配置文件
|
mocks/ # YAML Mock 配置文件
|
||||||
storage/ # 上传文件存储(按 YYYY-MM-DD 分目录)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mock 配置
|
## Mock 配置
|
||||||
|
|
||||||
详细配置规范请参考 [.claude/rules/mock-spec.md](.claude/rules/mock-spec.md)
|
详细配置规范请参考 [.claude/rules/mock-spec.md](.claude/rules/mock-spec.md)
|
||||||
|
|
||||||
## API
|
## 未来计划
|
||||||
|
|
||||||
### 文件上传
|
### 1. 动态参数与模板响应
|
||||||
|
|
||||||
```http
|
支持路径参数捕获(如 `/api/users/{id}`)和响应模板,可在响应中引用请求参数:
|
||||||
POST /api/upload
|
|
||||||
Content-Type: multipart/form-data
|
|
||||||
|
|
||||||
file: <binary>
|
```yaml
|
||||||
|
# 请求: GET /api/users/123
|
||||||
|
# 响应: { "id": "123", "name": "User 123" }
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应示例:**
|
### 2. HTTPS 支持
|
||||||
|
|
||||||
```json
|
内置 TLS/SSL 支持,提供安全的 HTTPS 服务。
|
||||||
{
|
|
||||||
"filename": "example.txt",
|
### 3. 可视化管理界面
|
||||||
"path": "storage/2024-01-15/example.txt",
|
|
||||||
"size": 1024
|
- **TUI(终端界面)**:基于 Ratatui 的交互式终端管理
|
||||||
}
|
- **GUI(图形界面)**:Web Dashboard 或桌面应用,可视化管理 Mock 规则
|
||||||
```
|
|
||||||
|
> 此功能需要深入讨论,欢迎提出建议和需求
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
id: "user-login"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/login"
|
|
||||||
query_params:
|
|
||||||
redirect: "/dashboard"
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body:
|
|
||||||
username: "test"
|
|
||||||
password: "123456"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: '{"code": 0, "message": "success", "data": {"token": "mock_token_12345"}}'
|
|
||||||
settings:
|
|
||||||
delay_ms: 100
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
id: "auth_login_001"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/auth/login"
|
|
||||||
# 必须包含此 Header 才会匹配
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "111"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"username":"user",
|
|
||||||
"password":"123"
|
|
||||||
}
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
X-Mock-Engine: "Rust-Gemini-v1.2"
|
|
||||||
# 直接内联 JSON 字符串
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6" },
|
|
||||||
"msg": "success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 2000 # 模拟真实网络延迟
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
- id: "auth_login_out_001"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/auth/login_out"
|
|
||||||
# 必须包含此 Header 才会匹配
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "111"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body:
|
|
||||||
type: true
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
X-Mock-Engine: "Rust-Gemini-v1.2"
|
|
||||||
# 直接内联 JSON 字符串
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": "退出成功",
|
|
||||||
"msg": "success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 200 # 模拟真实网络延迟
|
|
||||||
- id: "auth_login_out_002"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/auth/login_out"
|
|
||||||
# 必须包含此 Header 才会匹配
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "111"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body:
|
|
||||||
type: false
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
X-Mock-Engine: "Rust-Gemini-v1.2"
|
|
||||||
# 直接内联 JSON 字符串
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 1,
|
|
||||||
"data": "退出失败",
|
|
||||||
"msg": "success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 200 # 模拟真实网络延迟
|
|
||||||
118
mocks/v1/auth/multiple_login.yaml
Normal file
118
mocks/v1/auth/multiple_login.yaml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# 用户登录 - 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
mocks/v1/auth/register.yaml
Normal file
26
mocks/v1/auth/register.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 用户注册 - 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
mocks/v1/auth/single_login.yaml
Normal file
29
mocks/v1/auth/single_login.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 用户登录 - 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 # 模拟真实网络延迟
|
||||||
30
mocks/v1/data/export.yaml
Normal file
30
mocks/v1/data/export.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 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>
|
||||||
15
mocks/v1/health.yaml
Normal file
15
mocks/v1/health.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 健康检查 - 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 +1,12 @@
|
|||||||
id: "prod_export_pdf"
|
name: "prod_export_pdf"
|
||||||
request:
|
request:
|
||||||
method: "GET"
|
method: "GET"
|
||||||
path: "/api/v1/products/report"
|
path: "/v1/products/report"
|
||||||
body: '{"username":"user","password":"123"}'
|
body: '{"username":"user001","password":"password123"}'
|
||||||
response:
|
response:
|
||||||
status: 200
|
status: 200
|
||||||
headers:
|
headers:
|
||||||
Content-Type: "application/pdf"
|
Content-Type: "application/pdf"
|
||||||
Content-Disposition: "attachment; filename=report.pdf"
|
Content-Disposition: "attachment; filename=report.pdf"
|
||||||
# 智能协议:引擎会自动识别前缀并异步读取磁盘文件
|
# 智能协议:引擎会自动识别前缀并异步读取磁盘文件
|
||||||
body: "file://./storage/reports/annual_2024.pdf"
|
body: "file://./storage/v1/hello.pdf"
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# 使用 YAML 数组语法定义多个规则
|
|
||||||
- id: "sys_ping"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/ping"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "pong"
|
|
||||||
|
|
||||||
- id: "sys_version"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/version"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: '{"version": "1.2.0-smart"}'
|
|
||||||
73
mocks/v1/user/avatar.yaml
Normal file
73
mocks/v1/user/avatar.yaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 上传头像 - 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
mocks/v1/user/download.yaml
Normal file
13
mocks/v1/user/download.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 下载用户数据文件 - 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"
|
||||||
13
mocks/v1/user/echo.yaml
Normal file
13
mocks/v1/user/echo.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 文本回显 - 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"
|
||||||
24
mocks/v1/user/login_form.yaml
Normal file
24
mocks/v1/user/login_form.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 表单登录 - 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
mocks/v1/user/profile.yaml
Normal file
24
mocks/v1/user/profile.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 获取用户信息 - 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
id: "user_search_admin"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/users"
|
|
||||||
# 请求中必须包含 role=admin 且 status=active
|
|
||||||
query_params:
|
|
||||||
role: "admin"
|
|
||||||
status: "active"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: '{"users": [{"id": 1, "name": "SuperAdmin"}]}'
|
|
||||||
158
src/handler.rs
158
src/handler.rs
@@ -5,28 +5,158 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock}; // 必须引入 RwLock
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
|
use crate::models::Payload;
|
||||||
use crate::router::MockRouter;
|
use crate::router::MockRouter;
|
||||||
|
|
||||||
/// 共享的应用状态,router 由 RwLock 保护以支持热重载
|
/// 共享的应用状态,router 现在由 RwLock 保护以支持热重载
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub router: RwLock<MockRouter>,
|
pub router: RwLock<MockRouter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 提取请求的 Content-Type(去掉参数部分,如 boundary)
|
||||||
|
fn extract_content_type(headers: &HeaderMap) -> Option<String> {
|
||||||
|
headers
|
||||||
|
.get(axum::http::header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.map(|s| s.split(';').next().unwrap_or(s).trim().to_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据 Content-Type 解析 Body(始终以请求的 Content-Type 为准)
|
||||||
|
fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> Payload {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Payload::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match content_type {
|
||||||
|
Some(ct) if ct.contains("application/json") => {
|
||||||
|
serde_json::from_slice(bytes)
|
||||||
|
.map(Payload::Json)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
// JSON 解析失败,降级为文本
|
||||||
|
Payload::Text(String::from_utf8_lossy(bytes).to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(ct) if ct.contains("xml") => {
|
||||||
|
Payload::Xml(String::from_utf8_lossy(bytes).to_string())
|
||||||
|
}
|
||||||
|
Some(ct) if ct.contains("form-urlencoded") => {
|
||||||
|
Payload::Form(parse_urlencoded(bytes))
|
||||||
|
}
|
||||||
|
Some(ct) if ct.contains("multipart/form-data") => {
|
||||||
|
Payload::Multipart(extract_multipart_data(bytes))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Payload::Text(String::from_utf8_lossy(bytes).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 urlencoded 格式
|
||||||
|
fn parse_urlencoded(bytes: &[u8]) -> HashMap<String, String> {
|
||||||
|
let body = String::from_utf8_lossy(bytes);
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for pair in body.split('&') {
|
||||||
|
if let Some((key, value)) = pair.split_once('=') {
|
||||||
|
let decoded_key = urlencoding_decode(key);
|
||||||
|
let decoded_value = urlencoding_decode(value);
|
||||||
|
map.insert(decoded_key, decoded_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL 解码(简单实现)
|
||||||
|
fn urlencoding_decode(s: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = s.chars().peekable();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '+' {
|
||||||
|
result.push(' ');
|
||||||
|
} else if c == '%' {
|
||||||
|
let hex: String = chars.by_ref().take(2).collect();
|
||||||
|
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
|
||||||
|
result.push(byte as char);
|
||||||
|
} else {
|
||||||
|
result.push('%');
|
||||||
|
result.push_str(&hex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 multipart body 中提取键值对
|
||||||
|
fn extract_multipart_data(bytes: &[u8]) -> HashMap<String, String> {
|
||||||
|
let body = String::from_utf8_lossy(bytes);
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
// 分割 boundary
|
||||||
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
|
let mut current_name: Option<String> = None;
|
||||||
|
let mut current_value = String::new();
|
||||||
|
let mut in_value = false;
|
||||||
|
|
||||||
|
for line in &lines {
|
||||||
|
// 检测 Content-Disposition 行,提取 name
|
||||||
|
if line.contains("Content-Disposition") && line.contains("name=") {
|
||||||
|
// 保存上一个字段的值
|
||||||
|
if let Some(name) = current_name.take() {
|
||||||
|
map.insert(name, current_value.trim().to_string());
|
||||||
|
current_value.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 name 属性
|
||||||
|
if let Some(start) = line.find("name=\"") {
|
||||||
|
let start = start + 6;
|
||||||
|
if let Some(end) = line[start..].find('"') {
|
||||||
|
current_name = Some(line[start..start + end].to_string());
|
||||||
|
in_value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if line.starts_with("Content-Type") {
|
||||||
|
// 跳过 Content-Type 行
|
||||||
|
continue;
|
||||||
|
} else if line.is_empty() {
|
||||||
|
// 空行后面是值
|
||||||
|
in_value = true;
|
||||||
|
} else if in_value {
|
||||||
|
// 收集值内容
|
||||||
|
if !current_value.is_empty() {
|
||||||
|
current_value.push('\n');
|
||||||
|
}
|
||||||
|
current_value.push_str(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存最后一个字段
|
||||||
|
if let Some(name) = current_name {
|
||||||
|
map.insert(name, current_value.trim().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
/// 全局统一请求处理函数
|
/// 全局统一请求处理函数
|
||||||
pub async fn mock_handler(
|
pub async fn mock_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>, // State 必须是第一个或靠前的参数
|
||||||
method: Method,
|
method: Method,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(params): Query<HashMap<String, String>>,
|
Query(params): Query<HashMap<String, String>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>, // Request<Body> 必须是最后一个参数
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
// 1. 提取 path 和 method
|
||||||
let path = req.uri().path().to_string();
|
let path = req.uri().path().to_string();
|
||||||
let method_str = method.as_str().to_string();
|
let method_str = method.as_str().to_string();
|
||||||
|
|
||||||
// 1. 将需要的数据克隆出来,断开与 req 的借用关系
|
// 2. 提取请求的 Content-Type
|
||||||
|
let req_content_type = extract_content_type(&headers);
|
||||||
|
|
||||||
|
// 3. 读取请求 body
|
||||||
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
|
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -37,9 +167,10 @@ pub async fn mock_handler(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let incoming_json: Option<serde_json::Value> = serde_json::from_slice(&body_bytes).ok();
|
// 4. 根据【请求的 Content-Type】解析 body
|
||||||
|
let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes);
|
||||||
|
|
||||||
// 2. 将 Axum HeaderMap 转换为简单的 HashMap
|
// 5. 将 Axum HeaderMap 转换为简单的 HashMap
|
||||||
let mut req_headers = HashMap::new();
|
let mut req_headers = HashMap::new();
|
||||||
for (name, value) in headers.iter() {
|
for (name, value) in headers.iter() {
|
||||||
if let Ok(v) = value.to_str() {
|
if let Ok(v) = value.to_str() {
|
||||||
@@ -47,22 +178,22 @@ pub async fn mock_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 执行匹配逻辑:先获取读锁 (Read Lock)
|
// 6. 执行匹配逻辑:先获取读锁 (Read Lock)
|
||||||
let maybe_rule = {
|
let maybe_rule = {
|
||||||
let router = state.router.read().expect("Failed to acquire read lock");
|
let router = state.router.read().expect("Failed to acquire read lock");
|
||||||
router.match_rule(&method_str, &path, ¶ms, &req_headers, &incoming_json).cloned()
|
router.match_rule(&method_str, &path, ¶ms, &req_headers, &parsed_body).cloned()
|
||||||
// 使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁
|
// 此处使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(rule) = maybe_rule {
|
if let Some(rule) = maybe_rule {
|
||||||
// 4. 处理模拟延迟
|
// 7. 处理模拟延迟
|
||||||
if let Some(ref settings) = rule.settings {
|
if let Some(ref settings) = rule.settings {
|
||||||
if let Some(delay) = settings.delay_ms {
|
if let Some(delay) = settings.delay_ms {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 构建响应
|
// 8. 构建响应
|
||||||
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
|
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
|
||||||
let mut response_builder = Response::builder().status(status);
|
let mut response_builder = Response::builder().status(status);
|
||||||
|
|
||||||
@@ -72,7 +203,7 @@ pub async fn mock_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Smart Body 逻辑
|
// 9. Smart Body 逻辑
|
||||||
if let Some(file_path) = rule.response.get_file_path() {
|
if let Some(file_path) = rule.response.get_file_path() {
|
||||||
match tokio::fs::File::open(file_path).await {
|
match tokio::fs::File::open(file_path).await {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
@@ -95,7 +226,6 @@ pub async fn mock_handler(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("请求头{:?}", req_headers);
|
|
||||||
// 匹配失败返回 404
|
// 匹配失败返回 404
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// 声明模块并设为 pub,这样 tests/ 目录才能看到它们
|
// 声明模块并设为 pub,这样 tests/ 目录才能看到它们
|
||||||
pub mod config;
|
pub mod models;
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod upload;
|
|
||||||
@@ -3,7 +3,7 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use walkdir::WalkDir; // 需在 Cargo.toml 添加 walkdir 依赖
|
use walkdir::WalkDir; // 需在 Cargo.toml 添加 walkdir 依赖
|
||||||
|
|
||||||
use crate::config::{MockRule, MockSource}; // 假设 config.rs 中定义了这两个类型
|
use crate::models::{MockRule, MockSource};
|
||||||
|
|
||||||
pub struct MockLoader;
|
pub struct MockLoader;
|
||||||
|
|
||||||
|
|||||||
29
src/main.rs
29
src/main.rs
@@ -2,42 +2,30 @@ use std::net::SocketAddr;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use axum::{routing::{any, post}, Router};
|
use axum::{routing::any, Router};
|
||||||
use notify_debouncer_mini::{new_debouncer, notify::*};
|
use notify_debouncer_mini::{new_debouncer, notify::*};
|
||||||
|
|
||||||
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};
|
||||||
use mock_server::upload::upload_handler;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// 1. 确保 mocks 目录存在
|
|
||||||
let mocks_dir = Path::new("./mocks");
|
let mocks_dir = Path::new("./mocks");
|
||||||
if !mocks_dir.exists() {
|
if !mocks_dir.exists() {
|
||||||
println!("Warning: 'mocks/' directory not found. Creating it...");
|
|
||||||
std::fs::create_dir_all(mocks_dir).unwrap();
|
std::fs::create_dir_all(mocks_dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 确保 storage 目录存在
|
// 1. 初始加载
|
||||||
let storage_dir = Path::new("./storage");
|
|
||||||
if !storage_dir.exists() {
|
|
||||||
println!("Creating storage directory...");
|
|
||||||
std::fs::create_dir_all(storage_dir).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 初始加载 Mock 配置
|
|
||||||
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);
|
||||||
|
|
||||||
// 4. 构建共享状态(使用 RwLock 支持热重载)
|
|
||||||
let shared_state = Arc::new(AppState {
|
let shared_state = Arc::new(AppState {
|
||||||
router: RwLock::new(MockRouter::new(index)),
|
router: RwLock::new(MockRouter::new(index)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 设置热加载监听器
|
// 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.to_path_buf();
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
@@ -63,20 +51,13 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. 配置 Axum 路由
|
// 3. 配置 Axum 路由
|
||||||
// 文件上传路由:POST /api/upload
|
|
||||||
// 其他所有请求由 mock_handler 处理
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/upload", post(upload_handler))
|
|
||||||
.fallback(any(mock_handler))
|
.fallback(any(mock_handler))
|
||||||
.with_state(shared_state);
|
.with_state(shared_state);
|
||||||
|
|
||||||
// 7. 启动服务
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
println!("🚀 Rust Mock Server is running on http://{}", addr);
|
println!("🚀 Server running at http://{}", addr);
|
||||||
println!("📁 File upload endpoint: POST http://{}/api/upload", addr);
|
|
||||||
println!("🔄 Hot reload enabled for mocks/ directory");
|
|
||||||
println!("Ready to handle requests based on your YAML definitions.");
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
|||||||
@@ -1,13 +1,58 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// 解析后的请求 Body
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Payload {
|
||||||
|
/// JSON 格式
|
||||||
|
Json(serde_json::Value),
|
||||||
|
/// XML 格式
|
||||||
|
Xml(String),
|
||||||
|
/// URL 编码表单
|
||||||
|
Form(HashMap<String, String>),
|
||||||
|
/// Multipart 表单(键值对)
|
||||||
|
Multipart(HashMap<String, String>),
|
||||||
|
/// 纯文本
|
||||||
|
Text(String),
|
||||||
|
/// 无 Body
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Payload {
|
||||||
|
/// 转换为字符串(用于兜底比较)
|
||||||
|
pub fn to_compare_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Payload::Json(v) => v.to_string(),
|
||||||
|
Payload::Xml(s) | Payload::Text(s) => s.clone(),
|
||||||
|
Payload::Form(map) | Payload::Multipart(map) => {
|
||||||
|
let mut pairs: Vec<_> = map.iter().collect();
|
||||||
|
pairs.sort_by_key(|(k, _)| *k);
|
||||||
|
pairs.iter().map(|(k, v)| format!("{}={}", k, v)).collect::<Vec<_>>().join("&")
|
||||||
|
}
|
||||||
|
Payload::None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取对应的 Content-Type 名称
|
||||||
|
pub fn content_type_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Payload::Json(_) => "application/json",
|
||||||
|
Payload::Xml(_) => "application/xml",
|
||||||
|
Payload::Form(_) => "application/x-www-form-urlencoded",
|
||||||
|
Payload::Multipart(_) => "multipart/form-data",
|
||||||
|
Payload::Text(_) => "text/plain",
|
||||||
|
Payload::None => "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 顶层包装:支持单对象或数组,自动打平
|
/// 顶层包装:支持单对象或数组,自动打平
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum MockSource {
|
pub enum MockSource {
|
||||||
/// 对应“一个接口一个文件”模式
|
/// 对应"一个接口一个文件"模式
|
||||||
Single(MockRule),
|
Single(MockRule),
|
||||||
/// 对应“一个文件多个接口”模式
|
/// 对应"一个文件多个接口"模式
|
||||||
Multiple(Vec<MockRule>),
|
Multiple(Vec<MockRule>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,16 +67,16 @@ impl MockSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 核心 Mock 规则定义
|
/// 核心 Mock 规则定义
|
||||||
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
pub struct MockRule {
|
pub struct MockRule {
|
||||||
pub id: String,
|
pub name: String,
|
||||||
pub request: RequestMatcher,
|
pub request: RequestMatcher,
|
||||||
pub response: MockResponse,
|
pub response: MockResponse,
|
||||||
pub settings: Option<MockSettings>,
|
pub settings: Option<MockSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求匹配条件
|
/// 请求匹配条件
|
||||||
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
pub struct RequestMatcher {
|
pub struct RequestMatcher {
|
||||||
pub method: String,
|
pub method: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -39,12 +84,12 @@ pub struct RequestMatcher {
|
|||||||
pub query_params: Option<HashMap<String, String>>,
|
pub query_params: Option<HashMap<String, String>>,
|
||||||
/// 选填:只有请求包含这些 Header 时才匹配
|
/// 选填:只有请求包含这些 Header 时才匹配
|
||||||
pub headers: Option<HashMap<String, String>>,
|
pub headers: Option<HashMap<String, String>>,
|
||||||
// 修改点:从 String 改为 Option<serde_json::Value>
|
/// 选填:请求体匹配条件
|
||||||
pub body: Option<serde_json::Value>,
|
pub body: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 响应内容定义
|
/// 响应内容定义
|
||||||
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
pub struct MockResponse {
|
pub struct MockResponse {
|
||||||
pub status: u16,
|
pub status: u16,
|
||||||
pub headers: Option<HashMap<String, String>>,
|
pub headers: Option<HashMap<String, String>>,
|
||||||
@@ -53,7 +98,7 @@ pub struct MockResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 模拟器行为设置
|
/// 模拟器行为设置
|
||||||
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
pub struct MockSettings {
|
pub struct MockSettings {
|
||||||
/// 模拟网络延迟(毫秒)
|
/// 模拟网络延迟(毫秒)
|
||||||
pub delay_ms: Option<u64>,
|
pub delay_ms: Option<u64>,
|
||||||
173
src/router.rs
173
src/router.rs
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::config::MockRule;
|
use crate::models::{MockRule, Payload};
|
||||||
|
|
||||||
pub struct MockRouter {
|
pub struct MockRouter {
|
||||||
// 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则
|
// 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则
|
||||||
@@ -18,7 +18,7 @@ impl MockRouter {
|
|||||||
path: &str,
|
path: &str,
|
||||||
queries: &HashMap<String, String>,
|
queries: &HashMap<String, String>,
|
||||||
headers: &HashMap<String, String>,
|
headers: &HashMap<String, String>,
|
||||||
incoming_body: &Option<serde_json::Value>, // 修改 1: 增加参数
|
payload: &Payload,
|
||||||
) -> Option<&MockRule> {
|
) -> Option<&MockRule> {
|
||||||
// 1. 提取请求路径的首段作为索引 Key
|
// 1. 提取请求路径的首段作为索引 Key
|
||||||
let key = self.extract_first_segment(path);
|
let key = self.extract_first_segment(path);
|
||||||
@@ -27,7 +27,7 @@ impl MockRouter {
|
|||||||
if let Some(rules) = self.index.get(&key) {
|
if let Some(rules) = self.index.get(&key) {
|
||||||
// 3. 在候选集中进行线性深度匹配
|
// 3. 在候选集中进行线性深度匹配
|
||||||
for rule in rules {
|
for rule in rules {
|
||||||
if self.is_match(rule, method, path, queries, headers,incoming_body) {
|
if self.is_match(rule, method, path, queries, headers, payload) {
|
||||||
return Some(rule);
|
return Some(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,18 +44,19 @@ impl MockRouter {
|
|||||||
path: &str,
|
path: &str,
|
||||||
queries: &HashMap<String, String>,
|
queries: &HashMap<String, String>,
|
||||||
headers: &HashMap<String, String>,
|
headers: &HashMap<String, String>,
|
||||||
incoming_body: &Option<serde_json::Value>, // 修改 3: 增加参数
|
payload: &Payload,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠)
|
// A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠)
|
||||||
if rule.request.method.to_uppercase() != method.to_uppercase() {
|
if rule.request.method.to_uppercase() != method.to_uppercase() {
|
||||||
println!("DEBUG: [ID:{}] Method Mismatch: YAML={}, Req={}", rule.id, rule.request.method, method);
|
println!("DEBUG: [NAME:{}] Method Mismatch: YAML={}, Req={}", rule.name, rule.request.method, method);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
|
if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
|
||||||
println!("DEBUG: [ID:{}] Path Mismatch: YAML='{}', Req='{}'", rule.id, rule.request.path, path);
|
println!("DEBUG: [NAME:{}] Path Mismatch: YAML='{}', Req='{}'", rule.name, rule.request.path, path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
println!("DEBUG: [ID:{}] Method and Path matched! Checking headers...", rule.id);
|
println!("DEBUG: [NAME:{}] Method and Path matched! Checking headers...", rule.name);
|
||||||
|
|
||||||
// B. Query 参数校验 (子集匹配原则)
|
// B. Query 参数校验 (子集匹配原则)
|
||||||
if let Some(ref required_queries) = rule.request.query_params {
|
if let Some(ref required_queries) = rule.request.query_params {
|
||||||
for (key, val) in required_queries {
|
for (key, val) in required_queries {
|
||||||
@@ -65,46 +66,71 @@ impl MockRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// C. Header 校验 (优化版)
|
// C. Header 校验 (大小写不敏感,Content-Type 使用前缀匹配)
|
||||||
if let Some(ref required_headers) = rule.request.headers {
|
if let Some(ref required_headers) = rule.request.headers {
|
||||||
for (key, val) in required_headers {
|
for (key, val) in required_headers {
|
||||||
println!("{}:{}",key.clone(), val.clone());
|
let key_lower = key.to_lowercase();
|
||||||
// 方案:将 Key 统一转为小写比较,并检查请求头是否“包含”期望的值
|
|
||||||
let matched = headers.iter().any(|(k, v)| {
|
let matched = headers.iter().any(|(k, v)| {
|
||||||
// k.to_lowercase() == key.to_lowercase() && v.contains(val)
|
if k.to_lowercase() != key_lower {
|
||||||
k.to_lowercase() == key.to_lowercase() && v==val
|
return false;
|
||||||
|
}
|
||||||
|
// Content-Type 使用前缀匹配(因为可能包含 boundary 等参数)
|
||||||
|
if key_lower == "content-type" {
|
||||||
|
v.to_lowercase().starts_with(&val.to_lowercase())
|
||||||
|
} else {
|
||||||
|
v == val
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if !matched {
|
if !matched {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// D. 智能 Body 全量比对逻辑
|
|
||||||
if let Some(ref required_val) = rule.request.body {
|
|
||||||
match incoming_body {
|
|
||||||
Some(actual_val) => {
|
|
||||||
// 实现你的想法:尝试将 YAML 中的 String 转换为 Object 再对比
|
|
||||||
let final_required = if let Some(s) = required_val.as_str() {
|
|
||||||
// 如果能解析成 JSON,就用解析后的对象,否则用原始字符串 Value
|
|
||||||
serde_json::from_str::<serde_json::Value>(s).unwrap_or_else(|_| required_val.clone())
|
|
||||||
} else {
|
|
||||||
required_val.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行全量相等比对
|
// D. Body 匹配(根据 Payload 类型智能比较)
|
||||||
if final_required != *actual_val {
|
if let Some(ref yaml_body) = rule.request.body {
|
||||||
println!("DEBUG: [ID:{}] Body Mismatch", rule.id);
|
return self.match_body(yaml_body, payload);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => return false, // YAML 要求有 Body 但请求为空
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Body 匹配逻辑
|
||||||
|
fn match_body(&self, yaml_body: &serde_json::Value, payload: &Payload) -> bool {
|
||||||
|
// 将 YAML body 规范化:如果是字符串,尝试解析为 JSON
|
||||||
|
let normalized_body = normalize_yaml_body(yaml_body);
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
Payload::Json(actual) => {
|
||||||
|
// JSON 对象比较
|
||||||
|
&normalized_body == actual
|
||||||
|
}
|
||||||
|
Payload::Xml(actual) => {
|
||||||
|
// XML 字符串比较(规范化后比较)
|
||||||
|
normalized_body.as_str()
|
||||||
|
.map(|expected| normalize_xml(expected) == normalize_xml(actual))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
Payload::Form(actual) => {
|
||||||
|
// Form 键值对比较(子集匹配)
|
||||||
|
compare_form_with_yaml(&normalized_body, actual)
|
||||||
|
}
|
||||||
|
Payload::Multipart(actual_data) => {
|
||||||
|
// Multipart 匹配:支持键值对或字段名列表
|
||||||
|
compare_multipart_with_yaml(&normalized_body, actual_data)
|
||||||
|
}
|
||||||
|
Payload::Text(actual) => {
|
||||||
|
// 字符串比较(去掉首尾空白)
|
||||||
|
normalized_body.as_str()
|
||||||
|
.map(|expected| expected.trim() == actual.trim())
|
||||||
|
.unwrap_or_else(|| normalized_body.to_string().trim() == actual.trim())
|
||||||
|
}
|
||||||
|
Payload::None => {
|
||||||
|
false // YAML 配置了 body,但请求没有 body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 与 Loader 保持一致的 Key 提取算法
|
/// 与 Loader 保持一致的 Key 提取算法
|
||||||
fn extract_first_segment(&self, path: &str) -> String {
|
fn extract_first_segment(&self, path: &str) -> String {
|
||||||
path.trim_start_matches('/')
|
path.trim_start_matches('/')
|
||||||
@@ -114,3 +140,86 @@ impl MockRouter {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 规范化 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 字符串:去掉声明、多余空白、格式化为紧凑形式
|
||||||
|
fn normalize_xml(xml: &str) -> String {
|
||||||
|
let mut result = xml.to_string();
|
||||||
|
|
||||||
|
// 去掉 XML 声明
|
||||||
|
if let Some(pos) = result.find("?>") {
|
||||||
|
if result[..pos].contains("<?xml") {
|
||||||
|
result = result[pos + 2..].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉多余空白字符
|
||||||
|
result = result
|
||||||
|
.chars()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.chunks(1)
|
||||||
|
.map(|c| c[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// 分割成行,去掉每行首尾空白,过滤空行
|
||||||
|
result = result
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim())
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Form 比较:YAML 中的键值对必须是请求的子集
|
||||||
|
fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
||||||
|
let yaml_map = match yaml_body.as_object() {
|
||||||
|
Some(obj) => obj,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, yaml_val) in yaml_map {
|
||||||
|
let expected = yaml_val.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_val.to_string());
|
||||||
|
if actual.get(key) != Some(&expected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multipart 比较:对象和数组形式都只匹配字段名是否存在
|
||||||
|
fn compare_multipart_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
||||||
|
// 方式 1:对象形式 - 只匹配键名是否存在(忽略值)
|
||||||
|
if let Some(yaml_map) = yaml_body.as_object() {
|
||||||
|
for key in yaml_map.keys() {
|
||||||
|
if !actual.contains_key(key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式 2:数组形式 - 只匹配字段名是否存在
|
||||||
|
if let Some(yaml_array) = yaml_body.as_array() {
|
||||||
|
for yaml_field in yaml_array {
|
||||||
|
let field_name = yaml_field.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_field.to_string());
|
||||||
|
if !actual.contains_key(&field_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|||||||
150
src/upload.rs
150
src/upload.rs
@@ -1,150 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
body::Body,
|
|
||||||
extract::State,
|
|
||||||
http::StatusCode,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use axum_extra::extract::Multipart;
|
|
||||||
use chrono::Local;
|
|
||||||
use serde_json::json;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::fs;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::handler::AppState;
|
|
||||||
|
|
||||||
/// 文件上传结果
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub struct UploadResult {
|
|
||||||
pub success: bool,
|
|
||||||
pub files: Vec<FileInfo>,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 单个文件信息
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub struct FileInfo {
|
|
||||||
pub original_name: String,
|
|
||||||
pub stored_name: String,
|
|
||||||
pub path: String,
|
|
||||||
pub size: usize,
|
|
||||||
pub content_type: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UploadResult {
|
|
||||||
pub fn success(files: Vec<FileInfo>) -> Self {
|
|
||||||
UploadResult {
|
|
||||||
success: true,
|
|
||||||
files,
|
|
||||||
message: "Files uploaded successfully".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(message: String) -> Self {
|
|
||||||
UploadResult {
|
|
||||||
success: false,
|
|
||||||
files: vec![],
|
|
||||||
message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for UploadResult {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
let status = if self.success {
|
|
||||||
StatusCode::OK
|
|
||||||
} else {
|
|
||||||
StatusCode::BAD_REQUEST
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = serde_json::to_string(&self).unwrap_or_else(|_| {
|
|
||||||
json!({
|
|
||||||
"success": false,
|
|
||||||
"files": [],
|
|
||||||
"message": "Failed to serialize response"
|
|
||||||
})
|
|
||||||
.to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
Response::builder()
|
|
||||||
.status(status)
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.body(Body::from(body))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理文件上传
|
|
||||||
pub async fn upload_handler(
|
|
||||||
State(_state): State<Arc<AppState>>,
|
|
||||||
mut multipart: Multipart,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let mut uploaded_files = Vec::new();
|
|
||||||
|
|
||||||
// 确保 storage 目录存在
|
|
||||||
let storage_root = PathBuf::from("storage");
|
|
||||||
if let Err(e) = fs::create_dir_all(&storage_root).await {
|
|
||||||
return UploadResult::error(format!("Failed to create storage directory: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建按日期分类的子目录
|
|
||||||
let date_dir = Local::now().format("%Y-%m-%d").to_string();
|
|
||||||
let upload_dir = storage_root.join(&date_dir);
|
|
||||||
if let Err(e) = fs::create_dir_all(&upload_dir).await {
|
|
||||||
return UploadResult::error(format!("Failed to create upload directory: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理每个上传的字段
|
|
||||||
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
|
||||||
// 获取文件名
|
|
||||||
let original_name = field
|
|
||||||
.file_name()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
// 获取内容类型
|
|
||||||
let content_type = field.content_type().map(|s| s.to_string());
|
|
||||||
|
|
||||||
// 读取文件数据
|
|
||||||
let data = match field.bytes().await {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to read file data: {}", e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成唯一文件名,保留原始扩展名
|
|
||||||
let extension = std::path::Path::new(&original_name)
|
|
||||||
.extension()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.map(|s| format!(".{}", s))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let stored_name = format!("{}{}", Uuid::new_v4(), extension);
|
|
||||||
let file_path = upload_dir.join(&stored_name);
|
|
||||||
|
|
||||||
// 保存文件
|
|
||||||
match fs::write(&file_path, &data).await {
|
|
||||||
Ok(_) => {
|
|
||||||
uploaded_files.push(FileInfo {
|
|
||||||
original_name: original_name.clone(),
|
|
||||||
stored_name: stored_name.clone(),
|
|
||||||
path: format!("storage/{}/{}", date_dir, stored_name),
|
|
||||||
size: data.len(),
|
|
||||||
content_type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to save file {}: {}", original_name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if uploaded_files.is_empty() {
|
|
||||||
UploadResult::error("No files were uploaded".to_string())
|
|
||||||
} else {
|
|
||||||
UploadResult::success(uploaded_files)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
storage/v1/hello.pdf
Normal file
BIN
storage/v1/hello.pdf
Normal file
Binary file not shown.
20
storage/v1/user_data.json
Normal file
20
storage/v1/user_data.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"exportInfo": {
|
||||||
|
"exportedAt": "2026-03-27T10:00:00Z",
|
||||||
|
"version": "2.0.0"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"id": 10001,
|
||||||
|
"username": "admin",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"nickname": "管理员",
|
||||||
|
"role": "administrator",
|
||||||
|
"createdAt": "2025-01-01T00:00:00Z",
|
||||||
|
"lastLoginAt": "2026-03-27T09:30:00Z"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "dark",
|
||||||
|
"language": "zh-CN",
|
||||||
|
"notifications": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
use mock_server::config::{MockResponse, MockSettings};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mock_response_file_protocol() {
|
|
||||||
// 测试 1: 文件协议识别
|
|
||||||
let response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "file://./data/file.txt".to_string(),
|
|
||||||
};
|
|
||||||
assert!(response.is_file_protocol());
|
|
||||||
|
|
||||||
// 测试 2: 非文件协议识别
|
|
||||||
let inline_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "{\"key\": \"value\"}".to_string(),
|
|
||||||
};
|
|
||||||
assert!(!inline_response.is_file_protocol());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mock_response_file_path_extraction() {
|
|
||||||
// 测试 1: 提取文件路径
|
|
||||||
let response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "file://./data/file.txt".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(response.get_file_path(), Some("./data/file.txt"));
|
|
||||||
|
|
||||||
// 测试 2: 提取相对路径
|
|
||||||
let response2 = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "file:///absolute/path/file.pdf".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(response2.get_file_path(), Some("/absolute/path/file.pdf"));
|
|
||||||
|
|
||||||
// 测试 3: 非文件协议返回 None
|
|
||||||
let inline_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "inline content".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(inline_response.get_file_path(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mock_settings_delay() {
|
|
||||||
// 测试 1: 有延迟设置
|
|
||||||
let settings = MockSettings {
|
|
||||||
delay_ms: Some(1000),
|
|
||||||
};
|
|
||||||
assert_eq!(settings.delay_ms, Some(1000));
|
|
||||||
|
|
||||||
// 测试 2: 无延迟设置
|
|
||||||
let settings = MockSettings { delay_ms: None };
|
|
||||||
assert_eq!(settings.delay_ms, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mock_response_various_body_formats() {
|
|
||||||
// 测试 1: JSON 格式
|
|
||||||
let json_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: r#"{"code": 0, "message": "success"}"#.to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(json_response.status, 200);
|
|
||||||
assert!(!json_response.is_file_protocol());
|
|
||||||
|
|
||||||
// 测试 2: HTML 格式
|
|
||||||
let html_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: Some(vec![("Content-Type".to_string(), "text/html".to_string())]
|
|
||||||
.into_iter()
|
|
||||||
.collect()),
|
|
||||||
body: "<html><body>Hello</body></html>".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(html_response.status, 200);
|
|
||||||
|
|
||||||
// 测试 3: 纯文本
|
|
||||||
let text_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "Plain text response".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(text_response.body, "Plain text response");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mock_response_with_headers() {
|
|
||||||
let mut headers = std::collections::HashMap::new();
|
|
||||||
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
|
||||||
headers.insert("Authorization".to_string(), "Bearer token".to_string());
|
|
||||||
|
|
||||||
let response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: Some(headers),
|
|
||||||
body: "response body".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(response.status, 200);
|
|
||||||
assert!(response.headers.is_some());
|
|
||||||
let response_headers = response.headers.unwrap();
|
|
||||||
assert_eq!(response_headers.get("Content-Type"), Some(&"application/json".to_string()));
|
|
||||||
assert_eq!(response_headers.get("Authorization"), Some(&"Bearer token".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mock_response_edge_cases() {
|
|
||||||
// 测试 1: 空文件路径
|
|
||||||
let empty_file_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "file://".to_string(),
|
|
||||||
};
|
|
||||||
assert!(empty_file_response.is_file_protocol());
|
|
||||||
assert_eq!(empty_file_response.get_file_path(), Some(""));
|
|
||||||
|
|
||||||
// 测试 2: 空响应体
|
|
||||||
let empty_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(empty_response.body, "");
|
|
||||||
|
|
||||||
// 测试 3: 包含特殊字符的文件路径
|
|
||||||
let special_path_response = MockResponse {
|
|
||||||
status: 200,
|
|
||||||
headers: None,
|
|
||||||
body: "file://./data/file with spaces.txt".to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
special_path_response.get_file_path(),
|
|
||||||
Some("./data/file with spaces.txt")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use axum::{
|
|
||||||
body::Body,
|
|
||||||
extract::Request,
|
|
||||||
http::{HeaderMap, HeaderValue, Method, StatusCode},
|
|
||||||
response::IntoResponse,
|
|
||||||
};
|
|
||||||
use mock_server::config::MockSource;
|
|
||||||
use mock_server::handler::{AppState, mock_handler};
|
|
||||||
use mock_server::router::MockRouter;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Write;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
|
|
||||||
/// 创建带有 Mock 规则的 AppState
|
|
||||||
fn create_app_state_with_rules(rules: Vec<&str>) -> Arc<AppState> {
|
|
||||||
let temp_dir = tempdir().expect("无法创建临时目录");
|
|
||||||
let root_path = temp_dir.path();
|
|
||||||
|
|
||||||
let mut all_rules = Vec::new();
|
|
||||||
for (i, yaml_content) in rules.iter().enumerate() {
|
|
||||||
let file_path = root_path.join(format!("rule_{}.yaml", i));
|
|
||||||
let mut file = File::create(&file_path).unwrap();
|
|
||||||
writeln!(file, "{}", yaml_content).unwrap();
|
|
||||||
|
|
||||||
let content = fs::read_to_string(&file_path).unwrap();
|
|
||||||
if let Ok(source) = serde_yaml::from_str::<MockSource>(&content) {
|
|
||||||
all_rules.extend(source.flatten());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
for rule in all_rules {
|
|
||||||
let key = rule.request.path.trim_start_matches('/')
|
|
||||||
.split('/')
|
|
||||||
.next()
|
|
||||||
.unwrap_or("root")
|
|
||||||
.to_string();
|
|
||||||
index.entry(key).or_insert_with(Vec::new).push(rule);
|
|
||||||
}
|
|
||||||
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
Arc::new(AppState { router: RwLock::new(router) })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_basic_request() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "basic_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/test"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "test response"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri("/api/test")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::GET,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_with_query_params() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "query_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/search"
|
|
||||||
query_params:
|
|
||||||
q: "rust"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "search results"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri("/api/search?q=rust")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut query_params = HashMap::new();
|
|
||||||
query_params.insert("q".to_string(), "rust".to_string());
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::GET,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(query_params),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_with_headers() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "header_test"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/data"
|
|
||||||
headers:
|
|
||||||
Authorization: "Bearer token123"
|
|
||||||
Content-Type: "application/json"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "data response"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::POST)
|
|
||||||
.uri("/api/data")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
|
|
||||||
headers.insert("content-type", HeaderValue::from_static("application/json"));
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::POST,
|
|
||||||
headers,
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_with_body() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "body_test"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/users"
|
|
||||||
body:
|
|
||||||
name: "John"
|
|
||||||
age: 30
|
|
||||||
response:
|
|
||||||
status: 201
|
|
||||||
body: "user created"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let body_json = serde_json::json!({ "name": "John", "age": 30 });
|
|
||||||
let body_bytes = serde_json::to_vec(&body_json).unwrap();
|
|
||||||
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::POST)
|
|
||||||
.uri("/api/users")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.body(Body::from(body_bytes))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::POST,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::CREATED);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_no_match() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "existing_rule"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/existing"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "exists"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri("/api/nonexistent")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::GET,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_response_headers() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "headers_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/with-headers"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Cache-Control: "no-cache"
|
|
||||||
body: '{"key": "value"}'
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri("/api/with-headers")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::GET,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
assert_eq!(
|
|
||||||
response.headers().get("content-type").unwrap(),
|
|
||||||
"application/json"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
response.headers().get("cache-control").unwrap(),
|
|
||||||
"no-cache"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_different_status_codes() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml1 = r#"
|
|
||||||
id: "success"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/success"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "ok"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let yaml2 = r#"
|
|
||||||
id: "created"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/created"
|
|
||||||
response:
|
|
||||||
status: 201
|
|
||||||
body: "created"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let yaml3 = r#"
|
|
||||||
id: "error"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/error"
|
|
||||||
response:
|
|
||||||
status: 500
|
|
||||||
body: "server error"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml1, yaml2, yaml3]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
// 测试 200
|
|
||||||
let request1 = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri("/api/success")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response1 = mock_handler(
|
|
||||||
axum::extract::State(state.clone()),
|
|
||||||
Method::GET,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request1,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response1.status(), StatusCode::OK);
|
|
||||||
|
|
||||||
// 测试 201
|
|
||||||
let request2 = Request::builder()
|
|
||||||
.method(Method::POST)
|
|
||||||
.uri("/api/created")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response2 = mock_handler(
|
|
||||||
axum::extract::State(state.clone()),
|
|
||||||
Method::POST,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request2,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response2.status(), StatusCode::CREATED);
|
|
||||||
|
|
||||||
// 测试 500
|
|
||||||
let request3 = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri("/api/error")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let response3 = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::GET,
|
|
||||||
HeaderMap::new(),
|
|
||||||
axum::extract::Query(HashMap::new()),
|
|
||||||
request3,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response3.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_handler_complex_matching() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
let yaml = r#"
|
|
||||||
id: "complex"
|
|
||||||
request:
|
|
||||||
method: "PUT"
|
|
||||||
path: "/api/users/123"
|
|
||||||
query_params:
|
|
||||||
update: "true"
|
|
||||||
headers:
|
|
||||||
Authorization: "Bearer secret"
|
|
||||||
body:
|
|
||||||
name: "Updated Name"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "updated"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let state = create_app_state_with_rules(vec![yaml]);
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let body_json = serde_json::json!({ "name": "Updated Name" });
|
|
||||||
let body_bytes = serde_json::to_vec(&body_json).unwrap();
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("content-type", HeaderValue::from_static("application/json"));
|
|
||||||
headers.insert("authorization", HeaderValue::from_static("Bearer secret"));
|
|
||||||
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::PUT)
|
|
||||||
.uri("/api/users/123")
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.header("authorization", "Bearer secret")
|
|
||||||
.body(Body::from(body_bytes))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut query_params = HashMap::new();
|
|
||||||
query_params.insert("update".to_string(), "true".to_string());
|
|
||||||
|
|
||||||
let response = mock_handler(
|
|
||||||
axum::extract::State(state),
|
|
||||||
Method::PUT,
|
|
||||||
headers,
|
|
||||||
axum::extract::Query(query_params),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,160 +1,92 @@
|
|||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
// 确保 Cargo.toml 中有 serde_json
|
use mock_server::models::MockSource;
|
||||||
use serde_json::json;
|
|
||||||
use mock_server::config::MockSource;
|
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
use mock_server::router::MockRouter;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// 模块一:验证 Config 反序列化逻辑
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_parsing_scenarios() {
|
fn test_config_deserialization() {
|
||||||
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
// 测试 1:验证单接口 YAML 解析
|
||||||
let yaml_single = r#"
|
let single_yaml = r#"
|
||||||
id: "auth_v1"
|
name: "auth_v1"
|
||||||
request:
|
request:
|
||||||
method: "POST"
|
method: "POST"
|
||||||
path: "/api/v1/login"
|
path: "/api/v1/login"
|
||||||
body: { "user": "admin" }
|
response:
|
||||||
response: { status: 200, body: "welcome" }
|
status: 200
|
||||||
|
body: "inline_content"
|
||||||
"#;
|
"#;
|
||||||
let source_s: MockSource = serde_yaml::from_str(yaml_single).expect("解析单接口失败");
|
let res: MockSource = serde_yaml::from_str(single_yaml).expect("应该成功解析单接口");
|
||||||
let rules = source_s.flatten();
|
assert_eq!(res.flatten().len(), 1);
|
||||||
assert_eq!(rules.len(), 1);
|
|
||||||
// 验证 body 是否被成功解析为 Value::Object
|
|
||||||
assert!(rules[0].request.body.is_some());
|
|
||||||
assert_eq!(rules[0].request.body.as_ref().unwrap()["user"], "admin");
|
|
||||||
|
|
||||||
// 场景 B: 验证多接口配置 (原有逻辑不变)
|
// 测试 2:验证多接口 YAML 数组解析
|
||||||
let yaml_multi = r#"
|
let multi_yaml = r#"
|
||||||
- id: "api_1"
|
- name: "api_1"
|
||||||
request: { method: "GET", path: "/health" }
|
request: { method: "GET", path: "/1" }
|
||||||
response: { status: 200, body: "ok" }
|
response: { status: 200, body: "b1" }
|
||||||
- id: "api_2"
|
- name: "api_2"
|
||||||
request: { method: "GET", path: "/version" }
|
request: { method: "GET", path: "/2" }
|
||||||
response: { status: 200, body: "1.0" }
|
response: { status: 200, body: "b2" }
|
||||||
"#;
|
"#;
|
||||||
let source_m: MockSource = serde_yaml::from_str(yaml_multi).expect("解析多接口失败");
|
let res_multi: MockSource = serde_yaml::from_str(multi_yaml).expect("应该成功解析接口数组");
|
||||||
assert_eq!(source_m.flatten().len(), 2);
|
assert_eq!(res_multi.flatten().len(), 2);
|
||||||
|
|
||||||
// 场景 C: 验证 Smart Body 的 file:// 协议字符串解析 (原有逻辑不变)
|
|
||||||
let yaml_file = r#"
|
|
||||||
id: "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 递归扫描与索引构建 (不涉及 Matcher 逻辑,基本保持不变)
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_loader_recursive_indexing() {
|
fn test_recursive_loading_logic() {
|
||||||
let temp_root = tempdir().expect("无法创建临时目录");
|
// 创建临时 Mock 目录结构
|
||||||
let root_path = temp_root.path();
|
let root_dir = tempdir().expect("创建临时目录失败");
|
||||||
|
let root_path = root_dir.path();
|
||||||
|
|
||||||
let auth_path = root_path.join("v1/auth");
|
// 构造物理层级:mocks/v1/user/
|
||||||
fs::create_dir_all(&auth_path).unwrap();
|
let user_dir = root_path.join("v1/user");
|
||||||
|
fs::create_dir_all(&user_dir).unwrap();
|
||||||
|
|
||||||
let mut f1 = File::create(auth_path.join("login.yaml")).unwrap();
|
// 在深层目录创建单接口文件
|
||||||
writeln!(f1, "id: 'l1'\nrequest: {{ method: 'POST', path: '/api/v1/login' }}\nresponse: {{ status: 200, body: 'ok' }}").unwrap();
|
let mut file1 = File::create(user_dir.join("get_profile.yaml")).unwrap();
|
||||||
|
writeln!(
|
||||||
|
file1,
|
||||||
|
r#"
|
||||||
|
name: "user_profile"
|
||||||
|
request:
|
||||||
|
method: "GET"
|
||||||
|
path: "/api/v1/user/profile"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body: "profile_data"
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut f2 = File::create(root_path.join("sys.yaml")).unwrap();
|
// 在根目录创建多接口文件
|
||||||
writeln!(f2, "- id: 's1'\n request: {{ method: 'GET', path: '/health' }}\n response: {{ status: 200, body: 'up' }}").unwrap();
|
let mut file2 = File::create(root_path.join("system.yaml")).unwrap();
|
||||||
|
writeln!(
|
||||||
|
file2,
|
||||||
|
r#"
|
||||||
|
- name: "sys_health"
|
||||||
|
request: {{ method: "GET", path: "/health" }}
|
||||||
|
response: {{ status: 200, body: "ok" }}
|
||||||
|
- name: "sys_version"
|
||||||
|
request: {{ method: "GET", path: "/version" }}
|
||||||
|
response: {{ status: 200, body: "1.0.0" }}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 执行加载
|
||||||
let index = MockLoader::load_all_from_dir(root_path);
|
let index = MockLoader::load_all_from_dir(root_path);
|
||||||
|
|
||||||
assert!(index.contains_key("api"));
|
// 断言结果:
|
||||||
assert!(index.contains_key("health"));
|
// 1. 检查 Key 是否根据路径首段正确提取
|
||||||
let total: usize = index.values().map(|v| v.len()).sum();
|
assert!(index.contains_key("api"), "索引应包含 'api' 键");
|
||||||
assert_eq!(total, 2);
|
assert!(index.contains_key("health"), "索引应包含 'health' 键");
|
||||||
}
|
assert!(index.contains_key("version"), "索引应包含 'version' 键");
|
||||||
|
|
||||||
#[test]
|
// 2. 检查规则总数
|
||||||
fn test_router_matching_logic() {
|
let total_rules: usize = index.values().map(|v| v.len()).sum();
|
||||||
let mut index = HashMap::new();
|
assert_eq!(total_rules, 3, "总规则数应为 3");
|
||||||
|
|
||||||
// 1. 准备带有 Body 的规则
|
// 3. 检查深层文件是否被正确读取
|
||||||
let rule_auth = serde_yaml::from_str::<MockSource>(
|
let api_rules = &index["api"];
|
||||||
r#"
|
assert_eq!(api_rules[0].name, "user_profile");
|
||||||
id: "auth_v1"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/login"
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
body: { "code": 123 }
|
|
||||||
response: { status: 200, body: "token_123" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule_auth[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 2. 测试场景 A:完全匹配 (包括 Body)
|
|
||||||
let mut headers = HashMap::new();
|
|
||||||
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
|
||||||
|
|
||||||
// 构造请求 Body
|
|
||||||
let incoming_body = Some(json!({ "code": 123 }));
|
|
||||||
|
|
||||||
let matched = router.match_rule(
|
|
||||||
"POST",
|
|
||||||
"/api/v1/login",
|
|
||||||
&HashMap::new(),
|
|
||||||
&headers,
|
|
||||||
&incoming_body // 传入新参数
|
|
||||||
);
|
|
||||||
assert!(matched.is_some());
|
|
||||||
assert_eq!(matched.unwrap().id, "auth_v1");
|
|
||||||
|
|
||||||
// 3. 测试场景 B:Body 不匹配
|
|
||||||
let wrong_body = Some(json!({ "code": 456 }));
|
|
||||||
let matched_fail = router.match_rule(
|
|
||||||
"POST",
|
|
||||||
"/api/v1/login",
|
|
||||||
&HashMap::new(),
|
|
||||||
&headers,
|
|
||||||
&wrong_body
|
|
||||||
);
|
|
||||||
assert!(matched_fail.is_none(), "Body 不一致时不应匹配成功");
|
|
||||||
|
|
||||||
// 4. 测试场景 C:智能字符串转换验证 (YAML 是字符串,请求是对象)
|
|
||||||
let rule_str_body = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "str_match"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/str"
|
|
||||||
body: '{"type": "json_in_string"}' # 这里 YAML 解析为 Value::String
|
|
||||||
response: { status: 200, body: "ok" }
|
|
||||||
"#).unwrap().flatten();
|
|
||||||
|
|
||||||
let mut index2 = HashMap::new();
|
|
||||||
index2.insert("api".to_string(), vec![rule_str_body[0].clone()]);
|
|
||||||
let router2 = MockRouter::new(index2);
|
|
||||||
|
|
||||||
let incoming_obj = Some(json!({ "type": "json_in_string" })); // 请求是 JSON 对象
|
|
||||||
let matched_str = router2.match_rule(
|
|
||||||
"POST",
|
|
||||||
"/api/str",
|
|
||||||
&HashMap::new(),
|
|
||||||
&HashMap::new(),
|
|
||||||
&incoming_obj
|
|
||||||
);
|
|
||||||
assert!(matched_str.is_some(), "应该支持将 YAML 字符串 body 转换为对象进行匹配");
|
|
||||||
|
|
||||||
// 5. 测试场景 D:末尾斜杠兼容性测试
|
|
||||||
let matched_slash = router.match_rule(
|
|
||||||
"POST",
|
|
||||||
"/api/v1/login/",
|
|
||||||
&HashMap::new(),
|
|
||||||
&headers,
|
|
||||||
&incoming_body
|
|
||||||
);
|
|
||||||
assert!(matched_slash.is_some());
|
|
||||||
}
|
}
|
||||||
@@ -1,114 +1,425 @@
|
|||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
// 假设你的项目名在 Cargo.toml 中叫 mock_server
|
use serde_json::json;
|
||||||
use mock_server::config::MockSource;
|
use mock_server::models::{MockRule, MockSource, Payload};
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
|
use mock_server::router::MockRouter;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// 模块一:验证 Config 反序列化逻辑
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_deserialization() {
|
fn test_config_parsing_scenarios() {
|
||||||
// 测试 1:验证单接口 YAML 解析
|
// 场景 A: 验证单接口配置 (增加 body 结构化校验)
|
||||||
let single_yaml = r#"
|
let yaml_single = r#"
|
||||||
id: "auth_v1"
|
name: "auth_v1"
|
||||||
request:
|
request:
|
||||||
method: "POST"
|
method: "POST"
|
||||||
path: "/api/v1/login"
|
path: "/api/v1/login"
|
||||||
response:
|
body: { "user": "admin" }
|
||||||
status: 200
|
response: { status: 200, body: "welcome" }
|
||||||
body: "inline_content"
|
|
||||||
"#;
|
"#;
|
||||||
let res: MockSource = serde_yaml::from_str(single_yaml).expect("应该成功解析单接口");
|
let source_s: MockSource = serde_yaml::from_str(yaml_single).expect("解析单接口失败");
|
||||||
assert_eq!(res.flatten().len(), 1);
|
let rules = source_s.flatten();
|
||||||
// assert_eq!(res.flatten()[0].id, "auth_v1");
|
assert_eq!(rules.len(), 1);
|
||||||
|
// 验证 body 是否被成功解析为 Value::Object
|
||||||
|
assert!(rules[0].request.body.is_some());
|
||||||
|
assert_eq!(rules[0].request.body.as_ref().unwrap()["user"], "admin");
|
||||||
|
|
||||||
// 测试 2:验证多接口 YAML 数组解析
|
// 场景 B: 验证多接口配置
|
||||||
let multi_yaml = r#"
|
let yaml_multi = r#"
|
||||||
- id: "api_1"
|
- name: "api_1"
|
||||||
request: { method: "GET", path: "/1" }
|
request: { method: "GET", path: "/health" }
|
||||||
response: { status: 200, body: "b1" }
|
response: { status: 200, body: "ok" }
|
||||||
- id: "api_2"
|
- name: "api_2"
|
||||||
request: { method: "GET", path: "/2" }
|
request: { method: "GET", path: "/version" }
|
||||||
response: { status: 200, body: "b2" }
|
response: { status: 200, body: "1.0" }
|
||||||
"#;
|
"#;
|
||||||
let res_multi: MockSource = serde_yaml::from_str(multi_yaml).expect("应该成功解析接口数组");
|
let source_m: MockSource = serde_yaml::from_str(yaml_multi).expect("解析多接口失败");
|
||||||
assert_eq!(res_multi.flatten().len(), 2);
|
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 递归扫描与索引构建
|
||||||
#[test]
|
#[test]
|
||||||
fn test_recursive_loading_logic() {
|
fn test_loader_recursive_indexing() {
|
||||||
// 创建临时 Mock 目录结构
|
let temp_root = tempdir().expect("无法创建临时目录");
|
||||||
let root_dir = tempdir().expect("创建临时目录失败");
|
let root_path = temp_root.path();
|
||||||
let root_path = root_dir.path();
|
|
||||||
|
|
||||||
// 构造物理层级:mocks/v1/user/
|
let auth_path = root_path.join("v1/auth");
|
||||||
let user_dir = root_path.join("v1/user");
|
fs::create_dir_all(&auth_path).unwrap();
|
||||||
fs::create_dir_all(&user_dir).unwrap();
|
|
||||||
|
|
||||||
// 在深层目录创建单接口文件
|
let mut f1 = File::create(auth_path.join("single_login.yaml")).unwrap();
|
||||||
let mut file1 = File::create(user_dir.join("get_profile.yaml")).unwrap();
|
writeln!(f1, "name: 'l1'\nrequest: {{ method: 'POST', path: '/api/v1/login' }}\nresponse: {{ status: 200, body: 'ok' }}").unwrap();
|
||||||
writeln!(
|
|
||||||
file1,
|
|
||||||
r#"
|
|
||||||
id: "user_profile"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/user/profile"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "profile_data"
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// 在根目录创建多接口文件
|
let mut f2 = File::create(root_path.join("sys.yaml")).unwrap();
|
||||||
let mut file2 = File::create(root_path.join("system.yaml")).unwrap();
|
writeln!(f2, "- name: 's1'\n request: {{ method: 'GET', path: '/health' }}\n response: {{ status: 200, body: 'up' }}").unwrap();
|
||||||
writeln!(
|
|
||||||
file2,
|
|
||||||
r#"
|
|
||||||
- id: "sys_health"
|
|
||||||
request: {{ method: "GET", path: "/health" }}
|
|
||||||
response: {{ status: 200, body: "ok" }}
|
|
||||||
- id: "sys_version"
|
|
||||||
request: {{ method: "GET", path: "/version" }}
|
|
||||||
response: {{ status: 200, body: "1.0.0" }}
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// writeln!(
|
|
||||||
// file2,
|
|
||||||
// r#"
|
|
||||||
// - id: "sys_health"
|
|
||||||
// request:
|
|
||||||
// method: "GET"
|
|
||||||
// path: "/health"
|
|
||||||
// response:
|
|
||||||
// status: 200
|
|
||||||
// body: "ok"
|
|
||||||
// - id: "sys_version"
|
|
||||||
// request:
|
|
||||||
// method: "GET"
|
|
||||||
// path: "/version"
|
|
||||||
// response:
|
|
||||||
// status: 200
|
|
||||||
// body: "1.0.0"
|
|
||||||
// "#
|
|
||||||
// )
|
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
// 执行加载
|
|
||||||
let index = MockLoader::load_all_from_dir(root_path);
|
let index = MockLoader::load_all_from_dir(root_path);
|
||||||
|
|
||||||
// 断言结果:
|
assert!(index.contains_key("api"));
|
||||||
// 1. 检查 Key 是否根据路径首段正确提取(/api/v1/... -> api, /health -> health)
|
assert!(index.contains_key("health"));
|
||||||
assert!(index.contains_key("api"), "索引应包含 'api' 键");
|
let total: usize = index.values().map(|v| v.len()).sum();
|
||||||
assert!(index.contains_key("health"), "索引应包含 'health' 键");
|
assert_eq!(total, 2);
|
||||||
assert!(index.contains_key("version"), "索引应包含 'version' 键");
|
}
|
||||||
|
|
||||||
// 2. 检查规则总数
|
/// 模块三:验证 Router 匹配逻辑(使用新的 Payload 类型)
|
||||||
let total_rules: usize = index.values().map(|v| v.len()).sum();
|
#[test]
|
||||||
assert_eq!(total_rules, 3, "总规则数应为 3");
|
fn test_router_matching_logic() {
|
||||||
|
let mut index = HashMap::new();
|
||||||
// 3. 检查深层文件是否被正确读取
|
|
||||||
let api_rules = &index["api"];
|
// 1. 准备带有 Body 的规则
|
||||||
assert_eq!(api_rules[0].id, "user_profile");
|
let rule_auth = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "auth_v1"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/v1/login"
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
body: { "code": 123 }
|
||||||
|
response: { status: 200, body: "token_123" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
index.insert("api".to_string(), vec![rule_auth[0].clone()]);
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
// 2. 测试场景 A:JSON 完全匹配
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
|
let payload = Payload::Json(json!({ "code": 123 }));
|
||||||
|
|
||||||
|
let matched = router.match_rule(
|
||||||
|
"POST",
|
||||||
|
"/api/v1/login",
|
||||||
|
&HashMap::new(),
|
||||||
|
&headers,
|
||||||
|
&payload,
|
||||||
|
);
|
||||||
|
assert!(matched.is_some());
|
||||||
|
assert_eq!(matched.unwrap().name, "auth_v1");
|
||||||
|
|
||||||
|
// 3. 测试场景 B:Body 不匹配
|
||||||
|
let wrong_payload = Payload::Json(json!({ "code": 456 }));
|
||||||
|
let matched_fail = router.match_rule(
|
||||||
|
"POST",
|
||||||
|
"/api/v1/login",
|
||||||
|
&HashMap::new(),
|
||||||
|
&headers,
|
||||||
|
&wrong_payload,
|
||||||
|
);
|
||||||
|
assert!(matched_fail.is_none(), "Body 不一致时不应匹配成功");
|
||||||
|
|
||||||
|
// 4. 测试场景 C:末尾斜杠兼容性测试
|
||||||
|
let matched_slash = router.match_rule(
|
||||||
|
"POST",
|
||||||
|
"/api/v1/login/",
|
||||||
|
&HashMap::new(),
|
||||||
|
&headers,
|
||||||
|
&payload,
|
||||||
|
);
|
||||||
|
assert!(matched_slash.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模块四:验证不同 Payload 类型的匹配
|
||||||
|
#[test]
|
||||||
|
fn test_payload_type_matching() {
|
||||||
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
|
// XML 规则
|
||||||
|
let xml_rule = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "xml_api"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/xml"
|
||||||
|
body: "<root><name>test</name></root>"
|
||||||
|
response: { status: 200, body: "ok" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
// Form 规则
|
||||||
|
let form_rule = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "form_api"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/form"
|
||||||
|
body: { "username": "admin", "password": "123456" }
|
||||||
|
response: { status: 200, body: "login_ok" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
// Text 规则
|
||||||
|
let text_rule = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "text_api"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/text"
|
||||||
|
body: "plain text content"
|
||||||
|
response: { status: 200, body: "text_ok" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
index.insert("api".to_string(), vec![xml_rule[0].clone(), form_rule[0].clone(), text_rule[0].clone()]);
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
// 测试 XML 匹配
|
||||||
|
let xml_payload = Payload::Xml("<root><name>test</name></root>".to_string());
|
||||||
|
let xml_matched = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &xml_payload);
|
||||||
|
assert!(xml_matched.is_some(), "XML 应该匹配");
|
||||||
|
assert_eq!(xml_matched.unwrap().name, "xml_api");
|
||||||
|
|
||||||
|
// 测试 Form 匹配
|
||||||
|
let mut form_data = HashMap::new();
|
||||||
|
form_data.insert("username".to_string(), "admin".to_string());
|
||||||
|
form_data.insert("password".to_string(), "123456".to_string());
|
||||||
|
let form_payload = Payload::Form(form_data);
|
||||||
|
let form_matched = router.match_rule("POST", "/api/form", &HashMap::new(), &HashMap::new(), &form_payload);
|
||||||
|
assert!(form_matched.is_some(), "Form 应该匹配");
|
||||||
|
assert_eq!(form_matched.unwrap().name, "form_api");
|
||||||
|
|
||||||
|
// 测试 Text 匹配
|
||||||
|
let text_payload = Payload::Text("plain text content".to_string());
|
||||||
|
let text_matched = router.match_rule("POST", "/api/text", &HashMap::new(), &HashMap::new(), &text_payload);
|
||||||
|
assert!(text_matched.is_some(), "Text 应该匹配");
|
||||||
|
assert_eq!(text_matched.unwrap().name, "text_api");
|
||||||
|
|
||||||
|
// 测试 None 不匹配
|
||||||
|
let none_payload = Payload::None;
|
||||||
|
let none_matched = router.match_rule("POST", "/api/text", &HashMap::new(), &HashMap::new(), &none_payload);
|
||||||
|
assert!(none_matched.is_none(), "None 不应该匹配有 body 的规则");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模块五:验证 Payload 方法
|
||||||
|
#[test]
|
||||||
|
fn test_payload_methods() {
|
||||||
|
// 测试 content_type_name
|
||||||
|
assert_eq!(Payload::Json(json!({})).content_type_name(), "application/json");
|
||||||
|
assert_eq!(Payload::Xml("".to_string()).content_type_name(), "application/xml");
|
||||||
|
assert_eq!(Payload::Form(HashMap::new()).content_type_name(), "application/x-www-form-urlencoded");
|
||||||
|
assert_eq!(Payload::Multipart(HashMap::new()).content_type_name(), "multipart/form-data");
|
||||||
|
assert_eq!(Payload::Text("".to_string()).content_type_name(), "text/plain");
|
||||||
|
assert_eq!(Payload::None.content_type_name(), "none");
|
||||||
|
|
||||||
|
// 测试 to_compare_string
|
||||||
|
let json_payload = Payload::Json(json!({"a": 1, "b": 2}));
|
||||||
|
assert!(json_payload.to_compare_string().contains("\"a\":1"));
|
||||||
|
|
||||||
|
let text_payload = Payload::Text("hello world".to_string());
|
||||||
|
assert_eq!(text_payload.to_compare_string(), "hello world");
|
||||||
|
|
||||||
|
let none_payload = Payload::None;
|
||||||
|
assert_eq!(none_payload.to_compare_string(), "");
|
||||||
|
|
||||||
|
// 测试 Form 排序后的字符串
|
||||||
|
let mut form_map = HashMap::new();
|
||||||
|
form_map.insert("b".to_string(), "2".to_string());
|
||||||
|
form_map.insert("a".to_string(), "1".to_string());
|
||||||
|
let form_payload = Payload::Form(form_map);
|
||||||
|
assert_eq!(form_payload.to_compare_string(), "a=1&b=2");
|
||||||
|
|
||||||
|
// 测试 Multipart 排序后的字符串
|
||||||
|
let mut multipart_map = HashMap::new();
|
||||||
|
multipart_map.insert("b".to_string(), "2".to_string());
|
||||||
|
multipart_map.insert("a".to_string(), "1".to_string());
|
||||||
|
let multipart_payload = Payload::Multipart(multipart_map);
|
||||||
|
assert_eq!(multipart_payload.to_compare_string(), "a=1&b=2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模块六:验证 Multipart 匹配
|
||||||
|
#[test]
|
||||||
|
fn test_multipart_matching() {
|
||||||
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
|
// 数组形式:只匹配字段名
|
||||||
|
let array_rule = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "upload_array"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/upload/array"
|
||||||
|
body: ["file", "description"]
|
||||||
|
response: { status: 200, body: "ok" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
// 对象形式:匹配键值对
|
||||||
|
let object_rule = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "upload_object"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/upload/object"
|
||||||
|
body: { "username": "admin", "role": "user" }
|
||||||
|
response: { status: 200, body: "ok" }
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
index.insert("api".to_string(), vec![array_rule[0].clone(), object_rule[0].clone()]);
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
// 测试数组形式:匹配字段名
|
||||||
|
let mut full_data = HashMap::new();
|
||||||
|
full_data.insert("file".to_string(), "file_content".to_string());
|
||||||
|
full_data.insert("description".to_string(), "test file".to_string());
|
||||||
|
let full_payload = Payload::Multipart(full_data);
|
||||||
|
let matched = router.match_rule("POST", "/api/upload/array", &HashMap::new(), &HashMap::new(), &full_payload);
|
||||||
|
assert!(matched.is_some(), "包含所有字段应该匹配");
|
||||||
|
|
||||||
|
// 测试数组形式:缺少字段
|
||||||
|
let mut partial_data = HashMap::new();
|
||||||
|
partial_data.insert("file".to_string(), "file_content".to_string());
|
||||||
|
let partial_payload = Payload::Multipart(partial_data);
|
||||||
|
let not_matched = router.match_rule("POST", "/api/upload/array", &HashMap::new(), &HashMap::new(), &partial_payload);
|
||||||
|
assert!(not_matched.is_none(), "缺少字段不应该匹配");
|
||||||
|
|
||||||
|
// 测试对象形式:键名匹配(值被忽略)
|
||||||
|
let mut correct_data = HashMap::new();
|
||||||
|
correct_data.insert("username".to_string(), "any_value".to_string()); // 值不重要
|
||||||
|
correct_data.insert("role".to_string(), "any_role".to_string());
|
||||||
|
let correct_payload = Payload::Multipart(correct_data);
|
||||||
|
let object_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &correct_payload);
|
||||||
|
assert!(object_matched.is_some(), "键名匹配应该成功");
|
||||||
|
|
||||||
|
// 测试对象形式:键名不匹配
|
||||||
|
let mut wrong_data = HashMap::new();
|
||||||
|
wrong_data.insert("username".to_string(), "admin".to_string());
|
||||||
|
wrong_data.insert("other_field".to_string(), "value".to_string()); // 缺少 role 字段
|
||||||
|
let wrong_payload = Payload::Multipart(wrong_data);
|
||||||
|
let wrong_matched = router.match_rule("POST", "/api/upload/object", &HashMap::new(), &HashMap::new(), &wrong_payload);
|
||||||
|
assert!(wrong_matched.is_none(), "缺少键名不应该成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模块七:验证 YAML 块语法(折叠 `>` 和字面量 `|`)
|
||||||
|
#[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]
|
||||||
|
fn test_xml_normalized_matching() {
|
||||||
|
let mut index = HashMap::new();
|
||||||
|
|
||||||
|
// YAML 中的 XML(带格式化)
|
||||||
|
let xml_rule = serde_yaml::from_str::<MockSource>(
|
||||||
|
r#"
|
||||||
|
name: "xml_api"
|
||||||
|
request:
|
||||||
|
method: "POST"
|
||||||
|
path: "/api/xml"
|
||||||
|
body: |
|
||||||
|
<user>
|
||||||
|
<id>1001</id>
|
||||||
|
<name>张三</name>
|
||||||
|
</user>
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body: "ok"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
index.insert("api".to_string(), vec![xml_rule[0].clone()]);
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
// 测试 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 matched2 = router.match_rule("POST", "/api/xml", &HashMap::new(), &HashMap::new(), &compact_xml);
|
||||||
|
assert!(matched2.is_some(), "紧凑格式 XML 应该匹配");
|
||||||
|
|
||||||
|
// 测试 3:带 XML 声明的请求
|
||||||
|
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);
|
||||||
|
assert!(matched3.is_some(), "带声明的 XML 应该匹配");
|
||||||
|
|
||||||
|
// 测试 4:内容不同,不应该匹配
|
||||||
|
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);
|
||||||
|
assert!(matched4.is_none(), "内容不同的 XML 不应该匹配");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,354 +0,0 @@
|
|||||||
use mock_server::config::MockSource;
|
|
||||||
use mock_server::router::MockRouter;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_basic_method_path_matching() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "basic_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/test"
|
|
||||||
response: { status: 200, body: "ok" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试精确匹配
|
|
||||||
let matched = router.match_rule("GET", "/api/test", &HashMap::new(), &HashMap::new(), &None);
|
|
||||||
assert!(matched.is_some());
|
|
||||||
assert_eq!(matched.unwrap().id, "basic_test");
|
|
||||||
|
|
||||||
// 测试方法不匹配
|
|
||||||
let not_matched = router.match_rule("POST", "/api/test", &HashMap::new(), &HashMap::new(), &None);
|
|
||||||
assert!(not_matched.is_none());
|
|
||||||
|
|
||||||
// 测试路径不匹配
|
|
||||||
let not_matched = router.match_rule("GET", "/api/other", &HashMap::new(), &HashMap::new(), &None);
|
|
||||||
assert!(not_matched.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_case_insensitive_method() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "case_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/data"
|
|
||||||
response: { status: 200, body: "data" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试不同大小写的方法匹配
|
|
||||||
assert!(router.match_rule("get", "/api/data", &HashMap::new(), &HashMap::new(), &None).is_some());
|
|
||||||
assert!(router.match_rule("GET", "/api/data", &HashMap::new(), &HashMap::new(), &None).is_some());
|
|
||||||
assert!(router.match_rule("Get", "/api/data", &HashMap::new(), &HashMap::new(), &None).is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_path_trailing_slash() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "slash_test"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/users"
|
|
||||||
response: { status: 200, body: "created" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试带和不带斜杠的路径匹配
|
|
||||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &None).is_some());
|
|
||||||
assert!(router.match_rule("POST", "/api/users/", &HashMap::new(), &HashMap::new(), &None).is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_query_params_matching() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "query_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/search"
|
|
||||||
query_params:
|
|
||||||
q: "rust"
|
|
||||||
limit: "10"
|
|
||||||
response: { status: 200, body: "results" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试查询参数完全匹配
|
|
||||||
let mut queries = HashMap::new();
|
|
||||||
queries.insert("q".to_string(), "rust".to_string());
|
|
||||||
queries.insert("limit".to_string(), "10".to_string());
|
|
||||||
queries.insert("extra".to_string(), "value".to_string()); // 额外参数应该忽略
|
|
||||||
|
|
||||||
assert!(router.match_rule("GET", "/api/search", &queries, &HashMap::new(), &None).is_some());
|
|
||||||
|
|
||||||
// 测试查询参数不匹配
|
|
||||||
let mut wrong_queries = HashMap::new();
|
|
||||||
wrong_queries.insert("q".to_string(), "python".to_string());
|
|
||||||
|
|
||||||
assert!(router.match_rule("GET", "/api/search", &wrong_queries, &HashMap::new(), &None).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_headers_matching() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "header_test"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/data"
|
|
||||||
headers:
|
|
||||||
Authorization: "Bearer token123"
|
|
||||||
Content-Type: "application/json"
|
|
||||||
response: { status: 200, body: "data" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试头部完全匹配(大小写不敏感)
|
|
||||||
let mut headers = HashMap::new();
|
|
||||||
headers.insert("authorization".to_string(), "Bearer token123".to_string());
|
|
||||||
headers.insert("content-type".to_string(), "application/json".to_string());
|
|
||||||
headers.insert("extra-header".to_string(), "value".to_string());
|
|
||||||
|
|
||||||
assert!(router.match_rule("GET", "/api/data", &HashMap::new(), &headers, &None).is_some());
|
|
||||||
|
|
||||||
// 测试头部不匹配
|
|
||||||
let mut wrong_headers = HashMap::new();
|
|
||||||
wrong_headers.insert("authorization".to_string(), "Bearer wrong".to_string());
|
|
||||||
|
|
||||||
assert!(router.match_rule("GET", "/api/data", &HashMap::new(), &wrong_headers, &None).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_body_matching() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "body_test"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/users"
|
|
||||||
body:
|
|
||||||
name: "John"
|
|
||||||
age: 30
|
|
||||||
response: { status: 200, body: "created" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试 Body 完全匹配
|
|
||||||
let body = Some(json!({ "name": "John", "age": 30 }));
|
|
||||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &body).is_some());
|
|
||||||
|
|
||||||
// 测试 Body 不匹配
|
|
||||||
let wrong_body = Some(json!({ "name": "Jane", "age": 25 }));
|
|
||||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &wrong_body).is_none());
|
|
||||||
|
|
||||||
// 测试请求无 Body
|
|
||||||
assert!(router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &None).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_body_string_to_json() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
// YAML 中 body 是字符串,但内容是 JSON
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "str_body_test"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/login"
|
|
||||||
body: '{"username": "admin", "password": "secret"}'
|
|
||||||
response: { status: 200, body: "token" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 请求中 body 是 JSON 对象,应该匹配
|
|
||||||
let body = Some(json!({ "username": "admin", "password": "secret" }));
|
|
||||||
assert!(router.match_rule("POST", "/api/login", &HashMap::new(), &HashMap::new(), &body).is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_multiple_rules_same_segment() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule1 = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "rule1"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/users"
|
|
||||||
response: { status: 200, body: "users" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let rule2 = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "rule2"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/users"
|
|
||||||
response: { status: 201, body: "created" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule1[0].clone(), rule2[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试不同方法匹配不同规则
|
|
||||||
let get_matched = router.match_rule("GET", "/api/users", &HashMap::new(), &HashMap::new(), &None);
|
|
||||||
assert!(get_matched.is_some());
|
|
||||||
assert_eq!(get_matched.unwrap().id, "rule1");
|
|
||||||
|
|
||||||
let post_matched = router.match_rule("POST", "/api/users", &HashMap::new(), &HashMap::new(), &None);
|
|
||||||
assert!(post_matched.is_some());
|
|
||||||
assert_eq!(post_matched.unwrap().id, "rule2");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_complex_matching_scenario() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let complex_rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "complex_rule"
|
|
||||||
request:
|
|
||||||
method: "PUT"
|
|
||||||
path: "/api/users/123"
|
|
||||||
query_params:
|
|
||||||
update: "true"
|
|
||||||
headers:
|
|
||||||
Authorization: "Bearer token456"
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body:
|
|
||||||
name: "Updated"
|
|
||||||
response: { status: 200, body: "updated" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![complex_rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试完全匹配所有条件
|
|
||||||
let mut queries = HashMap::new();
|
|
||||||
queries.insert("update".to_string(), "true".to_string());
|
|
||||||
|
|
||||||
let mut headers = HashMap::new();
|
|
||||||
headers.insert("authorization".to_string(), "Bearer token456".to_string());
|
|
||||||
headers.insert("content-type".to_string(), "application/json".to_string());
|
|
||||||
|
|
||||||
let body = Some(json!({ "name": "Updated" }));
|
|
||||||
|
|
||||||
let matched = router.match_rule("PUT", "/api/users/123", &queries, &headers, &body);
|
|
||||||
assert!(matched.is_some());
|
|
||||||
assert_eq!(matched.unwrap().id, "complex_rule");
|
|
||||||
|
|
||||||
// 测试缺少任何条件都不匹配
|
|
||||||
let no_body_matched = router.match_rule("PUT", "/api/users/123", &queries, &headers, &None);
|
|
||||||
assert!(no_body_matched.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_root_path_handling() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let root_rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "root_rule"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/"
|
|
||||||
response: { status: 200, body: "root" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
// 根路径会被提取为空字符串 key
|
|
||||||
index.insert("".to_string(), vec![root_rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试根路径匹配
|
|
||||||
assert!(router.match_rule("GET", "/", &HashMap::new(), &HashMap::new(), &None).is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_router_nonexistent_key() {
|
|
||||||
let mut index = HashMap::new();
|
|
||||||
|
|
||||||
let rule = serde_yaml::from_str::<MockSource>(
|
|
||||||
r#"
|
|
||||||
id: "test_rule"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/data"
|
|
||||||
response: { status: 200, body: "data" }
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
index.insert("api".to_string(), vec![rule[0].clone()]);
|
|
||||||
let router = MockRouter::new(index);
|
|
||||||
|
|
||||||
// 测试请求不同段路径,应该不匹配
|
|
||||||
assert!(router.match_rule("GET", "/health", &HashMap::new(), &HashMap::new(), &None).is_none());
|
|
||||||
assert!(router.match_rule("GET", "/v2/data", &HashMap::new(), &HashMap::new(), &None).is_none());
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
};
|
|
||||||
use mock_server::upload::{UploadResult, FileInfo};
|
|
||||||
use tempfile::TempDir;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
|
|
||||||
/// 创建模拟的 multipart 数据
|
|
||||||
fn create_multipart_body(boundary: &str, files: Vec<(&str, &str, &[u8])>) -> Vec<u8> {
|
|
||||||
let mut body = Vec::new();
|
|
||||||
|
|
||||||
for (field_name, filename, content) in files {
|
|
||||||
body.extend_from_slice(
|
|
||||||
format!(
|
|
||||||
"--{}\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n\r\n",
|
|
||||||
boundary, field_name, filename
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
);
|
|
||||||
body.extend_from_slice(content);
|
|
||||||
body.extend_from_slice(b"\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
|
|
||||||
body
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_upload_result_success() {
|
|
||||||
let file_info = FileInfo {
|
|
||||||
original_name: "test.txt".to_string(),
|
|
||||||
stored_name: "uuid-test.txt".to_string(),
|
|
||||||
path: "storage/2026-03-19/uuid-test.txt".to_string(),
|
|
||||||
size: 1024,
|
|
||||||
content_type: Some("text/plain".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = UploadResult::success(vec![file_info]);
|
|
||||||
|
|
||||||
// 验证序列化
|
|
||||||
let json = serde_json::to_string(&result).unwrap();
|
|
||||||
assert!(json.contains("test.txt"));
|
|
||||||
assert!(json.contains("success"));
|
|
||||||
assert!(json.contains("1024"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_upload_result_error() {
|
|
||||||
let result = UploadResult::error("Test error message".to_string());
|
|
||||||
|
|
||||||
assert!(!result.success);
|
|
||||||
assert_eq!(result.message, "Test error message");
|
|
||||||
assert!(result.files.is_empty());
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&result).unwrap();
|
|
||||||
assert!(json.contains("Test error message"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_info_serialization() {
|
|
||||||
let file_info = FileInfo {
|
|
||||||
original_name: "document.pdf".to_string(),
|
|
||||||
stored_name: "550e8400-e29b-41d4-a716-446655440000.pdf".to_string(),
|
|
||||||
path: "storage/2026-03-19/550e8400-e29b-41d4-a716-446655440000.pdf".to_string(),
|
|
||||||
size: 2048,
|
|
||||||
content_type: Some("application/pdf".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&file_info).unwrap();
|
|
||||||
|
|
||||||
assert!(json.contains("document.pdf"));
|
|
||||||
assert!(json.contains("2048"));
|
|
||||||
assert!(json.contains("application/pdf"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_upload_result_into_response() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let result = UploadResult::success(vec![]);
|
|
||||||
let response = result.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
|
|
||||||
let result_error = UploadResult::error("Error".to_string());
|
|
||||||
let response_error = result_error.into_response();
|
|
||||||
|
|
||||||
assert_eq!(response_error.status(), StatusCode::BAD_REQUEST);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_storage_directory_creation() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let storage_path = temp_dir.path().join("storage");
|
|
||||||
|
|
||||||
// 测试目录创建
|
|
||||||
tokio::fs::create_dir_all(&storage_path).await.unwrap();
|
|
||||||
assert!(storage_path.exists());
|
|
||||||
|
|
||||||
// 测试日期目录创建
|
|
||||||
let date_dir_name = chrono::Local::now().format("%Y-%m-%d").to_string();
|
|
||||||
let date_dir = storage_path.join(&date_dir_name);
|
|
||||||
tokio::fs::create_dir_all(&date_dir).await.unwrap();
|
|
||||||
assert!(date_dir.exists());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_save_and_read() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let file_path = temp_dir.path().join("test_file.txt");
|
|
||||||
|
|
||||||
let content = b"Test file content";
|
|
||||||
|
|
||||||
// 写入文件
|
|
||||||
tokio::fs::write(&file_path, content).await.unwrap();
|
|
||||||
assert!(file_path.exists());
|
|
||||||
|
|
||||||
// 读取文件
|
|
||||||
let read_content = tokio::fs::read(&file_path).await.unwrap();
|
|
||||||
assert_eq!(read_content, content);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multipart_body_creation() {
|
|
||||||
let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
|
|
||||||
let content = b"Hello, World!";
|
|
||||||
|
|
||||||
let body = create_multipart_body(boundary, vec![("file", "test.txt", content)]);
|
|
||||||
|
|
||||||
let body_str = String::from_utf8_lossy(&body);
|
|
||||||
|
|
||||||
assert!(body_str.contains(boundary));
|
|
||||||
assert!(body_str.contains("test.txt"));
|
|
||||||
assert!(body_str.contains("Hello, World!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_files_multipart() {
|
|
||||||
let boundary = "----WebKitFormBoundary";
|
|
||||||
let files = vec![
|
|
||||||
("file1", "a.txt", b"content a" as &[u8]),
|
|
||||||
("file2", "b.txt", b"content b" as &[u8]),
|
|
||||||
];
|
|
||||||
|
|
||||||
let body = create_multipart_body(boundary, files);
|
|
||||||
let body_str = String::from_utf8_lossy(&body);
|
|
||||||
|
|
||||||
assert!(body_str.contains("a.txt"));
|
|
||||||
assert!(body_str.contains("b.txt"));
|
|
||||||
assert!(body_str.contains("content a"));
|
|
||||||
assert!(body_str.contains("content b"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_uuid_generation() {
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
let uuid1 = Uuid::new_v4();
|
|
||||||
let uuid2 = Uuid::new_v4();
|
|
||||||
|
|
||||||
// UUID 应该是唯一的
|
|
||||||
assert_ne!(uuid1, uuid2);
|
|
||||||
|
|
||||||
// UUID 应该是有效的
|
|
||||||
assert!(!uuid1.is_nil());
|
|
||||||
assert!(!uuid2.is_nil());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_date_directory_format() {
|
|
||||||
let date_dir_name = chrono::Local::now().format("%Y-%m-%d").to_string();
|
|
||||||
|
|
||||||
// 验证日期格式
|
|
||||||
assert!(date_dir_name.contains('-'));
|
|
||||||
let parts: Vec<&str> = date_dir_name.split('-').collect();
|
|
||||||
assert_eq!(parts.len(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_extension_extraction() {
|
|
||||||
let test_cases = vec![
|
|
||||||
("test.txt", ".txt"),
|
|
||||||
("document.pdf", ".pdf"),
|
|
||||||
("image.png", ".png"),
|
|
||||||
("archive.tar.gz", ".gz"),
|
|
||||||
("noextension", ""),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (filename, expected_ext) in test_cases {
|
|
||||||
let extension = std::path::Path::new(filename)
|
|
||||||
.extension()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.map(|s| format!(".{}", s))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
assert_eq!(extension, expected_ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_large_file_content() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let file_path = temp_dir.path().join("large_file.bin");
|
|
||||||
|
|
||||||
// 创建 1MB 的数据
|
|
||||||
let large_content = vec![0u8; 1024 * 1024];
|
|
||||||
|
|
||||||
tokio::fs::write(&file_path, &large_content).await.unwrap();
|
|
||||||
|
|
||||||
let file_size = tokio::fs::metadata(&file_path).await.unwrap().len();
|
|
||||||
assert_eq!(file_size, 1024 * 1024);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_binary_file_content() {
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let file_path = temp_dir.path().join("binary.bin");
|
|
||||||
|
|
||||||
// 创建二进制数据
|
|
||||||
let binary_data: Vec<u8> = (0..=255).collect();
|
|
||||||
|
|
||||||
tokio::fs::write(&file_path, &binary_data).await.unwrap();
|
|
||||||
|
|
||||||
let read_data = tokio::fs::read(&file_path).await.unwrap();
|
|
||||||
assert_eq!(read_data.len(), 256);
|
|
||||||
assert_eq!(read_data, binary_data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
238
tests/v2_integration_test.rs
Normal file
238
tests/v2_integration_test.rs
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use serde_json::json;
|
||||||
|
use mock_server::models::Payload;
|
||||||
|
use mock_server::loader::MockLoader;
|
||||||
|
use mock_server::router::MockRouter;
|
||||||
|
|
||||||
|
/// 加载 v1 目录的 mock 规则
|
||||||
|
fn load_v2_mocks() -> HashMap<String, Vec<mock_server::models::MockRule>> {
|
||||||
|
let v2_path = Path::new("../mocks/v1");
|
||||||
|
MockLoader::load_all_from_dir(v2_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块一:验证所有 YAML 文件正确加载 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_load_all_mocks() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
|
||||||
|
// 验证索引键存在
|
||||||
|
assert!(index.contains_key("v1"), "应包含 'v2' 索引键");
|
||||||
|
|
||||||
|
// 验证规则总数
|
||||||
|
let total: usize = index.values().map(|v| v.len()).sum();
|
||||||
|
assert_eq!(total, 9, "v2 目录应有 9 个 mock 规则");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块二:JSON Payload 测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_json_login() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
|
let payload = Payload::Json(json!({
|
||||||
|
"username": "admin",
|
||||||
|
"password": "password123"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/v1/auth/login", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "JSON 登录应匹配成功");
|
||||||
|
let rule = matched.unwrap();
|
||||||
|
assert_eq!(rule.name, "v2_user_login");
|
||||||
|
assert_eq!(rule.response.status, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_json_register() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
|
let payload = Payload::Json(json!({
|
||||||
|
"username": "newuser",
|
||||||
|
"password": "newpass123",
|
||||||
|
"email": "newuser@example.com"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/v1/auth/register", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "JSON 注册应匹配成功");
|
||||||
|
let rule = matched.unwrap();
|
||||||
|
assert_eq!(rule.name, "v2_user_register");
|
||||||
|
assert_eq!(rule.response.status, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块三:Form Payload 测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_form_login() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
|
||||||
|
|
||||||
|
let mut form_data = HashMap::new();
|
||||||
|
form_data.insert("username".to_string(), "formuser".to_string());
|
||||||
|
form_data.insert("password".to_string(), "formpass".to_string());
|
||||||
|
let payload = Payload::Form(form_data);
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/v1/user/login/form", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "Form 登录应匹配成功");
|
||||||
|
assert_eq!(matched.unwrap().name, "v2_user_login_form");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块四:Text Payload 测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_text_echo() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "text/plain".to_string());
|
||||||
|
|
||||||
|
let payload = Payload::Text("Hello V2 Mock Server".to_string());
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/v1/user/echo", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "Text 回显应匹配成功");
|
||||||
|
let rule = matched.unwrap();
|
||||||
|
assert_eq!(rule.name, "v2_user_echo");
|
||||||
|
assert!(rule.response.body.contains("Echo from V2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块五:XML Payload 测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_xml_export() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/xml".to_string());
|
||||||
|
|
||||||
|
let xml_body = r#"<request><userId>10001</userId><format>xml</format></request>"#;
|
||||||
|
let payload = Payload::Xml(xml_body.to_string());
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/v1/data/export", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "XML 导出应匹配成功");
|
||||||
|
assert_eq!(matched.unwrap().name, "v2_data_export");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块六:Multipart Payload 测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_multipart_upload() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "multipart/form-data".to_string());
|
||||||
|
|
||||||
|
let mut multipart_data = HashMap::new();
|
||||||
|
multipart_data.insert("avatar".to_string(), "file_content".to_string());
|
||||||
|
multipart_data.insert("description".to_string(), "user avatar".to_string());
|
||||||
|
let payload = Payload::Multipart(multipart_data);
|
||||||
|
|
||||||
|
let matched = router.match_rule("POST", "/v1/user/avatar", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "Multipart 上传应匹配成功");
|
||||||
|
assert_eq!(matched.unwrap().name, "v2_user_upload_avatar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块七:None Payload 测试 (GET 无 Body) ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_health_check() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let payload = Payload::None;
|
||||||
|
|
||||||
|
let matched = router.match_rule("GET", "/v1/health", &HashMap::new(), &HashMap::new(), &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "健康检查应匹配成功");
|
||||||
|
let rule = matched.unwrap();
|
||||||
|
assert_eq!(rule.name, "v2_health_check");
|
||||||
|
assert_eq!(rule.response.status, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_get_profile() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Authorization".to_string(), "Bearer v2_test_token".to_string());
|
||||||
|
|
||||||
|
let payload = Payload::None;
|
||||||
|
|
||||||
|
let matched = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &headers, &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "获取用户信息应匹配成功");
|
||||||
|
assert_eq!(matched.unwrap().name, "v2_user_profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块八:Query Params 测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_query_params() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
let mut query = HashMap::new();
|
||||||
|
query.insert("format".to_string(), "json".to_string());
|
||||||
|
|
||||||
|
let payload = Payload::None;
|
||||||
|
|
||||||
|
let matched = router.match_rule("GET", "/v1/user/download", &query, &HashMap::new(), &payload);
|
||||||
|
|
||||||
|
assert!(matched.is_some(), "带 query params 的下载应匹配成功");
|
||||||
|
let rule = matched.unwrap();
|
||||||
|
assert_eq!(rule.name, "v2_user_download");
|
||||||
|
assert!(rule.response.body.starts_with("file://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 模块九:Header 匹配测试 ==========
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v2_header_required() {
|
||||||
|
let index = load_v2_mocks();
|
||||||
|
let router = MockRouter::new(index);
|
||||||
|
|
||||||
|
// 无 Authorization header 不应匹配
|
||||||
|
let payload = Payload::None;
|
||||||
|
let no_match = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &HashMap::new(), &payload);
|
||||||
|
assert!(no_match.is_none(), "无 Authorization 不应匹配");
|
||||||
|
|
||||||
|
// 有正确的 Authorization header 应匹配
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Authorization".to_string(), "Bearer v2_test_token".to_string());
|
||||||
|
let matched = router.match_rule("GET", "/v1/user/profile", &HashMap::new(), &headers, &payload);
|
||||||
|
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