1use crate::AdapterRecord;
2pub mod cookie_cache;
3mod request;
4use crate::config::AuthConfig;
5use crate::entity::{AuthSession, AuthUser};
6use crate::error::AuthResult;
7use crate::schema::AuthSchema;
8use crate::store::AuthStore;
9use crate::types::CreateSession;
10use chrono::Utc;
11pub use request::SessionRequest;
12use std::sync::Arc;
13
14#[derive(Clone, Copy, Debug)]
18pub struct SessionRefreshSuppressed;
19
20#[derive(Debug, Clone, Copy)]
22pub struct SessionReadOptions {
23 pub allow_refresh: bool,
24 pub cleanup_expired: bool,
25}
26
27impl Default for SessionReadOptions {
28 fn default() -> Self {
29 Self {
30 allow_refresh: true,
31 cleanup_expired: true,
32 }
33 }
34}
35
36pub struct SessionRead<T> {
38 pub session: Option<T>,
39 pub needs_refresh: bool,
40 pub refreshed: bool,
41}
42
43impl<T> SessionRead<T> {
44 const fn absent() -> Self {
45 Self {
46 session: None,
47 needs_refresh: false,
48 refreshed: false,
49 }
50 }
51}
52
53pub struct SessionManager<S: AuthSchema> {
55 config: Arc<AuthConfig>,
56 database: Arc<dyn AuthStore<S>>,
57}
58
59impl<S: AuthSchema> Clone for SessionManager<S> {
60 fn clone(&self) -> Self {
61 Self {
62 config: Arc::clone(&self.config),
63 database: Arc::clone(&self.database),
64 }
65 }
66}
67
68impl<S: AuthSchema> SessionManager<S> {
69 #[must_use]
70 pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
71 Self { config, database }
72 }
73
74 pub async fn create_session(
80 &self,
81 user: &impl AuthUser,
82 ip_address: Option<String>,
83 user_agent: Option<String>,
84 ) -> AuthResult<S::Session> {
85 self.database
86 .create_session(self.new_session(user, ip_address, user_agent))
87 .await
88 }
89
90 pub async fn create_session_record(
95 &self,
96 user: &impl AuthUser,
97 ip_address: Option<String>,
98 user_agent: Option<String>,
99 ) -> AuthResult<AdapterRecord<S::Session>> {
100 self.database
101 .create_session_record(self.new_session(user, ip_address, user_agent))
102 .await
103 }
104
105 pub async fn create_session_record_in_transaction(
110 &self,
111 user: &impl AuthUser,
112 ip_address: Option<String>,
113 user_agent: Option<String>,
114 database: &dyn crate::AuthTransaction<S>,
115 ) -> AuthResult<AdapterRecord<S::Session>> {
116 database
117 .create_session_record(self.new_session(user, ip_address, user_agent))
118 .await
119 }
120
121 fn new_session(
122 &self,
123 user: &impl AuthUser,
124 ip_address: Option<String>,
125 user_agent: Option<String>,
126 ) -> CreateSession {
127 CreateSession {
128 additional_fields: crate::field_policy::FieldValues::default(),
129 token: None,
130 active_team_id: None,
131 user_id: user.id().to_string(),
132 expires_at: Utc::now() + self.config.session.expires_in,
133 ip_address,
134 user_agent,
135 impersonated_by: None,
136 active_organization_id: None,
137 }
138 }
139
140 pub async fn get_session(&self, token: &str) -> AuthResult<Option<S::Session>> {
146 Ok(self
147 .read_session(token, SessionReadOptions::default())
148 .await?
149 .session)
150 }
151
152 pub async fn read_session(
159 &self,
160 token: &str,
161 options: SessionReadOptions,
162 ) -> AuthResult<SessionRead<S::Session>> {
163 let Some(session) = self.database.get_session(token).await? else {
164 return Ok(SessionRead::absent());
165 };
166 self.read_loaded_session(session, options).await
167 }
168
169 pub async fn read_loaded_session(
178 &self,
179 session: S::Session,
180 options: SessionReadOptions,
181 ) -> AuthResult<SessionRead<S::Session>> {
182 self.read_loaded(session, options, |token, expires_at| async move {
183 self.database.refresh_session(&token, expires_at).await
184 })
185 .await
186 }
187
188 pub async fn read_loaded_session_record(
191 &self,
192 session: AdapterRecord<S::Session>,
193 options: SessionReadOptions,
194 ) -> AuthResult<SessionRead<AdapterRecord<S::Session>>> {
195 self.read_loaded(session, options, |token, expires_at| async move {
196 self.database
197 .refresh_session_record(&token, expires_at)
198 .await
199 })
200 .await
201 }
202
203 async fn read_loaded<T, F, Fut>(
204 &self,
205 session: T,
206 options: SessionReadOptions,
207 refresh: F,
208 ) -> AuthResult<SessionRead<T>>
209 where
210 T: AuthSession,
211 F: FnOnce(String, chrono::DateTime<chrono::Utc>) -> Fut,
212 Fut: std::future::Future<Output = AuthResult<Option<T>>>,
213 {
214 let token = session.token();
215 let now = Utc::now();
216 if session.expires_at() < now {
217 if options.cleanup_expired {
218 self.database.delete_session(token).await?;
219 }
220 return Ok(SessionRead::absent());
221 }
222 let needs_refresh = !self.config.session.disable_session_refresh
223 && self.config.session.update_age.is_none_or(|age| {
224 session.expires_at() - self.config.session.expires_in + age <= now
225 });
226 if needs_refresh && options.allow_refresh {
227 let refreshed_session =
228 refresh(token.to_owned(), now + self.config.session.expires_in).await?;
229 let refreshed = refreshed_session.is_some();
230 return Ok(SessionRead {
231 session: refreshed_session,
232 needs_refresh,
233 refreshed,
234 });
235 }
236 Ok(SessionRead {
237 session: Some(session),
238 needs_refresh,
239 refreshed: false,
240 })
241 }
242
243 #[must_use]
247 pub fn request_disables_refresh(&self, request: &impl SessionRequest) -> bool {
248 request.session_query_truthy("disableRefresh") || self.has_dont_remember_cookie(request)
249 }
250
251 #[must_use]
253 pub fn has_dont_remember_cookie(&self, request: &impl SessionRequest) -> bool {
254 let name = crate::utils::cookie_utils::related_cookie_name(&self.config, "dont_remember");
255 request
256 .session_headers()
257 .get("cookie")
258 .is_some_and(|header| {
259 cookie::Cookie::split_parse(header)
260 .flatten()
261 .find(|cookie| cookie.name() == name)
262 .and_then(|cookie| {
263 crate::utils::cookie_utils::verify_cookie_value(
264 cookie.value(),
265 self.config.current_secret(),
266 )
267 })
268 .is_some_and(|value| !value.is_empty())
269 })
270 }
271
272 pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
278 self.database.delete_session(token).await
279 }
280
281 pub async fn delete_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<()> {
287 self.database.delete_user_sessions(user_id.as_ref()).await
288 }
289
290 pub async fn list_user_sessions(
296 &self,
297 user_id: impl AsRef<str>,
298 ) -> AuthResult<Vec<S::Session>> {
299 let sessions = self.database.get_user_sessions(user_id.as_ref()).await?;
300 let now = Utc::now();
301 Ok(sessions
302 .into_iter()
303 .filter(|session| session.expires_at() > now)
304 .collect())
305 }
306
307 pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
313 if self.get_session(token).await?.is_none() {
314 return Ok(false);
315 }
316 self.delete_session(token).await?;
317 Ok(true)
318 }
319
320 pub async fn revoke_all_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<usize> {
326 let user_id = user_id.as_ref();
327 let count = self.list_user_sessions(user_id).await?.len();
328 self.delete_user_sessions(user_id).await?;
329 Ok(count)
330 }
331
332 pub async fn revoke_other_user_sessions(
338 &self,
339 user_id: impl AsRef<str>,
340 current_token: &str,
341 ) -> AuthResult<usize> {
342 let sessions = self.list_user_sessions(user_id).await?;
343 let mut count = 0;
344
345 for session in sessions {
346 if session.token() != current_token {
347 self.delete_session(session.token()).await?;
348 count += 1;
349 }
350 }
351
352 Ok(count)
353 }
354
355 pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
361 match self.config.session.fresh_age {
362 Some(fresh_age) if fresh_age != chrono::Duration::zero() => {
363 session.created_at() + fresh_age > Utc::now()
364 }
365 _ => true,
366 }
367 }
368
369 #[must_use]
371 pub fn validate_token_format(&self, token: &str) -> bool {
372 token.len() == 32 && token.bytes().all(|byte| byte.is_ascii_alphanumeric())
373 }
374
375 #[must_use]
380 pub fn extract_session_token(&self, req: &impl SessionRequest) -> Option<String> {
381 let header = req.session_headers().get("cookie")?;
382 header
383 .split(';')
384 .find_map(|pair| {
385 let (name, value) = pair.split_once('=')?;
386 (crate::utils::javascript::trim(name) == self.config.session.cookie_name)
387 .then(|| crate::utils::javascript::trim(value))
388 })
389 .map(|value| {
390 if value.starts_with('"') {
393 value.get(1..value.len().saturating_sub(1)).unwrap_or("")
394 } else {
395 value
396 }
397 })
398 .and_then(|value| {
399 crate::utils::cookie_utils::verify_cookie_value(value, self.config.current_secret())
400 })
401 .filter(|token| !token.is_empty())
402 }
403}
404
405impl<T> std::fmt::Debug for SessionRead<T> {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 f.debug_struct("SessionRead").finish_non_exhaustive()
408 }
409}
410
411impl<S: AuthSchema> std::fmt::Debug for SessionManager<S> {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 f.debug_struct("SessionManager").finish_non_exhaustive()
414 }
415}
416
417#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::entity::AuthSession;
422 use crate::test_store::{BundledSchema, test_config, test_database};
423 use crate::types::AuthRequest;
424 use crate::types::HttpMethod;
425 use crate::wire::SessionView;
426 use chrono::Duration;
427
428 fn test_manager() -> SessionManager<BundledSchema> {
429 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
430 SessionManager::new(test_config(), runtime.block_on(test_database()))
431 }
432
433 #[test]
435 fn valid_token_format() {
436 let mgr = test_manager();
437 let token = "abcdefghijklmnopqrstuvwxyz123456";
438 assert!(mgr.validate_token_format(token));
439 }
440
441 #[test]
443 fn invalid_token_non_alphanumeric() {
444 let mgr = test_manager();
445 assert!(!mgr.validate_token_format("abcdefghijklmnopqrstuvwxy_123456"));
446 }
447
448 #[test]
450 fn invalid_token_too_short() {
451 let mgr = test_manager();
452 assert!(!mgr.validate_token_format("session_short"));
453 }
454
455 #[test]
458 fn extract_rejects_bare_bearer() {
459 let mgr = test_manager();
460 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
461 _ = req
462 .headers
463 .insert("authorization".into(), "Bearer my-token".into());
464 assert_eq!(mgr.extract_session_token(&req), None);
465 }
466
467 #[test]
468 fn extract_cookie_checks_signature_and_first_duplicate() {
469 let mgr = test_manager();
470 let signed = crate::utils::cookie_utils::sign_cookie_value("tok123", &mgr.config.secret);
471 let foreign =
472 crate::utils::cookie_utils::sign_cookie_value("tok123", "another-server-secret");
473 for (cookie, expected) in [
474 (
475 format!("better-auth.session_token={signed}; other=val"),
476 Some("tok123"),
477 ),
478 ("better-auth.session_token=tok123".to_owned(), None),
479 (format!("better-auth.session_token={foreign}"), None),
480 (
481 format!("better-auth.session_token={signed}; better-auth.session_token=invalid"),
482 Some("tok123"),
483 ),
484 (
485 format!("better-auth.session_token=invalid; better-auth.session_token={signed}"),
486 None,
487 ),
488 ] {
489 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
490 _ = req.headers.insert("cookie".into(), cookie);
491 assert_eq!(mgr.extract_session_token(&req).as_deref(), expected);
492 }
493 }
494
495 #[test]
496 fn extract_ignores_bearer_when_signed_cookie_exists() {
497 let mgr = test_manager();
498 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
499 _ = req
500 .headers
501 .insert("authorization".into(), "Bearer bearer-tok".into());
502 let signed =
503 crate::utils::cookie_utils::sign_cookie_value("cookie-tok", &mgr.config.secret);
504 _ = req.headers.insert(
505 "cookie".into(),
506 format!("better-auth.session_token={signed}"),
507 );
508 assert_eq!(mgr.extract_session_token(&req), Some("cookie-tok".into()));
509 }
510
511 #[test]
513 fn extract_returns_none_without_auth() {
514 let mgr = test_manager();
515 let req = AuthRequest::new(HttpMethod::Get, "/test");
516 assert_eq!(mgr.extract_session_token(&req), None);
517 }
518
519 #[test]
521 fn extract_skips_empty_cookie_value() {
522 let mgr = test_manager();
523 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
524 _ = req
525 .headers
526 .insert("cookie".into(), "better-auth.session_token=".into());
527 assert_eq!(mgr.extract_session_token(&req), None);
528 }
529
530 #[test]
532 fn session_fresh_when_within_window() {
533 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
534 config.session.fresh_age = Some(Duration::minutes(10));
535 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
536 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
537
538 let session = SessionView {
540 omitted_fields: std::collections::BTreeSet::default(),
541 active_team_id: None,
542 extension_fields: std::collections::BTreeMap::default(),
543 id: "s1".into(),
544 expires_at: Utc::now() + Duration::hours(1),
545 token: "tok".into(),
546 created_at: Utc::now(),
547 updated_at: Utc::now(),
548 ip_address: None,
549 user_agent: None,
550 user_id: "u1".into(),
551 impersonated_by: None,
552 active_organization_id: None,
553 };
554 assert!(mgr.is_session_fresh(&session));
555 }
556
557 #[test]
559 fn session_not_fresh_when_old() {
560 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
561 config.session.fresh_age = Some(Duration::minutes(10));
562 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
563 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
564
565 let session = SessionView {
566 omitted_fields: std::collections::BTreeSet::default(),
567 active_team_id: None,
568 extension_fields: std::collections::BTreeMap::default(),
569 id: "s1".into(),
570 expires_at: Utc::now() + Duration::hours(1),
571 token: "tok".into(),
572 created_at: Utc::now() - Duration::minutes(20),
573 updated_at: Utc::now(),
574 ip_address: None,
575 user_agent: None,
576 user_id: "u1".into(),
577 impersonated_by: None,
578 active_organization_id: None,
579 };
580 assert!(!mgr.is_session_fresh(&session));
581 }
582
583 #[test]
585 fn disabled_freshness_allows_an_old_session() {
586 let mut config = (*test_config()).clone();
587 config.session.fresh_age = None;
588 let mgr = SessionManager::new(Arc::new(config), test_manager().database);
589 let session = SessionView {
590 omitted_fields: std::collections::BTreeSet::default(),
591 active_team_id: None,
592 extension_fields: std::collections::BTreeMap::default(),
593 id: "s1".into(),
594 expires_at: Utc::now() + Duration::hours(1),
595 token: "tok".into(),
596 created_at: Utc::now() - Duration::days(30),
597 updated_at: Utc::now(),
598 ip_address: None,
599 user_agent: None,
600 user_id: "u1".into(),
601 impersonated_by: None,
602 active_organization_id: None,
603 };
604 assert!(mgr.is_session_fresh(&session));
605 }
606
607 #[tokio::test]
609 async fn create_and_get_session() {
610 let db = test_database().await;
611 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
612
613 let user = db
615 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
616 .await
617 .unwrap();
618
619 let session = mgr.create_session(&user, None, None).await.unwrap();
620 let token = session.token().to_owned();
621
622 let retrieved = mgr.get_session(&token).await.unwrap();
623 assert!(retrieved.is_some());
624 }
625
626 #[tokio::test]
628 async fn refresh_returns_the_persisted_expiry() {
629 let db = test_database().await;
630 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
631 config.session.update_age = None;
633 let mgr = SessionManager::new(Arc::new(config), Arc::clone(&db));
634
635 let user = db
636 .create_user(crate::types::CreateUser::new().with_email("refresh@test.com"))
637 .await
638 .unwrap();
639 let session = mgr.create_session(&user, None, None).await.unwrap();
640 let token = session.token().to_owned();
641
642 let stale = session.expires_at() - Duration::minutes(30);
644 db.update_session_expiry(&token, stale).await.unwrap();
645
646 let returned = mgr
647 .get_session(&token)
648 .await
649 .unwrap()
650 .expect("session should still be live");
651 let stored = db
652 .get_session(&token)
653 .await
654 .unwrap()
655 .expect("session should still be stored");
656
657 assert!(
658 returned.expires_at() > stale,
659 "refresh should have extended the expiry"
660 );
661 assert_eq!(
662 returned.expires_at(),
663 stored.expires_at(),
664 "returned session must reflect the persisted expiry, not the pre-refresh value"
665 );
666 }
667
668 #[test]
669 fn refresh_preferences_verify_the_first_cookie_and_use_javascript_truthiness() {
670 let manager = test_manager();
671 let name =
672 crate::utils::cookie_utils::related_cookie_name(&manager.config, "dont_remember");
673 let valid = crate::utils::cookie_utils::sign_cookie_value("true", &manager.config.secret);
674 let empty = crate::utils::cookie_utils::sign_cookie_value("", &manager.config.secret);
675 let wrong = crate::utils::cookie_utils::sign_cookie_value("true", "foreign-secret");
676 for (header, expected) in [
677 (format!("{name}={valid}"), true),
678 (format!("{name}={empty}"), false),
679 (format!("{name}={wrong}"), false),
680 (format!("{name}=invalid; {name}={valid}"), false),
681 (format!("{name}={valid}; {name}=invalid"), true),
682 ] {
683 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
684 _ = request.headers.insert("cookie".into(), header);
685 assert_eq!(manager.request_disables_refresh(&request), expected);
686 }
687 for (value, expected) in [("", false), ("false", true), ("0", true), ("true", true)] {
688 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
689 _ = request.query.insert("disableRefresh".into(), value.into());
690 assert_eq!(manager.request_disables_refresh(&request), expected);
691 }
692 }
693
694 #[tokio::test]
695 async fn recent_sessions_refresh_at_the_expiry_based_half_second_boundary() {
696 let db = test_database().await;
697 let config = test_config();
698 let manager = SessionManager::new(Arc::clone(&config), Arc::clone(&db));
699 let user = db
700 .create_user(crate::types::CreateUser::new().with_email("half-second@test.com"))
701 .await
702 .unwrap();
703 for difference in [Duration::milliseconds(500), Duration::milliseconds(-500)] {
704 let session = manager.create_session(&user, None, None).await.unwrap();
705 let expiry = Utc::now() + config.session.expires_in
706 - config.session.update_age.unwrap()
707 + difference;
708 db.update_session_expiry(session.token(), expiry)
709 .await
710 .unwrap();
711 let returned = manager.get_session(session.token()).await.unwrap().unwrap();
712 let stored = db.get_session(session.token()).await.unwrap().unwrap();
713 assert_eq!(returned.expires_at(), stored.expires_at());
714 assert_eq!(returned.token(), session.token());
715 if difference > Duration::zero() {
716 assert_eq!(stored.expires_at(), expiry);
717 } else {
718 assert!(stored.expires_at() > expiry + Duration::hours(23));
719 }
720 }
721 }
722
723 #[tokio::test]
725 async fn create_session_without_metadata_uses_empty_strings() {
726 let db = test_database().await;
727 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
728
729 let user = db
730 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
731 .await
732 .unwrap();
733
734 let session = mgr.create_session(&user, None, None).await.unwrap();
735 assert_eq!(session.ip_address.as_deref(), Some(""));
736 assert_eq!(session.user_agent.as_deref(), Some(""));
737 }
738
739 #[tokio::test]
741 async fn delete_session_removes_it() {
742 let db = test_database().await;
743 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
744
745 let user = db
746 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
747 .await
748 .unwrap();
749
750 let session = mgr.create_session(&user, None, None).await.unwrap();
751 let token = session.token().to_owned();
752
753 mgr.delete_session(&token).await.unwrap();
754 let retrieved = mgr.get_session(&token).await.unwrap();
755 assert!(retrieved.is_none());
756 }
757
758 #[tokio::test]
760 async fn revoke_session_returns_true_when_found() {
761 let db = test_database().await;
762 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
763
764 let user = db
765 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
766 .await
767 .unwrap();
768
769 let session = mgr.create_session(&user, None, None).await.unwrap();
770 let result = mgr.revoke_session(session.token()).await.unwrap();
771 assert!(result);
772 }
773
774 #[tokio::test]
776 async fn revoke_session_returns_false_when_not_found() {
777 let mgr = SessionManager::new(test_config(), test_database().await);
778 let result = mgr.revoke_session("nonexistent-token").await.unwrap();
779 assert!(!result);
780 }
781
782 #[tokio::test]
784 async fn list_user_sessions_excludes_expired() {
785 let db = test_database().await;
786 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
787
788 let user = db
789 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
790 .await
791 .unwrap();
792
793 _ = mgr.create_session(&user, None, None).await.unwrap();
795 _ = mgr.create_session(&user, None, None).await.unwrap();
796
797 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
798 assert_eq!(sessions.len(), 2);
799 }
800
801 #[tokio::test]
803 async fn revoke_all_user_sessions() {
804 let db = test_database().await;
805 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
806
807 let user = db
808 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
809 .await
810 .unwrap();
811
812 _ = mgr.create_session(&user, None, None).await.unwrap();
813 _ = mgr.create_session(&user, None, None).await.unwrap();
814
815 let count = mgr.revoke_all_user_sessions(user.id()).await.unwrap();
816 assert_eq!(count, 2);
817
818 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
819 assert_eq!(sessions, Vec::<SessionView>::new());
820 }
821
822 #[tokio::test]
824 async fn revoke_other_sessions_keeps_current() {
825 let db = test_database().await;
826 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
827
828 let user = db
829 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
830 .await
831 .unwrap();
832
833 let current = mgr.create_session(&user, None, None).await.unwrap();
834 _ = mgr.create_session(&user, None, None).await.unwrap();
835 _ = mgr.create_session(&user, None, None).await.unwrap();
836
837 let count = mgr
838 .revoke_other_user_sessions(user.id(), current.token())
839 .await
840 .unwrap();
841 assert_eq!(count, 2);
842
843 let remaining = mgr.list_user_sessions(user.id()).await.unwrap();
844 assert_eq!(remaining.len(), 1);
845 assert_eq!(
846 (*(remaining)
847 .first()
848 .expect("fixture contains the requested index"))
849 .token(),
850 current.token()
851 );
852 }
853}
854