Skip to main content

rust_supabase_sdk/auth/
session_store.rs

1//! Pluggable persistence for the current [`Session`].
2//!
3//! The default is [`InMemorySessionStore`] — a process-local `RwLock`. Plug in
4//! your own implementation via [`ClientBuilder::session_store`](crate::ClientBuilder::session_store)
5//! to persist to disk, the OS keyring, or a custom KV store.
6
7use std::sync::RwLock;
8
9use super::types::Session;
10
11/// A backing store for the active session.
12///
13/// All methods are synchronous to keep them callable from middleware paths.
14/// Implementations should be cheap to clone via `Arc`.
15pub trait SessionStore: Send + Sync + std::fmt::Debug {
16    fn get(&self) -> Option<Session>;
17    fn set(&self, session: Session);
18    fn clear(&self);
19}
20
21/// Default in-memory store. Cheap; not persisted across restarts.
22#[derive(Debug, Default)]
23pub struct InMemorySessionStore {
24    inner: RwLock<Option<Session>>,
25}
26
27impl InMemorySessionStore {
28    pub fn new() -> Self {
29        Self::default()
30    }
31}
32
33impl SessionStore for InMemorySessionStore {
34    fn get(&self) -> Option<Session> {
35        match self.inner.read() {
36            Ok(guard) => guard.clone(),
37            Err(poisoned) => poisoned.into_inner().clone(),
38        }
39    }
40
41    fn set(&self, session: Session) {
42        match self.inner.write() {
43            Ok(mut guard) => *guard = Some(session),
44            Err(poisoned) => *poisoned.into_inner() = Some(session),
45        }
46    }
47
48    fn clear(&self) {
49        match self.inner.write() {
50            Ok(mut guard) => *guard = None,
51            Err(poisoned) => *poisoned.into_inner() = None,
52        }
53    }
54}
55
56#[cfg(test)]
57#[allow(clippy::unwrap_used)]
58mod tests {
59    use super::*;
60    use chrono::Utc;
61    use serde_json::json;
62
63    fn make_session(token: &str) -> Session {
64        Session {
65            access_token: token.into(),
66            token_type: "bearer".into(),
67            expires_in: 3600,
68            expires_at: Utc::now().timestamp() + 3600,
69            refresh_token: "rt".into(),
70            user: serde_json::from_value(json!({
71                "id": "u1", "aud": "auth", "role": "auth",
72                "created_at": "2024-01-01T00:00:00Z"
73            }))
74            .unwrap(),
75        }
76    }
77
78    #[test]
79    fn starts_empty() {
80        let store = InMemorySessionStore::new();
81        assert!(store.get().is_none());
82    }
83
84    #[test]
85    fn set_then_get_returns_session() {
86        let store = InMemorySessionStore::new();
87        store.set(make_session("tok-1"));
88        assert_eq!(store.get().unwrap().access_token, "tok-1");
89    }
90
91    #[test]
92    fn set_overwrites_previous_session() {
93        let store = InMemorySessionStore::new();
94        store.set(make_session("old"));
95        store.set(make_session("new"));
96        assert_eq!(store.get().unwrap().access_token, "new");
97    }
98
99    #[test]
100    fn clear_removes_session() {
101        let store = InMemorySessionStore::new();
102        store.set(make_session("tok"));
103        store.clear();
104        assert!(store.get().is_none());
105    }
106
107    #[test]
108    fn clear_on_empty_is_a_noop() {
109        let store = InMemorySessionStore::new();
110        store.clear(); // should not panic
111        assert!(store.get().is_none());
112    }
113
114    #[test]
115    fn default_equals_new() {
116        let store = InMemorySessionStore::default();
117        assert!(store.get().is_none());
118    }
119
120    #[test]
121    fn store_is_send_sync() {
122        fn assert_send_sync<T: Send + Sync>() {}
123        assert_send_sync::<InMemorySessionStore>();
124    }
125
126    #[test]
127    fn get_clones_session_independently() {
128        let store = InMemorySessionStore::new();
129        store.set(make_session("tok"));
130        let s1 = store.get().unwrap();
131        store.set(make_session("new-tok"));
132        // s1 still holds the snapshot taken before the overwrite
133        assert_eq!(s1.access_token, "tok");
134        assert_eq!(store.get().unwrap().access_token, "new-tok");
135    }
136
137    #[test]
138    fn user_preserved_through_session_store() {
139        let store = InMemorySessionStore::new();
140        let session = make_session("tok");
141        let user_id = session.user.id.clone();
142        store.set(session);
143        assert_eq!(store.get().unwrap().user.id, user_id);
144    }
145
146    /// Poison the inner lock from a thread that panics while holding the write
147    /// guard, then verify `get`/`set`/`clear` still work via `poisoned.into_inner()`.
148    #[test]
149    fn get_recovers_from_poisoned_lock() {
150        use std::sync::Arc;
151        let store = Arc::new(InMemorySessionStore::new());
152        store.set(make_session("before-poison"));
153
154        let s = Arc::clone(&store);
155        let handle = std::thread::spawn(move || {
156            // Grab the write lock, then panic — this poisons the RwLock.
157            let _guard = s.inner.write().unwrap();
158            panic!("intentional poison");
159        });
160        assert!(handle.join().is_err(), "thread should have panicked");
161
162        // The lock is now poisoned. `get` must still return Some(...).
163        let s = store.get();
164        assert!(s.is_some());
165        assert_eq!(s.unwrap().access_token, "before-poison");
166    }
167
168    #[test]
169    fn set_recovers_from_poisoned_lock() {
170        use std::sync::Arc;
171        let store = Arc::new(InMemorySessionStore::new());
172
173        let s = Arc::clone(&store);
174        let handle = std::thread::spawn(move || {
175            let _guard = s.inner.write().unwrap();
176            panic!("intentional poison");
177        });
178        assert!(handle.join().is_err());
179
180        // After poisoning, `set` must still succeed.
181        store.set(make_session("after-poison"));
182        assert_eq!(store.get().unwrap().access_token, "after-poison");
183    }
184
185    #[test]
186    fn clear_recovers_from_poisoned_lock() {
187        use std::sync::Arc;
188        let store = Arc::new(InMemorySessionStore::new());
189        store.set(make_session("x"));
190
191        let s = Arc::clone(&store);
192        let handle = std::thread::spawn(move || {
193            let _guard = s.inner.write().unwrap();
194            panic!("intentional poison");
195        });
196        assert!(handle.join().is_err());
197
198        // After poisoning, `clear` must still succeed.
199        store.clear();
200        assert!(store.get().is_none());
201    }
202}