Skip to main content

salvo_session/
lib.rs

1//! # Salvo Session Support
2//!
3//! Salvo's session middleware is built on top of
4//! [`saysion`](https://github.com/salvo-rs/saysion).
5//!
6//! See a complete example: [`session-login`](https://github.com/salvo-rs/salvo/tree/main/examples/session-login)
7//!
8//! Sessions allow Salvo applications to securely attach data to browser sessions,
9//! enabling retrieval and modification of this data on subsequent visits.
10//! Session data is typically retained only for the duration of a browser session.
11//!
12//! ## Stores
13//!
14//! It is highly recommended to use an external-datastore-backed session storage
15//! for production Salvo applications. For a list of currently available session
16//! stores, see [the documentation for saysion](https://github.com/salvo-rs/saysion).
17//!
18//! ## Security
19//!
20//! While each session store may have different security implications,
21//! Salvo's session system works as follows:
22//!
23//! On each request, Salvo checks for the cookie specified by `cookie_name`
24//! in the handler configuration.
25//!
26//! ### When no cookie is found:
27//!
28//! 1. A cryptographically random cookie value is generated
29//! 2. A cookie is set on the outbound response and signed with an HKDF key derived from the
30//!    `secret` provided when creating the SessionHandler
31//! 3. The session store uses a SHA256 digest of the cookie value to store the session along with an
32//!    optional expiry time
33//!
34//! ### When a cookie is found:
35//!
36//! 1. The HKDF-derived signing key verifies the cookie value's signature
37//! 2. If verification succeeds, the value is passed to the session store to retrieve the associated
38//!    Session
39//! 3. For most session stores, this involves taking a SHA256 digest of the cookie value and
40//!    retrieving a serialized Session from an external datastore
41//!
42//! ### Expiry Handling
43//!
44//! Sessions include expiry information in both the cookie and the serialization format.
45//! Even if an adversary tampers with a cookie's expiry, Salvo validates
46//! the expiry on the contained session before using it.
47//!
48//! ### Error Handling
49//!
50//! If any failures occur during session retrieval, a new empty session
51//! is generated for the request, which proceeds through the application normally.
52//!
53//! ## Stale/Expired Session Cleanup
54//!
55//! Any session store (except the cookie store) will accumulate stale sessions over time.
56//! Although Salvo ensures expired sessions won't be used, it remains the
57//! application's responsibility to periodically call cleanup on the session
58//! store if required.
59//!
60//! Read more: <https://salvo.rs>
61#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
62#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
63#![cfg_attr(docsrs, feature(doc_cfg))]
64
65use std::fmt::{self, Formatter};
66use std::time::Duration;
67
68use cookie::{Cookie, Key, KeyError, SameSite};
69use salvo_core::http::uri::Scheme;
70use salvo_core::{Depot, Error, FlowCtrl, Handler, Request, Response, async_trait};
71use saysion::base64::Engine as _;
72use saysion::base64::engine::general_purpose;
73use saysion::hmac::{Hmac, Mac};
74use saysion::sha2::Sha256;
75pub use saysion::{CookieStore, MemoryStore, Session, SessionStore};
76
77/// Key for store data in depot.
78pub const SESSION_KEY: &str = "::salvo::session";
79const BASE64_DIGEST_LEN: usize = 44;
80
81/// Trait for `Depot` to get and set session.
82pub trait SessionDepotExt {
83    /// Sets session
84    fn set_session(&mut self, session: Session) -> &mut Self;
85    /// Take session
86    fn take_session(&mut self) -> Option<Session>;
87    /// Get session reference
88    fn session(&self) -> Option<&Session>;
89    /// Get session mutable reference
90    fn session_mut(&mut self) -> Option<&mut Session>;
91}
92
93impl SessionDepotExt for Depot {
94    #[inline]
95    fn set_session(&mut self, session: Session) -> &mut Self {
96        self.insert(SESSION_KEY, session);
97        self
98    }
99    #[inline]
100    fn take_session(&mut self) -> Option<Session> {
101        self.remove(SESSION_KEY).ok()
102    }
103    #[inline]
104    fn session(&self) -> Option<&Session> {
105        self.get(SESSION_KEY).ok()
106    }
107    #[inline]
108    fn session_mut(&mut self) -> Option<&mut Session> {
109        self.get_mut(SESSION_KEY).ok()
110    }
111}
112
113/// `HandlerBuilder` is a builder for [`SessionHandler`].
114pub struct HandlerBuilder<S> {
115    store: S,
116    cookie_path: String,
117    cookie_name: String,
118    cookie_domain: Option<String>,
119    session_ttl: Option<Duration>,
120    save_unchanged: bool,
121    same_site_policy: SameSite,
122    key: Key,
123    fallback_keys: Vec<Key>,
124}
125impl<S> fmt::Debug for HandlerBuilder<S>
126where
127    S: SessionStore + fmt::Debug,
128{
129    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
130        f.debug_struct("HandlerBuilder")
131            .field("store", &self.store)
132            .field("cookie_path", &self.cookie_path)
133            .field("cookie_name", &self.cookie_name)
134            .field("cookie_domain", &self.cookie_domain)
135            .field("session_ttl", &self.session_ttl)
136            .field("same_site_policy", &self.same_site_policy)
137            .field("key", &"..")
138            .field("fallback_keys", &"..")
139            .field("save_unchanged", &self.save_unchanged)
140            .finish()
141    }
142}
143
144impl<S> HandlerBuilder<S>
145where
146    S: SessionStore,
147{
148    /// Create new `HandlerBuilder`
149    ///
150    /// # Security Note
151    ///
152    /// The `secret` must be at least 64 bytes (512 bits) for adequate security. Will panic on keys
153    /// shorter than 64 bytes. For non-panicking version, use [`HandlerBuilder::try_new`].
154    ///
155    /// **Example of generating a secure key:**
156    /// ```ignore
157    /// use rand::Rng;
158    /// let mut key = [0u8; 64];
159    /// rand::rngs::SysRng.fill_bytes(&mut key);
160    /// ```
161    #[inline]
162    #[must_use]
163    pub fn new(store: S, secret: &[u8]) -> Self {
164        Self::try_new(store, secret).unwrap()
165    }
166
167    /// Try creating new `HandlerBuilder`
168    ///
169    /// # Security Note
170    ///
171    /// The `secret` must be at least 64 bytes (512 bits) for adequate security.
172    ///
173    /// **Example of generating a secure key:**
174    /// ```ignore
175    /// use rand::Rng;
176    /// let mut key = [0u8; 64];
177    /// rand::rngs::SysRng.fill_bytes(&mut key);
178    /// ```
179    #[inline]
180    pub fn try_new(store: S, secret: &[u8]) -> Result<Self, KeyError> {
181        let key = Key::try_from(secret)?;
182        Ok(Self {
183            store,
184            save_unchanged: true,
185            cookie_path: "/".into(),
186            cookie_name: "salvo.session.id".into(),
187            cookie_domain: None,
188            same_site_policy: SameSite::Lax,
189            session_ttl: Some(Duration::from_secs(24 * 60 * 60)),
190            key,
191            fallback_keys: vec![],
192        })
193    }
194
195    /// Sets a cookie path for this session middleware.
196    ///
197    /// The default for this value is "/".
198    #[inline]
199    #[must_use]
200    pub fn cookie_path(mut self, cookie_path: impl Into<String>) -> Self {
201        self.cookie_path = cookie_path.into();
202        self
203    }
204
205    /// Sets a session ttl. This will be used both for the cookie
206    /// expiry and also for the session-internal expiry.
207    ///
208    /// The default for this value is one day. Set this to None to not
209    /// set a cookie or session expiry. This is not recommended.
210    #[inline]
211    #[must_use]
212    pub fn session_ttl(mut self, session_ttl: Option<Duration>) -> Self {
213        self.session_ttl = session_ttl;
214        self
215    }
216
217    /// Sets the name of the cookie that the session is stored with or in.
218    ///
219    /// If you are running multiple tide applications on the same
220    /// domain, you will need different values for each
221    /// application. The default value is "salvo.session_id".
222    #[inline]
223    #[must_use]
224    pub fn cookie_name(mut self, cookie_name: impl Into<String>) -> Self {
225        self.cookie_name = cookie_name.into();
226        self
227    }
228
229    /// Sets the `save_unchanged` value.
230    ///
231    /// When `save_unchanged` is enabled, a session will cookie will always be set.
232    ///
233    /// With `save_unchanged` disabled, the session data must be modified
234    /// from the `Default` value in order for it to save. If a session
235    /// already exists and its data unmodified in the course of a
236    /// request, the session will only be persisted if
237    /// `save_unchanged` is enabled.
238    #[inline]
239    #[must_use]
240    pub fn save_unchanged(mut self, value: bool) -> Self {
241        self.save_unchanged = value;
242        self
243    }
244
245    /// Sets the same site policy for the session cookie. Defaults to
246    /// SameSite::Lax. See [incrementally better
247    /// cookies](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01)
248    /// for more information about this setting.
249    #[inline]
250    #[must_use]
251    pub fn same_site_policy(mut self, policy: SameSite) -> Self {
252        self.same_site_policy = policy;
253        self
254    }
255
256    /// Sets the domain of the cookie.
257    #[inline]
258    #[must_use]
259    pub fn cookie_domain(mut self, cookie_domain: impl AsRef<str>) -> Self {
260        self.cookie_domain = Some(cookie_domain.as_ref().to_owned());
261        self
262    }
263    /// Sets fallbacks.
264    #[inline]
265    #[must_use]
266    pub fn fallback_keys(mut self, keys: Vec<impl Into<Key>>) -> Self {
267        self.fallback_keys = keys.into_iter().map(|s| s.into()).collect();
268        self
269    }
270
271    /// Add fallback secret.
272    #[inline]
273    #[must_use]
274    pub fn add_fallback_key(mut self, key: impl Into<Key>) -> Self {
275        self.fallback_keys.push(key.into());
276        self
277    }
278
279    /// Build `SessionHandler`
280    pub fn build(self) -> Result<SessionHandler<S>, Error> {
281        let Self {
282            store,
283            save_unchanged,
284            cookie_path,
285            cookie_name,
286            cookie_domain,
287            session_ttl,
288            same_site_policy,
289            key,
290            fallback_keys,
291        } = self;
292        let hmac = Hmac::<Sha256>::new_from_slice(key.signing())
293            .map_err(|_| Error::Other("invalid key length".into()))?;
294        let fallback_hmacs = fallback_keys
295            .iter()
296            .map(|key| Hmac::<Sha256>::new_from_slice(key.signing()))
297            .collect::<Result<Vec<_>, _>>()
298            .map_err(|_| Error::Other("invalid key length".into()))?;
299        Ok(SessionHandler {
300            store,
301            save_unchanged,
302            cookie_path,
303            cookie_name,
304            cookie_domain,
305            session_ttl,
306            same_site_policy,
307            hmac,
308            fallback_hmacs,
309        })
310    }
311}
312
313/// `SessionHandler` is a middleware for session.
314pub struct SessionHandler<S> {
315    store: S,
316    cookie_path: String,
317    cookie_name: String,
318    cookie_domain: Option<String>,
319    session_ttl: Option<Duration>,
320    save_unchanged: bool,
321    same_site_policy: SameSite,
322    hmac: Hmac<Sha256>,
323    fallback_hmacs: Vec<Hmac<Sha256>>,
324}
325impl<S> fmt::Debug for SessionHandler<S>
326where
327    S: SessionStore + fmt::Debug,
328{
329    #[inline]
330    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
331        f.debug_struct("SessionHandler")
332            .field("store", &self.store)
333            .field("cookie_path", &self.cookie_path)
334            .field("cookie_name", &self.cookie_name)
335            .field("cookie_domain", &self.cookie_domain)
336            .field("session_ttl", &self.session_ttl)
337            .field("same_site_policy", &self.same_site_policy)
338            .field("key", &"..")
339            .field("fallback_keys", &"..")
340            .field("save_unchanged", &self.save_unchanged)
341            .finish()
342    }
343}
344#[async_trait]
345impl<S> Handler for SessionHandler<S>
346where
347    S: SessionStore + Send + Sync + 'static,
348{
349    async fn handle(
350        &self,
351        req: &mut Request,
352        depot: &mut Depot,
353        res: &mut Response,
354        ctrl: &mut FlowCtrl,
355    ) {
356        let cookie = req.cookies().get(&self.cookie_name);
357        let cookie_value = cookie.and_then(|cookie| self.verify_signature(cookie.value()).ok());
358
359        let mut session = self.load_or_create(cookie_value).await;
360
361        if let Some(ttl) = self.session_ttl {
362            session.expire_in(ttl);
363        }
364
365        depot.set_session(session);
366
367        ctrl.call_next(req, depot, res).await;
368        if ctrl.is_ceased() {
369            return;
370        }
371
372        let session = depot.take_session().expect("session should exist in depot");
373        if session.is_destroyed() {
374            if let Err(e) = self.store.destroy_session(session).await {
375                tracing::error!(error = ?e, "unable to destroy session");
376            }
377            res.remove_cookie(&self.cookie_name);
378        } else if self.save_unchanged || session.data_changed() {
379            match self.store.store_session(session).await {
380                Ok(cookie_value) => {
381                    if let Some(cookie_value) = cookie_value {
382                        let secure_cookie = req.uri().scheme() == Some(&Scheme::HTTPS);
383                        let cookie = self.build_cookie(secure_cookie, cookie_value);
384                        res.add_cookie(cookie);
385                    }
386                }
387                Err(e) => {
388                    tracing::error!(error = ?e, "store session error");
389                }
390            }
391        }
392    }
393}
394
395impl<S> SessionHandler<S>
396where
397    S: SessionStore + Send + Sync + 'static,
398{
399    /// Create new `HandlerBuilder`
400    pub fn builder(store: S, secret: &[u8]) -> HandlerBuilder<S> {
401        HandlerBuilder::new(store, secret)
402    }
403    #[inline]
404    async fn load_or_create(&self, cookie_value: Option<String>) -> Session {
405        let session = match cookie_value {
406            Some(cookie_value) => self.store.load_session(cookie_value).await.ok().flatten(),
407            None => None,
408        };
409
410        session
411            .and_then(|session| session.validate())
412            .unwrap_or_default()
413    }
414    // the following is reused verbatim from
415    // https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L51-L66
416    /// Given a signed value `str` where the signature is prepended to `value`,
417    /// verifies the signed value and returns it. If there's a problem, returns
418    /// an `Err` with a string describing the issue.
419    fn verify_signature(&self, cookie_value: &str) -> Result<String, Error> {
420        if cookie_value.len() < BASE64_DIGEST_LEN {
421            return Err(Error::Other(
422                "length of value is <= BASE64_DIGEST_LEN".into(),
423            ));
424        }
425
426        // Split [MAC | original-value] into its two parts.
427        let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
428        let digest = general_purpose::STANDARD
429            .decode(digest_str)
430            .map_err(|_| Error::Other("bad base64 digest".into()))?;
431
432        // Perform the verification.
433        let mut hmac = self.hmac.clone();
434        hmac.update(value.as_bytes());
435        if hmac.verify_slice(&digest).is_ok() {
436            return Ok(value.to_owned());
437        }
438        for hmac in &self.fallback_hmacs {
439            let mut hmac = hmac.clone();
440            hmac.update(value.as_bytes());
441            if hmac.verify_slice(&digest).is_ok() {
442                return Ok(value.to_owned());
443            }
444        }
445        Err(Error::Other("value did not verify".into()))
446    }
447    fn build_cookie(&self, secure: bool, cookie_value: String) -> Cookie<'static> {
448        let mut cookie = Cookie::build((self.cookie_name.clone(), cookie_value))
449            .http_only(true)
450            .same_site(self.same_site_policy)
451            .secure(secure)
452            .path(self.cookie_path.clone())
453            .build();
454
455        if let Some(ttl) = self.session_ttl {
456            cookie.set_expires(Some((std::time::SystemTime::now() + ttl).into()));
457        }
458
459        if let Some(cookie_domain) = self.cookie_domain.clone() {
460            cookie.set_domain(cookie_domain)
461        }
462
463        self.sign_cookie(&mut cookie);
464
465        cookie
466    }
467    // The following is reused verbatim from
468    // https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L37-46
469    /// signs the cookie's value providing integrity and authenticity.
470    fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
471        // Compute HMAC-SHA256 of the cookie's value.
472        let mut mac = self.hmac.clone();
473        mac.update(cookie.value().as_bytes());
474
475        // Cookie's new value is [MAC | original-value].
476        let mut new_value = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
477        new_value.push_str(cookie.value());
478        cookie.set_value(new_value);
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use salvo_core::http::Method;
485    use salvo_core::http::header::*;
486    use salvo_core::prelude::*;
487    use salvo_core::test::{ResponseExt, TestClient};
488
489    use super::*;
490
491    #[test]
492    fn test_session_data() {
493        let builder = SessionHandler::builder(
494            saysion::CookieStore,
495            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
496        )
497        .cookie_domain("test.domain")
498        .cookie_name("test_cookie")
499        .cookie_path("/abc")
500        .same_site_policy(SameSite::Strict)
501        .session_ttl(Some(Duration::from_secs(30)));
502        assert!(format!("{builder:?}").contains("test_cookie"));
503
504        let handler = builder.build().unwrap();
505        assert!(format!("{handler:?}").contains("test_cookie"));
506        assert_eq!(handler.cookie_domain, Some("test.domain".into()));
507        assert_eq!(handler.cookie_name, "test_cookie");
508        assert_eq!(handler.cookie_path, "/abc");
509        assert_eq!(handler.same_site_policy, SameSite::Strict);
510        assert_eq!(handler.session_ttl, Some(Duration::from_secs(30)));
511    }
512
513    #[tokio::test]
514    async fn test_session_login() {
515        #[handler]
516        pub async fn login(req: &mut Request, depot: &mut Depot, res: &mut Response) {
517            if req.method() == Method::POST {
518                let mut session = Session::new();
519                session
520                    .insert("username", req.form::<String>("username").await.unwrap())
521                    .unwrap();
522                depot.set_session(session);
523                res.render(Redirect::other("/"));
524            } else {
525                res.render(Text::Html("login page"));
526            }
527        }
528
529        #[handler]
530        pub async fn logout(depot: &mut Depot, res: &mut Response) {
531            if let Some(session) = depot.session_mut() {
532                session.remove("username");
533            }
534            res.render(Redirect::other("/"));
535        }
536
537        #[handler]
538        pub async fn home(depot: &mut Depot, res: &mut Response) {
539            let mut content = r#"home"#.into();
540            if let Some(session) = depot.session_mut() {
541                if let Some(username) = session.get::<String>("username") {
542                    content = username;
543                }
544            }
545            res.render(Text::Html(content));
546        }
547
548        let session_handler = SessionHandler::builder(
549            MemoryStore::new(),
550            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
551        )
552        .build()
553        .unwrap();
554        let router = Router::new()
555            .hoop(session_handler)
556            .get(home)
557            .push(Router::with_path("login").get(login).post(login))
558            .push(Router::with_path("logout").get(logout));
559        let service = Service::new(router);
560
561        let response = TestClient::post("http://127.0.0.1:8698/login")
562            .raw_form("username=salvo")
563            .send(&service)
564            .await;
565        assert_eq!(response.status_code, Some(StatusCode::SEE_OTHER));
566        let cookie = response.headers().get(SET_COOKIE).unwrap();
567
568        let mut response = TestClient::get("http://127.0.0.1:8698/")
569            .add_header(COOKIE, cookie, true)
570            .send(&service)
571            .await;
572        assert_eq!(response.take_string().await.unwrap(), "salvo");
573
574        let response = TestClient::get("http://127.0.0.1:8698/logout")
575            .send(&service)
576            .await;
577        assert_eq!(response.status_code, Some(StatusCode::SEE_OTHER));
578
579        let mut response = TestClient::get("http://127.0.0.1:8698/")
580            .send(&service)
581            .await;
582        assert_eq!(response.take_string().await.unwrap(), "home");
583    }
584
585    // Tests for HandlerBuilder
586    #[test]
587    fn test_handler_builder_new() {
588        let builder = HandlerBuilder::new(
589            MemoryStore::new(),
590            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
591        );
592        assert_eq!(builder.cookie_path, "/");
593        assert_eq!(builder.cookie_name, "salvo.session.id");
594        assert!(builder.cookie_domain.is_none());
595        assert!(builder.save_unchanged);
596        assert_eq!(builder.same_site_policy, SameSite::Lax);
597        assert_eq!(builder.session_ttl, Some(Duration::from_secs(24 * 60 * 60)));
598    }
599
600    #[test]
601    fn test_handler_builder_cookie_path() {
602        let builder = HandlerBuilder::new(
603            MemoryStore::new(),
604            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
605        )
606        .cookie_path("/custom");
607        assert_eq!(builder.cookie_path, "/custom");
608    }
609
610    #[test]
611    fn test_handler_builder_session_ttl() {
612        let builder = HandlerBuilder::new(
613            MemoryStore::new(),
614            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
615        )
616        .session_ttl(Some(Duration::from_secs(3600)));
617        assert_eq!(builder.session_ttl, Some(Duration::from_secs(3600)));
618    }
619
620    #[test]
621    fn test_handler_builder_session_ttl_none() {
622        let builder = HandlerBuilder::new(
623            MemoryStore::new(),
624            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
625        )
626        .session_ttl(None);
627        assert!(builder.session_ttl.is_none());
628    }
629
630    #[test]
631    fn test_handler_builder_cookie_name() {
632        let builder = HandlerBuilder::new(
633            MemoryStore::new(),
634            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
635        )
636        .cookie_name("my_session");
637        assert_eq!(builder.cookie_name, "my_session");
638    }
639
640    #[test]
641    fn test_handler_builder_save_unchanged() {
642        let builder = HandlerBuilder::new(
643            MemoryStore::new(),
644            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
645        )
646        .save_unchanged(false);
647        assert!(!builder.save_unchanged);
648    }
649
650    #[test]
651    fn test_handler_builder_same_site_policy() {
652        let builder = HandlerBuilder::new(
653            MemoryStore::new(),
654            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
655        )
656        .same_site_policy(SameSite::None);
657        assert_eq!(builder.same_site_policy, SameSite::None);
658    }
659
660    #[test]
661    fn test_handler_builder_cookie_domain() {
662        let builder = HandlerBuilder::new(
663            MemoryStore::new(),
664            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
665        )
666        .cookie_domain("example.com");
667        assert_eq!(builder.cookie_domain, Some("example.com".to_string()));
668    }
669
670    #[test]
671    fn test_handler_builder_fallback_keys() {
672        let builder = HandlerBuilder::new(
673            MemoryStore::new(),
674            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
675        )
676        .fallback_keys(vec![Key::from(
677            b"fallbackfallbackfallbackfallbackfallbackfallbackfallbackfallback" as &[u8],
678        )]);
679        assert_eq!(builder.fallback_keys.len(), 1);
680    }
681
682    #[test]
683    fn test_handler_builder_add_fallback_key() {
684        let builder = HandlerBuilder::new(
685            MemoryStore::new(),
686            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
687        )
688        .add_fallback_key(Key::from(
689            b"fallbackfallbackfallbackfallbackfallbackfallbackfallbackfallback" as &[u8],
690        ))
691        .add_fallback_key(Key::from(
692            b"anotherkeyanotherkeyanotherkeyanotherkeyanotherkeyanotherkeyanot" as &[u8],
693        ));
694        assert_eq!(builder.fallback_keys.len(), 2);
695    }
696
697    #[test]
698    fn test_handler_builder_build() {
699        let handler = HandlerBuilder::new(
700            MemoryStore::new(),
701            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
702        )
703        .build()
704        .unwrap();
705        assert_eq!(handler.cookie_path, "/");
706        assert_eq!(handler.cookie_name, "salvo.session.id");
707    }
708
709    #[test]
710    fn test_handler_builder_debug() {
711        let builder = HandlerBuilder::new(
712            MemoryStore::new(),
713            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
714        );
715        let debug_str = format!("{:?}", builder);
716        assert!(debug_str.contains("HandlerBuilder"));
717        assert!(debug_str.contains("cookie_path"));
718        assert!(debug_str.contains("cookie_name"));
719    }
720
721    #[test]
722    fn test_handler_builder_chain() {
723        let handler = HandlerBuilder::new(
724            MemoryStore::new(),
725            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
726        )
727        .cookie_path("/app")
728        .cookie_name("app_session")
729        .cookie_domain("app.example.com")
730        .session_ttl(Some(Duration::from_secs(7200)))
731        .save_unchanged(false)
732        .same_site_policy(SameSite::Strict)
733        .build()
734        .unwrap();
735
736        assert_eq!(handler.cookie_path, "/app");
737        assert_eq!(handler.cookie_name, "app_session");
738        assert_eq!(handler.cookie_domain, Some("app.example.com".to_string()));
739        assert_eq!(handler.session_ttl, Some(Duration::from_secs(7200)));
740        assert!(!handler.save_unchanged);
741        assert_eq!(handler.same_site_policy, SameSite::Strict);
742    }
743
744    // Tests for SessionHandler
745    #[test]
746    fn test_session_handler_builder() {
747        let handler = SessionHandler::builder(
748            MemoryStore::new(),
749            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
750        )
751        .build()
752        .unwrap();
753        assert_eq!(handler.cookie_name, "salvo.session.id");
754    }
755
756    #[test]
757    fn test_session_handler_debug() {
758        let handler = SessionHandler::builder(
759            MemoryStore::new(),
760            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
761        )
762        .build()
763        .unwrap();
764        let debug_str = format!("{:?}", handler);
765        assert!(debug_str.contains("SessionHandler"));
766        assert!(debug_str.contains("cookie_path"));
767    }
768
769    // Tests for SessionDepotExt
770    #[test]
771    fn test_depot_set_session() {
772        let mut depot = Depot::new();
773        let session = Session::new();
774        depot.set_session(session);
775        assert!(depot.session().is_some());
776    }
777
778    #[test]
779    fn test_depot_take_session() {
780        let mut depot = Depot::new();
781        let session = Session::new();
782        depot.set_session(session);
783        let taken = depot.take_session();
784        assert!(taken.is_some());
785        assert!(depot.session().is_none());
786    }
787
788    #[test]
789    fn test_depot_session() {
790        let mut depot = Depot::new();
791        assert!(depot.session().is_none());
792
793        depot.set_session(Session::new());
794        assert!(depot.session().is_some());
795    }
796
797    #[test]
798    fn test_depot_session_mut() {
799        let mut depot = Depot::new();
800        depot.set_session(Session::new());
801
802        if let Some(session) = depot.session_mut() {
803            session.insert("key", "value").unwrap();
804        }
805
806        if let Some(session) = depot.session() {
807            assert_eq!(session.get::<String>("key"), Some("value".to_string()));
808        }
809    }
810
811    // Tests for session with destroyed state
812    #[tokio::test]
813    async fn test_session_destroy() {
814        #[handler]
815        pub async fn destroy_session(depot: &mut Depot, res: &mut Response) {
816            if let Some(session) = depot.session_mut() {
817                session.destroy();
818            }
819            res.render("destroyed");
820        }
821
822        let session_handler = SessionHandler::builder(
823            MemoryStore::new(),
824            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
825        )
826        .build()
827        .unwrap();
828
829        let router = Router::new()
830            .hoop(session_handler)
831            .push(Router::with_path("destroy").get(destroy_session));
832        let service = Service::new(router);
833
834        let response = TestClient::get("http://127.0.0.1:8698/destroy")
835            .send(&service)
836            .await;
837        assert_eq!(response.status_code, Some(StatusCode::OK));
838    }
839
840    // Tests for session with save_unchanged = false
841    #[tokio::test]
842    async fn test_session_save_unchanged_false() {
843        #[handler]
844        pub async fn no_change(depot: &mut Depot, res: &mut Response) {
845            // Access session but don't modify it
846            let _ = depot.session();
847            res.render("no change");
848        }
849
850        let session_handler = SessionHandler::builder(
851            MemoryStore::new(),
852            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
853        )
854        .save_unchanged(false)
855        .build()
856        .unwrap();
857
858        let router = Router::new()
859            .hoop(session_handler)
860            .push(Router::with_path("nochange").get(no_change));
861        let service = Service::new(router);
862
863        let response = TestClient::get("http://127.0.0.1:8698/nochange")
864            .send(&service)
865            .await;
866        assert_eq!(response.status_code, Some(StatusCode::OK));
867        // When save_unchanged is false and no data is modified, no cookie should be set
868        // for a new session (unless there's existing session data)
869    }
870
871    // Tests for session data persistence
872    #[tokio::test]
873    async fn test_session_data_persistence() {
874        #[handler]
875        pub async fn set_data(depot: &mut Depot, res: &mut Response) {
876            if let Some(session) = depot.session_mut() {
877                session.insert("counter", 1).unwrap();
878            }
879            res.render("set");
880        }
881
882        #[handler]
883        pub async fn get_data(depot: &mut Depot, res: &mut Response) {
884            let counter = if let Some(session) = depot.session() {
885                session.get::<i32>("counter").unwrap_or(0)
886            } else {
887                0
888            };
889            res.render(format!("{}", counter));
890        }
891
892        let session_handler = SessionHandler::builder(
893            MemoryStore::new(),
894            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
895        )
896        .build()
897        .unwrap();
898
899        let router = Router::new()
900            .hoop(session_handler)
901            .push(Router::with_path("set").get(set_data))
902            .push(Router::with_path("get").get(get_data));
903        let service = Service::new(router);
904
905        // Set data
906        let response = TestClient::get("http://127.0.0.1:8698/set")
907            .send(&service)
908            .await;
909        let cookie = response.headers().get(SET_COOKIE).unwrap();
910
911        // Get data with same session
912        let mut response = TestClient::get("http://127.0.0.1:8698/get")
913            .add_header(COOKIE, cookie, true)
914            .send(&service)
915            .await;
916        assert_eq!(response.take_string().await.unwrap(), "1");
917    }
918
919    // Test for SESSION_KEY constant
920    #[test]
921    fn test_session_key_constant() {
922        assert_eq!(SESSION_KEY, "::salvo::session");
923    }
924
925    // Test for BASE64_DIGEST_LEN constant
926    #[test]
927    fn test_base64_digest_len() {
928        assert_eq!(BASE64_DIGEST_LEN, 44);
929    }
930}