Skip to main content

salvo_flash/
cookie_store.rs

1use salvo_core::http::cookie::time::Duration;
2use salvo_core::http::cookie::{Cookie, SameSite};
3use salvo_core::{Depot, Request, Response};
4
5use super::{Flash, FlashHandler, FlashStore};
6
7/// CookieStore is a `FlashStore` implementation that stores the flash messages in a cookie.
8#[derive(Debug)]
9#[non_exhaustive]
10pub struct CookieStore {
11    /// The cookie max age.
12    pub max_age: Duration,
13    /// The cookie same site.
14    pub same_site: SameSite,
15    /// The cookie http only.
16    pub http_only: bool,
17    /// The cookie path.
18    pub path: String,
19    /// The cookie name.
20    pub name: String,
21}
22impl Default for CookieStore {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl CookieStore {
29    /// Create a new `CookieStore`.
30    #[must_use]
31    pub fn new() -> Self {
32        Self {
33            max_age: Duration::seconds(60),
34            same_site: SameSite::Lax,
35            http_only: true,
36            path: "/".into(),
37            name: "salvo.flash".into(),
38        }
39    }
40
41    /// Sets cookie name.
42    #[must_use]
43    pub fn name(mut self, name: impl Into<String>) -> Self {
44        self.name = name.into();
45        self
46    }
47
48    /// Sets cookie max_age.
49    #[must_use]
50    pub fn max_age(mut self, max_age: Duration) -> Self {
51        self.max_age = max_age;
52        self
53    }
54
55    /// Sets cookie same site.
56    #[must_use]
57    pub fn same_site(mut self, same_site: SameSite) -> Self {
58        self.same_site = same_site;
59        self
60    }
61
62    /// Sets cookie http only.
63    #[must_use]
64    pub fn http_only(mut self, http_only: bool) -> Self {
65        self.http_only = http_only;
66        self
67    }
68
69    /// Sets cookie path.
70    #[must_use]
71    pub fn path(mut self, path: impl Into<String>) -> Self {
72        self.path = path.into();
73        self
74    }
75
76    /// Into `FlashHandler`.
77    #[must_use]
78    pub fn into_handler(self) -> FlashHandler<Self> {
79        FlashHandler::new(self)
80    }
81}
82impl FlashStore for CookieStore {
83    async fn load_flash(&self, req: &mut Request, _depot: &mut Depot) -> Option<Flash> {
84        match req.cookie(&self.name) {
85            None => None,
86            Some(cookie) => match serde_json::from_str(cookie.value()) {
87                Ok(flash) => Some(flash),
88                Err(e) => {
89                    tracing::error!(error = ?e, "deserialize flash cookie failed");
90                    None
91                }
92            },
93        }
94    }
95    async fn save_flash(
96        &self,
97        _req: &mut Request,
98        _depot: &mut Depot,
99        res: &mut Response,
100        flash: Flash,
101    ) {
102        res.add_cookie(
103            Cookie::build((
104                self.name.clone(),
105                serde_json::to_string(&flash).unwrap_or_default(),
106            ))
107            .max_age(self.max_age)
108            .path(self.path.clone())
109            .same_site(self.same_site)
110            .http_only(self.http_only)
111            .build(),
112        );
113    }
114    async fn clear_flash(&self, _depot: &mut Depot, res: &mut Response) {
115        res.add_cookie(
116            Cookie::build((self.name.clone(), ""))
117                .max_age(Duration::seconds(0))
118                .same_site(self.same_site)
119                .http_only(self.http_only)
120                .path(self.path.clone())
121                .build(),
122        );
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_cookie_store_new() {
132        let store = CookieStore::new();
133        assert_eq!(store.max_age, Duration::seconds(60));
134        assert_eq!(store.same_site, SameSite::Lax);
135        assert!(store.http_only);
136        assert_eq!(store.path, "/");
137        assert_eq!(store.name, "salvo.flash");
138    }
139
140    #[test]
141    fn test_cookie_store_default() {
142        let store = CookieStore::default();
143        assert_eq!(store.name, "salvo.flash");
144        assert_eq!(store.path, "/");
145    }
146
147    #[test]
148    fn test_cookie_store_name() {
149        let store = CookieStore::new().name("custom_flash");
150        assert_eq!(store.name, "custom_flash");
151    }
152
153    #[test]
154    fn test_cookie_store_max_age() {
155        let store = CookieStore::new().max_age(Duration::seconds(120));
156        assert_eq!(store.max_age, Duration::seconds(120));
157    }
158
159    #[test]
160    fn test_cookie_store_same_site() {
161        let store = CookieStore::new().same_site(SameSite::Strict);
162        assert_eq!(store.same_site, SameSite::Strict);
163    }
164
165    #[test]
166    fn test_cookie_store_same_site_none() {
167        let store = CookieStore::new().same_site(SameSite::None);
168        assert_eq!(store.same_site, SameSite::None);
169    }
170
171    #[test]
172    fn test_cookie_store_http_only() {
173        let store = CookieStore::new().http_only(false);
174        assert!(!store.http_only);
175    }
176
177    #[test]
178    fn test_cookie_store_path() {
179        let store = CookieStore::new().path("/app");
180        assert_eq!(store.path, "/app");
181    }
182
183    #[test]
184    fn test_cookie_store_into_handler() {
185        let store = CookieStore::new();
186        let handler = store.into_handler();
187        assert!(handler.minimum_level.is_none());
188    }
189
190    #[test]
191    fn test_cookie_store_chain_config() {
192        let store = CookieStore::new()
193            .name("my_flash")
194            .max_age(Duration::seconds(300))
195            .same_site(SameSite::Strict)
196            .http_only(false)
197            .path("/dashboard");
198
199        assert_eq!(store.name, "my_flash");
200        assert_eq!(store.max_age, Duration::seconds(300));
201        assert_eq!(store.same_site, SameSite::Strict);
202        assert!(!store.http_only);
203        assert_eq!(store.path, "/dashboard");
204    }
205
206    #[test]
207    fn test_cookie_store_debug() {
208        let store = CookieStore::new();
209        let debug_str = format!("{:?}", store);
210        assert!(debug_str.contains("CookieStore"));
211        assert!(debug_str.contains("max_age"));
212        assert!(debug_str.contains("same_site"));
213        assert!(debug_str.contains("http_only"));
214        assert!(debug_str.contains("path"));
215        assert!(debug_str.contains("name"));
216    }
217}