Skip to main content

sz_rust_state_facade/
cookie.rs

1//! Cookie 模块 — 对齐 PHP `think\Cookie`
2//!
3//! 本模块实现 Cookie 管理,对齐 PHP `think\Cookie` 的核心 API。
4//!
5//! ## PHP 对齐
6//!
7//! ### 核心 API 映射
8//!
9//! | PHP 方法 | Rust 方法 | 说明 |
10//! |---------|-----------|------|
11//! | `Cookie::set($name, $value, $option)` | [`CookieJar::set`] | 设置 cookie(暂存到 jar,等 save 时统一发送) |
12//! | `Cookie::get($name, $default)` | [`CookieJar::get`] / [`CookieJar::get_with_default`] | 从 Request 读取 cookie |
13//! | `Cookie::has($name)` | [`CookieJar::has`] | 检查 Request 中是否存在 cookie |
14//! | `Cookie::delete($name, $options)` | [`CookieJar::delete`] | 删除 cookie(设置过期时间为过去) |
15//! | `Cookie::forever($name, $value, $option)` | [`CookieJar::forever`] | 永久保存(10 年) |
16//! | `Cookie::save()` | [`CookieJar::apply_to_response`] | 将所有暂存的 cookie 写入 Response 的 Set-Cookie 头 |
17//!
18//! ### PHP 行为对齐
19//!
20//! - **延迟发送**:PHP `set()` 仅暂存到 `$this->cookie` 数组,`save()` 时统一发送
21//!   `Set-Cookie` 头。Rust 通过 [`CookieJar::set`] 暂存到内部 Vec,
22//!   [`CookieJar::apply_to_response`] 时统一写入 Response。
23//! - **配置覆盖**:PHP `set()` 的 `$option` 参数会覆盖默认配置。Rust 通过
24//!   [`CookieOptions`] 实现相同行为。
25//! - **永久 Cookie**:PHP `forever()` 设置 315360000 秒(10 年)过期。
26//!   Rust 使用相同常量。
27//! - **删除 Cookie**:PHP `delete()` 通过设置过期时间为 `time() - 3600` 实现。
28//!   Rust 使用相同策略(expire 设为 -3600)。
29//!
30//! ## 架构说明
31//!
32//! 本模块不依赖外部 `cookie` crate,直接基于 `axum::http::HeaderMap` 操作
33//! `Cookie` 和 `Set-Cookie` 头,保持依赖最小化。
34//!
35//! ### Cookie 头格式(RFC 6265)
36//!
37//! - **请求头**:`Cookie: name1=value1; name2=value2`
38//! - **响应头**:`Set-Cookie: name=value; Expires=...; Path=/; Domain=...; Secure; HttpOnly; SameSite=Lax`
39
40use axum::http::{HeaderName, HeaderValue, Request, Response};
41use chrono::{DateTime, Utc};
42use std::collections::HashMap;
43
44// ============================================================================
45// 默认配置常量(对齐 PHP `think\Cookie::$config` 默认值)
46// ============================================================================
47
48/// 永久 Cookie 过期时间(秒)— 对齐 PHP `Cookie::forever()` 的 315360000
49const FOREVER_EXPIRE_SECONDS: i64 = 315360000;
50
51/// 删除 Cookie 时使用的过期时间偏移(秒)— 对齐 PHP `delete()` 的 `time() - 3600`
52const DELETE_EXPIRE_OFFSET_SECONDS: i64 = -3600;
53
54// ============================================================================
55// Cookie 选项(对齐 PHP `think\Cookie::$config`)
56// ============================================================================
57
58/// Cookie 配置选项(对齐 PHP `think\Cookie::$config`)
59///
60/// 对齐 PHP 默认配置:
61/// ```php
62/// protected $config = [
63///     'expire'   => 0,
64///     'path'     => '/',
65///     'domain'   => '',
66///     'secure'   => false,
67///     'httponly' => false,
68///     'samesite' => '',
69/// ];
70/// ```
71#[derive(Debug, Clone)]
72pub struct CookieOptions {
73    /// 过期时间(秒)。0 表示会话 cookie(浏览器关闭时删除)。
74    /// 对齐 PHP `'expire' => 0`。
75    pub expire: i64,
76    /// Cookie 有效路径。对齐 PHP `'path' => '/'`。
77    pub path: String,
78    /// Cookie 有效域名。对齐 PHP `'domain' => ''`。
79    pub domain: String,
80    /// 是否仅通过 HTTPS 传输。对齐 PHP `'secure' => false`。
81    pub secure: bool,
82    /// 是否仅通过 HTTP 访问(JS 不可读)。对齐 PHP `'httponly' => false`。
83    pub httponly: bool,
84    /// SameSite 策略(`Strict` / `Lax` / `None` / 空)。对齐 PHP `'samesite' => ''`。
85    pub samesite: String,
86}
87
88impl Default for CookieOptions {
89    fn default() -> Self {
90        Self {
91            expire: 0,
92            path: "/".to_string(),
93            domain: String::new(),
94            secure: false,
95            httponly: false,
96            samesite: String::new(),
97        }
98    }
99}
100
101impl CookieOptions {
102    /// 创建一个指定过期时间的选项(其他字段使用默认值)
103    ///
104    /// 对齐 PHP `set($name, $value, $option)` 中 `$option` 为数字时
105    /// 转换为 `['expire' => $option]` 的行为。
106    pub fn with_expire(expire: i64) -> Self {
107        Self {
108            expire,
109            ..Default::default()
110        }
111    }
112}
113
114// ============================================================================
115// Cookie 条目(暂存的 Set-Cookie 数据)
116// ============================================================================
117
118/// 单个 Cookie 条目(暂存在 [`CookieJar`] 中,等待 `apply_to_response` 发送)
119///
120/// 对齐 PHP `think\Cookie::setCookie()` 暂存的 `[$value, $expire, $option]` 元组。
121#[derive(Debug, Clone)]
122pub struct CookieEntry {
123    /// Cookie 名称
124    pub name: String,
125    /// Cookie 值
126    pub value: String,
127    /// 过期时间戳(Unix 秒)。0 表示会话 cookie。
128    pub expire: i64,
129    /// 其他配置选项
130    pub options: CookieOptions,
131}
132
133impl CookieEntry {
134    /// 将 Cookie 条目转换为 `Set-Cookie` 头值字符串
135    ///
136    /// 格式(RFC 6265):
137    /// ```text
138    /// name=value; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/; Domain=example.com; Secure; HttpOnly; SameSite=Lax
139    /// ```
140    ///
141    /// 对齐 PHP `think\Cookie::saveCookie()` 的 `setcookie()` 调用。
142    pub fn to_header_string(&self) -> String {
143        let mut parts = vec![format!("{}={}", self.name, self.value)];
144
145        // Expires 头(仅当 expire > 0 时添加,对齐 PHP 仅在 expire 非零时发送)
146        if self.expire > 0 {
147            let expire_dt =
148                DateTime::<Utc>::from_timestamp(self.expire, 0).unwrap_or_else(Utc::now);
149            // 格式:Wed, 21 Oct 2026 07:28:00 GMT(RFC 7231 IMF-fixdate)
150            parts.push(format!(
151                "Expires={}",
152                expire_dt.format("%a, %d %b %Y %H:%M:%S GMT")
153            ));
154        }
155
156        // Path(对齐 PHP `'path' => '/'`)
157        if !self.options.path.is_empty() {
158            parts.push(format!("Path={}", self.options.path));
159        }
160
161        // Domain(对齐 PHP `'domain' => ''`)
162        if !self.options.domain.is_empty() {
163            parts.push(format!("Domain={}", self.options.domain));
164        }
165
166        // Secure(对齐 PHP `'secure' => false`)
167        if self.options.secure {
168            parts.push("Secure".to_string());
169        }
170
171        // HttpOnly(对齐 PHP `'httponly' => false`)
172        if self.options.httponly {
173            parts.push("HttpOnly".to_string());
174        }
175
176        // SameSite(对齐 PHP `'samesite' => ''`)
177        if !self.options.samesite.is_empty() {
178            parts.push(format!("SameSite={}", self.options.samesite));
179        }
180
181        parts.join("; ")
182    }
183}
184
185// ============================================================================
186// CookieJar — Cookie 管理器(对齐 PHP `think\Cookie` 类)
187// ============================================================================
188
189/// Cookie 管理器(对齐 PHP `think\Cookie`)
190///
191/// 同时管理两类数据:
192/// 1. **请求 Cookie**(从 `Request` 的 `Cookie` 头解析得到,只读)
193/// 2. **响应 Cookie**(通过 `set()` 暂存,`apply_to_response()` 时发送)
194///
195/// # 用法
196///
197/// ```ignore
198/// use sz_rust_state_facade::cookie::{CookieJar, CookieOptions};
199/// use axum::http::Request;
200/// use axum::body::Body;
201///
202/// // 1. 从 Request 创建 CookieJar
203/// let req = Request::<Body>::default();
204/// let jar = CookieJar::from_request(&req);
205///
206/// // 2. 读取请求 Cookie
207/// if let Some(value) = jar.get("session_id") {
208///     println!("session_id = {}", value);
209/// }
210///
211/// // 3. 设置响应 Cookie
212/// let jar = CookieJar::from_request(&req)
213///     .set("token", "abc123", CookieOptions::default());
214///
215/// // 4. 应用到 Response
216/// let mut resp = Response::new(Body::empty());
217/// jar.apply_to_response(&mut resp);
218/// ```
219#[derive(Debug, Clone, Default)]
220pub struct CookieJar {
221    /// 请求 Cookie(从 Request 解析,只读)
222    request_cookies: HashMap<String, String>,
223    /// 待发送的响应 Cookie(通过 `set()` 暂存)
224    response_cookies: Vec<CookieEntry>,
225    /// 默认配置(对齐 PHP `think\Cookie::$config`)
226    config: CookieOptions,
227}
228
229impl CookieJar {
230    /// 创建空的 CookieJar(使用默认配置)
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// 创建指定默认配置的 CookieJar
236    ///
237    /// 对齐 PHP `new Cookie($request, $config)` 构造函数。
238    pub fn with_config(config: CookieOptions) -> Self {
239        Self {
240            request_cookies: HashMap::new(),
241            response_cookies: Vec::new(),
242            config,
243        }
244    }
245
246    /// 从 Request 的 Cookie 头解析创建 CookieJar
247    ///
248    /// 对齐 PHP `Cookie::__construct(Request $request)` 通过 `$this->request->cookie()` 读取。
249    ///
250    /// # 解析格式
251    ///
252    /// `Cookie: name1=value1; name2=value2`
253    pub fn from_request<B>(req: &Request<B>) -> Self {
254        let mut jar = Self::default();
255        if let Some(cookie_header) = req.headers().get(axum::http::header::COOKIE) {
256            if let Ok(header_str) = cookie_header.to_str() {
257                jar.request_cookies = parse_cookie_header(header_str);
258            }
259        }
260        jar
261    }
262
263    /// 获取请求 Cookie 值(对齐 PHP `Cookie::get($name, $default)`)
264    ///
265    /// # 返回
266    ///
267    /// - `Some(value)`:Cookie 存在
268    /// - `None`:Cookie 不存在
269    pub fn get(&self, name: &str) -> Option<String> {
270        self.request_cookies.get(name).cloned()
271    }
272
273    /// 获取请求 Cookie 值,不存在时返回默认值
274    ///
275    /// 对齐 PHP `Cookie::get($name, $default)` 的 `$default` 参数。
276    pub fn get_with_default(&self, name: &str, default: &str) -> String {
277        self.request_cookies
278            .get(name)
279            .cloned()
280            .unwrap_or_else(|| default.to_string())
281    }
282
283    /// 检查请求 Cookie 是否存在(对齐 PHP `Cookie::has($name)`)
284    pub fn has(&self, name: &str) -> bool {
285        self.request_cookies.contains_key(name)
286    }
287
288    /// 设置响应 Cookie(对齐 PHP `Cookie::set($name, $value, $option)`)
289    ///
290    /// 仅暂存到内部 Vec,需要调用 [`CookieJar::apply_to_response`] 才会真正发送。
291    ///
292    /// # 参数
293    ///
294    /// - `name`:Cookie 名称
295    /// - `value`:Cookie 值
296    /// - `options`:Cookie 选项(覆盖默认配置)
297    ///
298    /// # 返回
299    ///
300    /// 返回 `self` 以支持链式调用(Builder 风格)。
301    pub fn set(mut self, name: &str, value: &str, options: CookieOptions) -> Self {
302        // 计算过期时间戳(对齐 PHP `time() + intval($config['expire'])`)
303        let expire = if options.expire > 0 {
304            Utc::now().timestamp() + options.expire
305        } else {
306            0
307        };
308
309        self.response_cookies.push(CookieEntry {
310            name: name.to_string(),
311            value: value.to_string(),
312            expire,
313            options,
314        });
315        self
316    }
317
318    /// 永久保存 Cookie(对齐 PHP `Cookie::forever($name, $value, $option)`)
319    ///
320    /// 设置过期时间为 10 年后(315360000 秒)。
321    pub fn forever(self, name: &str, value: &str, mut options: CookieOptions) -> Self {
322        options.expire = FOREVER_EXPIRE_SECONDS;
323        self.set(name, value, options)
324    }
325
326    /// 删除 Cookie(对齐 PHP `Cookie::delete($name, $options)`)
327    ///
328    /// 通过设置过期时间为过去(当前时间 - 3600 秒)实现删除。
329    pub fn delete(mut self, name: &str, options: CookieOptions) -> Self {
330        let expire = Utc::now().timestamp() + DELETE_EXPIRE_OFFSET_SECONDS;
331        self.response_cookies.push(CookieEntry {
332            name: name.to_string(),
333            value: String::new(),
334            expire,
335            options,
336        });
337        self
338    }
339
340    /// 将所有暂存的 Cookie 写入 Response 的 Set-Cookie 头
341    ///
342    /// 对齐 PHP `Cookie::save()` 的批量发送行为。
343    pub fn apply_to_response<B>(self, resp: &mut Response<B>) {
344        if self.response_cookies.is_empty() {
345            return;
346        }
347
348        let headers = resp.headers_mut();
349        for entry in &self.response_cookies {
350            // 每个 Cookie 一个独立的 Set-Cookie 头(RFC 6265 要求)
351            if let Ok(value) = HeaderValue::from_str(&entry.to_header_string()) {
352                headers.append(HeaderName::from_static("set-cookie"), value);
353            }
354        }
355    }
356
357    /// 获取所有暂存的响应 Cookie(对齐 PHP `Cookie::getCookie()`)
358    pub fn get_response_cookies(&self) -> &[CookieEntry] {
359        &self.response_cookies
360    }
361
362    /// 获取默认配置(不可变引用)
363    pub fn config(&self) -> &CookieOptions {
364        &self.config
365    }
366}
367
368// ============================================================================
369// Cookie 头解析工具函数
370// ============================================================================
371
372/// 解析 Request 的 Cookie 头字符串为 HashMap
373///
374/// 格式:`name1=value1; name2=value2`
375///
376/// 对齐 PHP `think\Request::cookie()` 的解析行为:
377/// - 按 `;` 分割多个 cookie
378/// - 每个条目按第一个 `=` 分割 name 和 value
379/// - 自动 trim 空白字符
380fn parse_cookie_header(header: &str) -> HashMap<String, String> {
381    let mut cookies = HashMap::new();
382    for pair in header.split(';') {
383        let pair = pair.trim();
384        if pair.is_empty() {
385            continue;
386        }
387        if let Some(eq_pos) = pair.find('=') {
388            let name = pair[..eq_pos].trim().to_string();
389            let value = pair[eq_pos + 1..].trim().to_string();
390            if !name.is_empty() {
391                cookies.insert(name, value);
392            }
393        }
394    }
395    cookies
396}
397
398// ============================================================================
399// 单元测试
400// ============================================================================
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use axum::body::Body;
406    use axum::http::{Request, Response};
407
408    // ------------------------------------------------------------------------
409    // CookieOptions 测试
410    // ------------------------------------------------------------------------
411
412    #[test]
413    fn test_cookie_options_default() {
414        let opts = CookieOptions::default();
415        assert_eq!(opts.expire, 0);
416        assert_eq!(opts.path, "/");
417        assert_eq!(opts.domain, "");
418        assert!(!opts.secure);
419        assert!(!opts.httponly);
420        assert_eq!(opts.samesite, "");
421    }
422
423    #[test]
424    fn test_cookie_options_with_expire() {
425        let opts = CookieOptions::with_expire(3600);
426        assert_eq!(opts.expire, 3600);
427        assert_eq!(opts.path, "/"); // 其他字段保持默认
428    }
429
430    // ------------------------------------------------------------------------
431    // CookieEntry::to_header_string 测试
432    // ------------------------------------------------------------------------
433
434    #[test]
435    fn test_cookie_entry_minimal_header() {
436        // 会话 cookie(expire=0),仅 name=value
437        let entry = CookieEntry {
438            name: "token".to_string(),
439            value: "abc123".to_string(),
440            expire: 0,
441            options: CookieOptions {
442                path: String::new(), // 空 path 不输出
443                domain: String::new(),
444                samesite: String::new(),
445                ..Default::default()
446            },
447        };
448        let header = entry.to_header_string();
449        assert_eq!(header, "token=abc123");
450    }
451
452    #[test]
453    fn test_cookie_entry_with_path() {
454        let entry = CookieEntry {
455            name: "token".to_string(),
456            value: "abc".to_string(),
457            expire: 0,
458            options: CookieOptions {
459                path: "/api".to_string(),
460                ..Default::default()
461            },
462        };
463        let header = entry.to_header_string();
464        assert!(header.contains("token=abc"));
465        assert!(header.contains("Path=/api"));
466    }
467
468    #[test]
469    fn test_cookie_entry_with_all_attributes() {
470        let entry = CookieEntry {
471            name: "session".to_string(),
472            value: "xyz".to_string(),
473            expire: 1893456000, // 2030-01-01
474            options: CookieOptions {
475                path: "/".to_string(),
476                domain: "example.com".to_string(),
477                secure: true,
478                httponly: true,
479                samesite: "Lax".to_string(),
480                ..Default::default()
481            },
482        };
483        let header = entry.to_header_string();
484        assert!(header.contains("session=xyz"));
485        assert!(header.contains("Expires="));
486        assert!(header.contains("Path=/"));
487        assert!(header.contains("Domain=example.com"));
488        assert!(header.contains("Secure"));
489        assert!(header.contains("HttpOnly"));
490        assert!(header.contains("SameSite=Lax"));
491    }
492
493    #[test]
494    fn test_cookie_entry_expire_zero_no_expires_header() {
495        // expire=0 表示会话 cookie,不应输出 Expires 头
496        let entry = CookieEntry {
497            name: "session".to_string(),
498            value: "v".to_string(),
499            expire: 0,
500            options: CookieOptions::default(),
501        };
502        let header = entry.to_header_string();
503        assert!(!header.contains("Expires="));
504    }
505
506    // ------------------------------------------------------------------------
507    // CookieJar 基本 API 测试
508    // ------------------------------------------------------------------------
509
510    #[test]
511    fn test_cookie_jar_default_empty() {
512        let jar = CookieJar::new();
513        assert!(jar.get("any").is_none());
514        assert!(!jar.has("any"));
515        assert!(jar.get_response_cookies().is_empty());
516    }
517
518    #[test]
519    fn test_cookie_jar_with_config() {
520        let config = CookieOptions {
521            path: "/app".to_string(),
522            ..Default::default()
523        };
524        let jar = CookieJar::with_config(config);
525        assert_eq!(jar.config().path, "/app");
526    }
527
528    #[test]
529    fn test_cookie_jar_set_adds_to_response_cookies() {
530        let jar = CookieJar::new().set("token", "abc", CookieOptions::default());
531        assert_eq!(jar.get_response_cookies().len(), 1);
532        assert_eq!(jar.get_response_cookies()[0].name, "token");
533        assert_eq!(jar.get_response_cookies()[0].value, "abc");
534    }
535
536    #[test]
537    fn test_cookie_jar_set_chain() {
538        // Builder 风格链式调用
539        let jar = CookieJar::new()
540            .set("a", "1", CookieOptions::default())
541            .set("b", "2", CookieOptions::default())
542            .set("c", "3", CookieOptions::default());
543        assert_eq!(jar.get_response_cookies().len(), 3);
544    }
545
546    #[test]
547    fn test_cookie_jar_set_with_expire_calculates_timestamp() {
548        let before = Utc::now().timestamp();
549        let jar = CookieJar::new().set("token", "abc", CookieOptions::with_expire(3600));
550        let after = Utc::now().timestamp();
551
552        let entry = &jar.get_response_cookies()[0];
553        // expire 应该在 [before + 3600, after + 3600] 范围内
554        assert!(entry.expire >= before + 3600);
555        assert!(entry.expire <= after + 3600);
556    }
557
558    #[test]
559    fn test_cookie_jar_set_expire_zero_keeps_zero() {
560        let jar = CookieJar::new().set("token", "abc", CookieOptions::default());
561        let entry = &jar.get_response_cookies()[0];
562        assert_eq!(entry.expire, 0);
563    }
564
565    #[test]
566    fn test_cookie_jar_forever_sets_10_year_expire() {
567        let before = Utc::now().timestamp();
568        let jar = CookieJar::new().forever("token", "abc", CookieOptions::default());
569
570        let entry = &jar.get_response_cookies()[0];
571        // 检查 expire ≈ now + 10 years
572        let expected_min = before + FOREVER_EXPIRE_SECONDS;
573        assert!(entry.expire >= expected_min);
574    }
575
576    #[test]
577    fn test_cookie_jar_delete_sets_past_expire() {
578        let before = Utc::now().timestamp();
579        let jar = CookieJar::new().delete("token", CookieOptions::default());
580
581        let entry = &jar.get_response_cookies()[0];
582        assert_eq!(entry.value, ""); // 删除 cookie 值为空
583                                     // expire 应该是过去时间(before - 3600 附近)
584        assert!(entry.expire < before);
585    }
586
587    // ------------------------------------------------------------------------
588    // CookieJar::from_request 测试
589    // ------------------------------------------------------------------------
590
591    #[test]
592    fn test_from_request_no_cookie_header() {
593        let req = Request::<Body>::default();
594        let jar = CookieJar::from_request(&req);
595        assert!(jar.get("any").is_none());
596    }
597
598    #[test]
599    fn test_from_request_single_cookie() {
600        let mut req = Request::<Body>::default();
601        req.headers_mut().insert(
602            axum::http::header::COOKIE,
603            HeaderValue::from_static("token=abc123"),
604        );
605        let jar = CookieJar::from_request(&req);
606        assert_eq!(jar.get("token"), Some("abc123".to_string()));
607        assert!(jar.has("token"));
608    }
609
610    #[test]
611    fn test_from_request_multiple_cookies() {
612        let mut req = Request::<Body>::default();
613        req.headers_mut().insert(
614            axum::http::header::COOKIE,
615            HeaderValue::from_static("token=abc; user=42; theme=dark"),
616        );
617        let jar = CookieJar::from_request(&req);
618        assert_eq!(jar.get("token"), Some("abc".to_string()));
619        assert_eq!(jar.get("user"), Some("42".to_string()));
620        assert_eq!(jar.get("theme"), Some("dark".to_string()));
621    }
622
623    #[test]
624    fn test_from_request_cookie_with_whitespace() {
625        let mut req = Request::<Body>::default();
626        req.headers_mut().insert(
627            axum::http::header::COOKIE,
628            HeaderValue::from_static("  token = abc  ;  user = 42  "),
629        );
630        let jar = CookieJar::from_request(&req);
631        assert_eq!(jar.get("token"), Some("abc".to_string()));
632        assert_eq!(jar.get("user"), Some("42".to_string()));
633    }
634
635    #[test]
636    fn test_from_request_cookie_value_with_equals() {
637        // 值中包含 = 字符(仅按第一个 = 分割)
638        let mut req = Request::<Body>::default();
639        req.headers_mut().insert(
640            axum::http::header::COOKIE,
641            HeaderValue::from_static("data=a=b=c"),
642        );
643        let jar = CookieJar::from_request(&req);
644        assert_eq!(jar.get("data"), Some("a=b=c".to_string()));
645    }
646
647    #[test]
648    fn test_from_request_empty_cookie_header() {
649        let mut req = Request::<Body>::default();
650        req.headers_mut()
651            .insert(axum::http::header::COOKIE, HeaderValue::from_static(""));
652        let jar = CookieJar::from_request(&req);
653        assert!(jar.get("any").is_none());
654    }
655
656    #[test]
657    fn test_from_request_malformed_pairs_ignored() {
658        let mut req = Request::<Body>::default();
659        req.headers_mut().insert(
660            axum::http::header::COOKIE,
661            HeaderValue::from_static("token=abc; malformed; =empty_name; valid=ok"),
662        );
663        let jar = CookieJar::from_request(&req);
664        assert_eq!(jar.get("token"), Some("abc".to_string()));
665        assert!(jar.get("malformed").is_none()); // 无 = 分隔
666        assert!(jar.get("").is_none()); // 空名被忽略
667        assert_eq!(jar.get("valid"), Some("ok".to_string()));
668    }
669
670    #[test]
671    fn test_get_with_default_returns_value_when_exists() {
672        let mut req = Request::<Body>::default();
673        req.headers_mut().insert(
674            axum::http::header::COOKIE,
675            HeaderValue::from_static("name=alice"),
676        );
677        let jar = CookieJar::from_request(&req);
678        assert_eq!(jar.get_with_default("name", "guest"), "alice");
679    }
680
681    #[test]
682    fn test_get_with_default_returns_default_when_missing() {
683        let req = Request::<Body>::default();
684        let jar = CookieJar::from_request(&req);
685        assert_eq!(jar.get_with_default("name", "guest"), "guest");
686    }
687
688    // ------------------------------------------------------------------------
689    // CookieJar::apply_to_response 测试
690    // ------------------------------------------------------------------------
691
692    #[test]
693    fn test_apply_to_response_no_cookies() {
694        let jar = CookieJar::new();
695        let mut resp = Response::new(Body::empty());
696        jar.apply_to_response(&mut resp);
697        assert!(resp.headers().get("set-cookie").is_none());
698    }
699
700    #[test]
701    fn test_apply_to_response_single_cookie() {
702        let jar = CookieJar::new().set("token", "abc", CookieOptions::default());
703        let mut resp = Response::new(Body::empty());
704        jar.apply_to_response(&mut resp);
705
706        let set_cookies: Vec<_> = resp.headers().get_all("set-cookie").iter().collect();
707        assert_eq!(set_cookies.len(), 1);
708        assert_eq!(set_cookies[0].to_str().unwrap(), "token=abc; Path=/");
709    }
710
711    #[test]
712    fn test_apply_to_response_multiple_cookies() {
713        let jar = CookieJar::new()
714            .set("a", "1", CookieOptions::default())
715            .set("b", "2", CookieOptions::default())
716            .set("c", "3", CookieOptions::default());
717        let mut resp = Response::new(Body::empty());
718        jar.apply_to_response(&mut resp);
719
720        let set_cookies: Vec<_> = resp
721            .headers()
722            .get_all("set-cookie")
723            .iter()
724            .map(|v| v.to_str().unwrap().to_string())
725            .collect();
726        assert_eq!(set_cookies.len(), 3);
727        assert!(set_cookies.contains(&"a=1; Path=/".to_string()));
728        assert!(set_cookies.contains(&"b=2; Path=/".to_string()));
729        assert!(set_cookies.contains(&"c=3; Path=/".to_string()));
730    }
731
732    #[test]
733    fn test_apply_to_response_with_all_attributes() {
734        let jar = CookieJar::new().set(
735            "session",
736            "xyz",
737            CookieOptions {
738                expire: 1893456000, // 2030-01-01
739                path: "/".to_string(),
740                domain: "example.com".to_string(),
741                secure: true,
742                httponly: true,
743                samesite: "Strict".to_string(),
744            },
745        );
746        let mut resp = Response::new(Body::empty());
747        jar.apply_to_response(&mut resp);
748
749        let header = resp
750            .headers()
751            .get("set-cookie")
752            .unwrap()
753            .to_str()
754            .unwrap()
755            .to_string();
756        assert!(header.contains("session=xyz"));
757        assert!(header.contains("Expires="));
758        assert!(header.contains("Path=/"));
759        assert!(header.contains("Domain=example.com"));
760        assert!(header.contains("Secure"));
761        assert!(header.contains("HttpOnly"));
762        assert!(header.contains("SameSite=Strict"));
763    }
764
765    // ------------------------------------------------------------------------
766    // parse_cookie_header 工具函数测试
767    // ------------------------------------------------------------------------
768
769    #[test]
770    fn test_parse_empty_header() {
771        let cookies = parse_cookie_header("");
772        assert!(cookies.is_empty());
773    }
774
775    #[test]
776    fn test_parse_single_pair() {
777        let cookies = parse_cookie_header("name=value");
778        assert_eq!(cookies.get("name"), Some(&"value".to_string()));
779    }
780
781    #[test]
782    fn test_parse_multiple_pairs() {
783        let cookies = parse_cookie_header("a=1; b=2; c=3");
784        assert_eq!(cookies.len(), 3);
785        assert_eq!(cookies.get("a"), Some(&"1".to_string()));
786        assert_eq!(cookies.get("b"), Some(&"2".to_string()));
787        assert_eq!(cookies.get("c"), Some(&"3".to_string()));
788    }
789
790    #[test]
791    fn test_parse_trims_whitespace() {
792        let cookies = parse_cookie_header("  a = 1  ;  b = 2  ");
793        assert_eq!(cookies.get("a"), Some(&"1".to_string()));
794        assert_eq!(cookies.get("b"), Some(&"2".to_string()));
795    }
796
797    #[test]
798    fn test_parse_skips_empty_pairs() {
799        let cookies = parse_cookie_header("a=1;; ;b=2");
800        assert_eq!(cookies.len(), 2);
801        assert_eq!(cookies.get("a"), Some(&"1".to_string()));
802        assert_eq!(cookies.get("b"), Some(&"2".to_string()));
803    }
804
805    #[test]
806    fn test_parse_skips_no_equals() {
807        let cookies = parse_cookie_header("a=1; invalid; b=2");
808        assert_eq!(cookies.len(), 2);
809        assert!(!cookies.contains_key("invalid"));
810    }
811
812    #[test]
813    fn test_parse_skips_empty_name() {
814        let cookies = parse_cookie_header("a=1; =empty; b=2");
815        assert_eq!(cookies.len(), 2);
816        assert!(!cookies.contains_key(""));
817    }
818
819    // ------------------------------------------------------------------------
820    // PHP 一致性综合流程测试
821    // ------------------------------------------------------------------------
822
823    #[test]
824    fn test_php_consistency_set_and_save_flow() {
825        // 模拟 PHP 流程:set() 暂存 → save() 发送
826        let req = Request::<Body>::default();
827        let jar =
828            CookieJar::from_request(&req).set("token", "abc123", CookieOptions::with_expire(3600));
829
830        let mut resp = Response::new(Body::empty());
831        jar.apply_to_response(&mut resp);
832
833        let header = resp.headers().get("set-cookie").unwrap().to_str().unwrap();
834        assert!(header.starts_with("token=abc123"));
835        assert!(header.contains("Expires="));
836        assert!(header.contains("Path=/"));
837    }
838
839    #[test]
840    fn test_php_consistency_delete_flow() {
841        // 模拟 PHP delete() 流程
842        let req = Request::<Body>::default();
843        let jar = CookieJar::from_request(&req).delete("token", CookieOptions::default());
844
845        let mut resp = Response::new(Body::empty());
846        jar.apply_to_response(&mut resp);
847
848        let header = resp.headers().get("set-cookie").unwrap().to_str().unwrap();
849        // 删除 cookie:值空 + Expires 为过去时间
850        assert!(header.starts_with("token="));
851        assert!(header.contains("Expires="));
852    }
853
854    #[test]
855    fn test_php_consistency_forever_flow() {
856        let req = Request::<Body>::default();
857        let jar = CookieJar::from_request(&req).forever("pref", "dark", CookieOptions::default());
858
859        let mut resp = Response::new(Body::empty());
860        jar.apply_to_response(&mut resp);
861
862        let header = resp.headers().get("set-cookie").unwrap().to_str().unwrap();
863        assert!(header.contains("pref=dark"));
864        assert!(header.contains("Expires="));
865    }
866
867    #[test]
868    fn test_php_consistency_request_response_isolation() {
869        // 请求 cookie 和响应 cookie 是隔离的
870        let mut req = Request::<Body>::default();
871        req.headers_mut().insert(
872            axum::http::header::COOKIE,
873            HeaderValue::from_static("old=value"),
874        );
875        let jar = CookieJar::from_request(&req).set("new", "value", CookieOptions::default());
876
877        // 请求 cookie 可读
878        assert_eq!(jar.get("old"), Some("value".to_string()));
879        // 响应 cookie 不会出现在请求 cookie 中
880        assert!(jar.get("new").is_none());
881        // 响应 cookie 在 response_cookies 中
882        assert_eq!(jar.get_response_cookies().len(), 1);
883        assert_eq!(jar.get_response_cookies()[0].name, "new");
884    }
885}