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