Skip to main content

salvo_session/
lib.rs

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