rust_supabase_sdk/auth/
session_store.rs1use std::sync::RwLock;
8
9use super::types::Session;
10
11pub 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#[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(); 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 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 #[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 let _guard = s.inner.write().unwrap();
158 panic!("intentional poison");
159 });
160 assert!(handle.join().is_err(), "thread should have panicked");
161
162 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 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 store.clear();
200 assert!(store.get().is_none());
201 }
202}