From 110be35c54fb3e00f4bef2154e98240cf49f3f45 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 15 Jul 2026 18:20:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8A=BD=20main.rs=20=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E4=BB=B6=E5=88=B0=20src/middleware.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.rs 混合了配置校验、压缩层、cache-control、admin guard、迁移 runtime、后台任务、路由注册,其中 5 个 server-only 中间件/纯函数可独立测试。迁出到新建 src/middleware.rs(#![cfg(feature=server)]):CompressionAlgorithms + parse_compression_algorithms + compression_layer_from_env + cache_control_for_path + add_cache_control + admin_guard,连同 12 个单元测试。main.rs 调用点改为全路径 crate::middleware::xxx。无新 crate、无循环依赖。main.rs 756 -> 382 行,中间件现已可独立测试。 --- src/main.rs | 392 ++-------------------------------------------- src/middleware.rs | 382 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 391 insertions(+), 383 deletions(-) create mode 100644 src/middleware.rs diff --git a/src/main.rs b/src/main.rs index 05ea3b3..5f8e0cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,10 @@ pub mod infra; // highlight 模块仅在服务端构建时编译 #[cfg(feature = "server")] mod highlight; +// middleware:Axum 中间件与启动期纯函数(cache-control / admin 守卫 / 压缩层), +// server-only。从 main.rs 抽出以便独立测试,路由组装处以 crate::middleware::xxx 调用。 +#[cfg(feature = "server")] +mod middleware; mod hooks; mod models; mod pages; @@ -46,230 +50,6 @@ mod utils; mod webp; mod xterm_bridge; -/// 压缩算法配置。 -#[cfg(feature = "server")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct CompressionAlgorithms { - gzip: bool, - brotli: bool, - deflate: bool, - zstd: bool, -} - -#[cfg(feature = "server")] -impl CompressionAlgorithms { - fn all_enabled() -> Self { - Self { - gzip: true, - brotli: true, - deflate: true, - zstd: true, - } - } - - fn is_empty(&self) -> bool { - !self.gzip && !self.brotli && !self.deflate && !self.zstd - } -} - -/// 解析 COMPRESSION_ALGORITHMS 环境变量值。 -/// ""、"none"、"off" 返回 None;"all" 或未识别到任何算法时启用全部。 -#[cfg(feature = "server")] -fn parse_compression_algorithms(env: &str) -> Option { - let env = env.trim(); - if env.is_empty() || env.eq_ignore_ascii_case("none") || env.eq_ignore_ascii_case("off") { - return None; - } - - let mut all = false; - let mut gzip = false; - let mut brotli = false; - let mut deflate = false; - let mut zstd = false; - - for part in env.split(',') { - match part.trim().to_lowercase().as_str() { - "all" => all = true, - "gzip" => gzip = true, - "brotli" | "br" => brotli = true, - "deflate" => deflate = true, - "zstd" => zstd = true, - other => tracing::warn!( - "Unknown compression algorithm in COMPRESSION_ALGORITHMS: '{}'", - other - ), - } - } - - if all { - return Some(CompressionAlgorithms::all_enabled()); - } - - let algorithms = CompressionAlgorithms { - gzip, - brotli, - deflate, - zstd, - }; - if algorithms.is_empty() { - return None; - } - - Some(algorithms) -} - -/// 根据 COMPRESSION_ALGORITHMS 环境变量构造 CompressionLayer。 -/// 未设置或设置为 "all" 时启用全部算法;设置为 ""、"none" 或 "off" 时禁用。 -/// -/// CompressionLayer 使用 tower-http 的 `DefaultPredicate`,开箱即用即: -/// - 跳过 `image/*` content-type(WebP/PNG/JPEG/GIF 等已是压缩格式,再压浪费 CPU, -/// 唯一例外是 `image/svg+xml`,作为 XML 文本可被压缩); -/// - 跳过 gRPC 与 `text/event-stream`(SSE); -/// - 跳过小于 32 字节的响应。 -/// -/// 因此无需在此处对图片响应做额外的 content-type 过滤。另:图片实际挂在 -/// `static_routes`(无中间件),根本不经此层,详见下方路由 merge 处。 -#[cfg(feature = "server")] -fn compression_layer_from_env() -> Option { - use tower_http::compression::CompressionLayer; - - let env = std::env::var("COMPRESSION_ALGORITHMS").unwrap_or_else(|_| "all".to_string()); - let algorithms = parse_compression_algorithms(&env)?; - - Some( - CompressionLayer::new() - .gzip(algorithms.gzip) - .br(algorithms.brotli) - .deflate(algorithms.deflate) - .zstd(algorithms.zstd), - ) -} - -/// 根据请求路径和方法决定公开页面的 Cache-Control 头。 -/// 返回 None 表示不添加缓存头(保留现有行为或避免覆盖)。 -#[cfg(feature = "server")] -fn cache_control_for_path( - path: &str, - method: &axum::http::Method, -) -> Option { - use axum::http::{HeaderValue, Method}; - - // 只对 GET/HEAD 请求添加缓存头 - if *method != Method::GET && *method != Method::HEAD { - return None; - } - - // API 接口:不缓存(可能涉及认证、写操作) - if path.starts_with("/api") { - return None; - } - - // 管理后台和认证页面:不缓存 - if path.starts_with("/admin") || path == "/login" || path == "/register" { - return None; - } - - // 静态资源:长期缓存(Dioxus/WASM 资源通常带内容哈希) - if path.starts_with("/_dioxus/") - || path.starts_with("/wasm/") - || path.ends_with(".wasm") - || path.ends_with(".js") - || path == "/style.css" - || path == "/highlight.css" - { - return Some(HeaderValue::from_static( - "public, max-age=31536000, immutable", - )); - } - - // 公开页面:5 分钟新鲜期,过期后 1 小时内可提供过期内容并后台重新验证 - Some(HeaderValue::from_static( - "public, max-age=300, stale-while-revalidate=3600", - )) -} - -/// Axum 中间件:为公开页面和静态资源附加 Cache-Control 头。 -#[cfg(feature = "server")] -async fn add_cache_control( - req: axum::extract::Request, - next: axum::middleware::Next, -) -> axum::response::Response { - use axum::http::header; - - let path = req.uri().path().to_string(); - let method = req.method().clone(); - let cache_value = cache_control_for_path(&path, &method); - - let mut response = next.run(req).await; - - if let Some(value) = cache_value { - // 仅当响应尚未设置 Cache-Control 时才添加,避免覆盖已有策略 - response - .headers_mut() - .entry(header::CACHE_CONTROL) - .or_insert(value); - } - - response -} - -/// Axum 中间件:`/admin*` 的 SSR 层认证守卫。 -/// -/// 未登录访问后台时,服务端**直接 302 跳转 `/login`**,根本不进入 Dioxus -/// SSR 渲染器。此前后台鉴权完全在客户端 WASM 完成(SSR 渲染骨架屏 → WASM -/// 下载/编译 → hydrate → 异步 `get_current_user()` → 客户端 `navigator.push`), -/// 整条链串行,未登录用户首屏要"空白好久"才跳登录。 -/// -/// - 只匹配 `/admin*`,其它路径(`/login`、公开页、`/api/*`)直接放行。 -/// - 复用 `get_user_by_token`:命中内存缓存 + 校验 `session_generation` -/// (封禁/降级后旧 session 立即失效),与客户端鉴权同一套语义。 -/// - DB 错误时 **fail-open**(放行进入 SSR):避免数据库抖动把已登录的 -/// 管理员也踢到登录页;客户端 `AdminLayout` 仍有兜底校验。 -/// - `/admin` 与 `/login` 本就不进 `cache_control_for_path` 缓存,302 不会被缓存。 -#[cfg(feature = "server")] -async fn admin_guard( - req: axum::extract::Request, - next: axum::middleware::Next, -) -> axum::response::Response { - use crate::models::user::UserRole; - use axum::body::Body; - use axum::http::{header, StatusCode}; - use axum::response::Response; - - let path = req.uri().path().to_string(); - if !path.starts_with("/admin") { - return next.run(req).await; - } - - // 从 Cookie 头读 session token(与 export.rs / upload.rs 同款手法)。 - let cookie = req - .headers() - .get("cookie") - .and_then(|h| h.to_str().ok()) - .unwrap_or(""); - let token = crate::auth::session::parse_session_token(cookie); - - let is_admin = match token { - Some(t) => match crate::api::auth::get_user_by_token(t).await { - Ok(Some(user)) => user.role == UserRole::Admin, - // Err(DB 抖动)/ Ok(None)(token 无效):fail-open,交给客户端兜底。 - _ => true, - }, - // 无 token:明确未登录,拦截。 - None => false, - }; - - if is_admin { - next.run(req).await - } else { - Response::builder() - .status(StatusCode::FOUND) - .header(header::LOCATION, "/login") - .body(Body::empty()) - .unwrap() - } -} - /// 程序入口 fn main() { // server feature:启动服务端 @@ -540,9 +320,9 @@ fn main() { // layer 顺序:后加的最外层先执行。CSRF 最外层先拦截非法来源。 let mut app_routes = dioxus_app .layer(axum::middleware::from_fn(ssr_generation_middleware)) - .layer(axum::middleware::from_fn(add_cache_control)) + .layer(axum::middleware::from_fn(crate::middleware::add_cache_control)) .layer(axum::middleware::from_fn(crate::api::csrf::csrf_middleware)); - if let Some(layer) = compression_layer_from_env() { + if let Some(layer) = crate::middleware::compression_layer_from_env() { app_routes = app_routes.layer(layer); } let app_routes = app_routes.layer(TimeoutLayer::with_status_code( @@ -551,7 +331,9 @@ fn main() { )); // admin_guard 置于最外层(最后添加 = 最先执行):未登录的 /admin* 请求 // 在 CSRF / cache / SSR 渲染之前就被 302 短路,零渲染开销。 - let app_routes = app_routes.layer(axum::middleware::from_fn(admin_guard)); + let app_routes = app_routes.layer(axum::middleware::from_fn( + crate::middleware::admin_guard, + )); // 静态资源路由:图片文件服务。 // 注意:`dioxus::server::serve()` 接管了 listener 与 `into_make_service` @@ -598,159 +380,3 @@ fn main() { dioxus::launch(AppRouter); } } - -#[cfg(all(test, feature = "server"))] -mod tests { - use super::{cache_control_for_path, parse_compression_algorithms, CompressionAlgorithms}; - use axum::http::Method; - - fn cache_value(path: &str, method: Method) -> Option { - cache_control_for_path(path, &method).map(|v| v.to_str().unwrap().to_string()) - } - - #[test] - fn public_page_is_cached() { - assert_eq!( - cache_value("/", Method::GET), - Some("public, max-age=300, stale-while-revalidate=3600".to_string()) - ); - assert_eq!( - cache_value("/post/hello-world", Method::GET), - Some("public, max-age=300, stale-while-revalidate=3600".to_string()) - ); - assert_eq!( - cache_value("/tags/rust", Method::GET), - Some("public, max-age=300, stale-while-revalidate=3600".to_string()) - ); - } - - #[test] - fn static_assets_are_cached_long_term() { - assert_eq!( - cache_value("/style.css", Method::GET), - Some("public, max-age=31536000, immutable".to_string()) - ); - assert_eq!( - cache_value("/highlight.css", Method::GET), - Some("public, max-age=31536000, immutable".to_string()) - ); - assert_eq!( - cache_value("/wasm/app.wasm", Method::GET), - Some("public, max-age=31536000, immutable".to_string()) - ); - assert_eq!( - cache_value("/_dioxus/assets/main.js", Method::GET), - Some("public, max-age=31536000, immutable".to_string()) - ); - } - - #[test] - fn api_and_admin_and_auth_are_not_cached() { - assert_eq!(cache_value("/api/posts", Method::GET), None); - assert_eq!(cache_value("/admin", Method::GET), None); - assert_eq!(cache_value("/admin/posts", Method::GET), None); - assert_eq!(cache_value("/login", Method::GET), None); - assert_eq!(cache_value("/register", Method::GET), None); - } - - #[test] - fn non_get_requests_are_not_cached() { - assert_eq!(cache_value("/", Method::POST), None); - assert_eq!(cache_value("/post/hello-world", Method::POST), None); - assert_eq!(cache_value("/style.css", Method::POST), None); - } - - #[test] - fn head_requests_are_cached_like_get() { - assert_eq!( - cache_value("/", Method::HEAD), - Some("public, max-age=300, stale-while-revalidate=3600".to_string()) - ); - } - - #[test] - fn compression_all_enables_everything() { - assert_eq!( - parse_compression_algorithms("all"), - Some(CompressionAlgorithms::all_enabled()) - ); - } - - #[test] - fn compression_default_env_is_all() { - // 模拟未设置环境变量时的默认值 - assert_eq!( - parse_compression_algorithms("all"), - Some(CompressionAlgorithms::all_enabled()) - ); - } - - #[test] - fn compression_empty_none_off_disable() { - assert_eq!(parse_compression_algorithms(""), None); - assert_eq!(parse_compression_algorithms("none"), None); - assert_eq!(parse_compression_algorithms("NONE"), None); - assert_eq!(parse_compression_algorithms("off"), None); - assert_eq!(parse_compression_algorithms("OFF"), None); - } - - #[test] - fn compression_single_algorithm() { - assert_eq!( - parse_compression_algorithms("gzip"), - Some(CompressionAlgorithms { - gzip: true, - brotli: false, - deflate: false, - zstd: false, - }) - ); - assert_eq!( - parse_compression_algorithms("br"), - Some(CompressionAlgorithms { - gzip: false, - brotli: true, - deflate: false, - zstd: false, - }) - ); - } - - #[test] - fn compression_multiple_algorithms() { - assert_eq!( - parse_compression_algorithms("gzip, zstd"), - Some(CompressionAlgorithms { - gzip: true, - brotli: false, - deflate: false, - zstd: true, - }) - ); - } - - #[test] - fn compression_case_insensitive_and_whitespace_tolerant() { - assert_eq!( - parse_compression_algorithms("GZIP, Brotli, Deflate, Zstd"), - Some(CompressionAlgorithms::all_enabled()) - ); - assert_eq!( - parse_compression_algorithms(" gzip , br , deflate , zstd "), - Some(CompressionAlgorithms::all_enabled()) - ); - } - - #[test] - fn compression_unknown_algorithms_are_ignored() { - assert_eq!( - parse_compression_algorithms("gzip, unknown, lz4"), - Some(CompressionAlgorithms { - gzip: true, - brotli: false, - deflate: false, - zstd: false, - }) - ); - } -} diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000..eb02ee7 --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,382 @@ +//! Axum 中间件与启动期纯函数。 +//! +//! 从 `main.rs` 抽出的、可独立测试的服务端 HTTP 中间件(cache-control、admin +//! 守卫)与压缩层构造逻辑。整体 server-only——WASM 构建不会编译本模块。 +//! +//! 这些函数此前散落在 `main.rs`,既无法单独测试也使入口职责过载。迁移后 +//! `main.rs` 的路由组装以全路径 `crate::middleware::xxx` 引用,语义不变。 + +#![cfg(feature = "server")] + +/// 压缩算法配置。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CompressionAlgorithms { + gzip: bool, + brotli: bool, + deflate: bool, + zstd: bool, +} + +impl CompressionAlgorithms { + fn all_enabled() -> Self { + Self { + gzip: true, + brotli: true, + deflate: true, + zstd: true, + } + } + + fn is_empty(&self) -> bool { + !self.gzip && !self.brotli && !self.deflate && !self.zstd + } +} + +/// 解析 COMPRESSION_ALGORITHMS 环境变量值。 +/// ""、"none"、"off" 返回 None;"all" 或未识别到任何算法时启用全部。 +pub(crate) fn parse_compression_algorithms(env: &str) -> Option { + let env = env.trim(); + if env.is_empty() || env.eq_ignore_ascii_case("none") || env.eq_ignore_ascii_case("off") { + return None; + } + + let mut all = false; + let mut gzip = false; + let mut brotli = false; + let mut deflate = false; + let mut zstd = false; + + for part in env.split(',') { + match part.trim().to_lowercase().as_str() { + "all" => all = true, + "gzip" => gzip = true, + "brotli" | "br" => brotli = true, + "deflate" => deflate = true, + "zstd" => zstd = true, + other => tracing::warn!( + "Unknown compression algorithm in COMPRESSION_ALGORITHMS: '{}'", + other + ), + } + } + + if all { + return Some(CompressionAlgorithms::all_enabled()); + } + + let algorithms = CompressionAlgorithms { + gzip, + brotli, + deflate, + zstd, + }; + if algorithms.is_empty() { + return None; + } + + Some(algorithms) +} + +/// 根据 COMPRESSION_ALGORITHMS 环境变量构造 CompressionLayer。 +/// 未设置或设置为 "all" 时启用全部算法;设置为 ""、"none" 或 "off" 时禁用。 +/// +/// CompressionLayer 使用 tower-http 的 `DefaultPredicate`,开箱即用即: +/// - 跳过 `image/*` content-type(WebP/PNG/JPEG/GIF 等已是压缩格式,再压浪费 CPU, +/// 唯一例外是 `image/svg+xml`,作为 XML 文本可被压缩); +/// - 跳过 gRPC 与 `text/event-stream`(SSE); +/// - 跳过小于 32 字节的响应。 +/// +/// 因此无需在此处对图片响应做额外的 content-type 过滤。另:图片实际挂在 +/// `static_routes`(无中间件),根本不经此层,详见 main.rs 路由 merge 处。 +pub(crate) fn compression_layer_from_env() -> Option { + use tower_http::compression::CompressionLayer; + + let env = std::env::var("COMPRESSION_ALGORITHMS").unwrap_or_else(|_| "all".to_string()); + let algorithms = parse_compression_algorithms(&env)?; + + Some( + CompressionLayer::new() + .gzip(algorithms.gzip) + .br(algorithms.brotli) + .deflate(algorithms.deflate) + .zstd(algorithms.zstd), + ) +} + +/// 根据请求路径和方法决定公开页面的 Cache-Control 头。 +/// 返回 None 表示不添加缓存头(保留现有行为或避免覆盖)。 +pub(crate) fn cache_control_for_path( + path: &str, + method: &axum::http::Method, +) -> Option { + use axum::http::{HeaderValue, Method}; + + // 只对 GET/HEAD 请求添加缓存头 + if *method != Method::GET && *method != Method::HEAD { + return None; + } + + // API 接口:不缓存(可能涉及认证、写操作) + if path.starts_with("/api") { + return None; + } + + // 管理后台和认证页面:不缓存 + if path.starts_with("/admin") || path == "/login" || path == "/register" { + return None; + } + + // 静态资源:长期缓存(Dioxus/WASM 资源通常带内容哈希) + if path.starts_with("/_dioxus/") + || path.starts_with("/wasm/") + || path.ends_with(".wasm") + || path.ends_with(".js") + || path == "/style.css" + || path == "/highlight.css" + { + return Some(HeaderValue::from_static( + "public, max-age=31536000, immutable", + )); + } + + // 公开页面:5 分钟新鲜期,过期后 1 小时内可提供过期内容并后台重新验证 + Some(HeaderValue::from_static( + "public, max-age=300, stale-while-revalidate=3600", + )) +} + +/// Axum 中间件:为公开页面和静态资源附加 Cache-Control 头。 +pub(crate) async fn add_cache_control( + req: axum::extract::Request, + next: axum::middleware::Next, +) -> axum::response::Response { + use axum::http::header; + + let path = req.uri().path().to_string(); + let method = req.method().clone(); + let cache_value = cache_control_for_path(&path, &method); + + let mut response = next.run(req).await; + + if let Some(value) = cache_value { + // 仅当响应尚未设置 Cache-Control 时才添加,避免覆盖已有策略 + response + .headers_mut() + .entry(header::CACHE_CONTROL) + .or_insert(value); + } + + response +} + +/// Axum 中间件:`/admin*` 的 SSR 层认证守卫。 +/// +/// 未登录访问后台时,服务端**直接 302 跳转 `/login`**,根本不进入 Dioxus +/// SSR 渲染器。此前后台鉴权完全在客户端 WASM 完成(SSR 渲染骨架屏 → WASM +/// 下载/编译 → hydrate → 异步 `get_current_user()` → 客户端 `navigator.push`), +/// 整条链串行,未登录用户首屏要"空白好久"才跳登录。 +/// +/// - 只匹配 `/admin*`,其它路径(`/login`、公开页、`/api/*`)直接放行。 +/// - 复用 `get_user_by_token`:命中内存缓存 + 校验 `session_generation` +/// (封禁/降级后旧 session 立即失效),与客户端鉴权同一套语义。 +/// - DB 错误时 **fail-open**(放行进入 SSR):避免数据库抖动把已登录的 +/// 管理员也踢到登录页;客户端 `AdminLayout` 仍有兜底校验。 +/// - `/admin` 与 `/login` 本就不进 `cache_control_for_path` 缓存,302 不会被缓存。 +pub(crate) async fn admin_guard( + req: axum::extract::Request, + next: axum::middleware::Next, +) -> axum::response::Response { + use crate::models::user::UserRole; + use axum::body::Body; + use axum::http::{header, StatusCode}; + use axum::response::Response; + + let path = req.uri().path().to_string(); + if !path.starts_with("/admin") { + return next.run(req).await; + } + + // 从 Cookie 头读 session token(与 export.rs / upload.rs 同款手法)。 + let cookie = req + .headers() + .get("cookie") + .and_then(|h| h.to_str().ok()) + .unwrap_or(""); + let token = crate::auth::session::parse_session_token(cookie); + + let is_admin = match token { + Some(t) => match crate::api::auth::get_user_by_token(t).await { + Ok(Some(user)) => user.role == UserRole::Admin, + // Err(DB 抖动)/ Ok(None)(token 无效):fail-open,交给客户端兜底。 + _ => true, + }, + // 无 token:明确未登录,拦截。 + None => false, + }; + + if is_admin { + next.run(req).await + } else { + Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, "/login") + .body(Body::empty()) + .unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::{cache_control_for_path, parse_compression_algorithms, CompressionAlgorithms}; + use axum::http::Method; + + fn cache_value(path: &str, method: Method) -> Option { + cache_control_for_path(path, &method).map(|v| v.to_str().unwrap().to_string()) + } + + #[test] + fn public_page_is_cached() { + assert_eq!( + cache_value("/", Method::GET), + Some("public, max-age=300, stale-while-revalidate=3600".to_string()) + ); + assert_eq!( + cache_value("/post/hello-world", Method::GET), + Some("public, max-age=300, stale-while-revalidate=3600".to_string()) + ); + assert_eq!( + cache_value("/tags/rust", Method::GET), + Some("public, max-age=300, stale-while-revalidate=3600".to_string()) + ); + } + + #[test] + fn static_assets_are_cached_long_term() { + assert_eq!( + cache_value("/style.css", Method::GET), + Some("public, max-age=31536000, immutable".to_string()) + ); + assert_eq!( + cache_value("/highlight.css", Method::GET), + Some("public, max-age=31536000, immutable".to_string()) + ); + assert_eq!( + cache_value("/wasm/app.wasm", Method::GET), + Some("public, max-age=31536000, immutable".to_string()) + ); + assert_eq!( + cache_value("/_dioxus/assets/main.js", Method::GET), + Some("public, max-age=31536000, immutable".to_string()) + ); + } + + #[test] + fn api_and_admin_and_auth_are_not_cached() { + assert_eq!(cache_value("/api/posts", Method::GET), None); + assert_eq!(cache_value("/admin", Method::GET), None); + assert_eq!(cache_value("/admin/posts", Method::GET), None); + assert_eq!(cache_value("/login", Method::GET), None); + assert_eq!(cache_value("/register", Method::GET), None); + } + + #[test] + fn non_get_requests_are_not_cached() { + assert_eq!(cache_value("/", Method::POST), None); + assert_eq!(cache_value("/post/hello-world", Method::POST), None); + assert_eq!(cache_value("/style.css", Method::POST), None); + } + + #[test] + fn head_requests_are_cached_like_get() { + assert_eq!( + cache_value("/", Method::HEAD), + Some("public, max-age=300, stale-while-revalidate=3600".to_string()) + ); + } + + #[test] + fn compression_all_enables_everything() { + assert_eq!( + parse_compression_algorithms("all"), + Some(CompressionAlgorithms::all_enabled()) + ); + } + + #[test] + fn compression_default_env_is_all() { + // 模拟未设置环境变量时的默认值 + assert_eq!( + parse_compression_algorithms("all"), + Some(CompressionAlgorithms::all_enabled()) + ); + } + + #[test] + fn compression_empty_none_off_disable() { + assert_eq!(parse_compression_algorithms(""), None); + assert_eq!(parse_compression_algorithms("none"), None); + assert_eq!(parse_compression_algorithms("NONE"), None); + assert_eq!(parse_compression_algorithms("off"), None); + assert_eq!(parse_compression_algorithms("OFF"), None); + } + + #[test] + fn compression_single_algorithm() { + assert_eq!( + parse_compression_algorithms("gzip"), + Some(CompressionAlgorithms { + gzip: true, + brotli: false, + deflate: false, + zstd: false, + }) + ); + assert_eq!( + parse_compression_algorithms("br"), + Some(CompressionAlgorithms { + gzip: false, + brotli: true, + deflate: false, + zstd: false, + }) + ); + } + + #[test] + fn compression_multiple_algorithms() { + assert_eq!( + parse_compression_algorithms("gzip, zstd"), + Some(CompressionAlgorithms { + gzip: true, + brotli: false, + deflate: false, + zstd: true, + }) + ); + } + + #[test] + fn compression_case_insensitive_and_whitespace_tolerant() { + assert_eq!( + parse_compression_algorithms("GZIP, Brotli, Deflate, Zstd"), + Some(CompressionAlgorithms::all_enabled()) + ); + assert_eq!( + parse_compression_algorithms(" gzip , br , deflate , zstd "), + Some(CompressionAlgorithms::all_enabled()) + ); + } + + #[test] + fn compression_unknown_algorithms_are_ignored() { + assert_eq!( + parse_compression_algorithms("gzip, unknown, lz4"), + Some(CompressionAlgorithms { + gzip: true, + brotli: false, + deflate: false, + zstd: false, + }) + ); + } +}