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 fn new_session(
106 &self,
107 user: &impl AuthUser,
108 ip_address: Option<String>,
109 user_agent: Option<String>,
110 ) -> CreateSession {
111 CreateSession {
112 additional_fields: crate::field_policy::FieldValues::default(),
113 token: None,
114 active_team_id: None,
115 user_id: user.id().to_string(),
116 expires_at: Utc::now() + self.config.session.expires_in,
117 ip_address,
118 user_agent,
119 impersonated_by: None,
120 active_organization_id: None,
121 }
122 }
123
124 pub async fn get_session(&self, token: &str) -> AuthResult<Option<S::Session>> {
130 Ok(self
131 .read_session(token, SessionReadOptions::default())
132 .await?
133 .session)
134 }
135
136 pub async fn read_session(
143 &self,
144 token: &str,
145 options: SessionReadOptions,
146 ) -> AuthResult<SessionRead<S::Session>> {
147 let Some(session) = self.database.get_session(token).await? else {
148 return Ok(SessionRead::absent());
149 };
150 self.read_loaded_session(session, options).await
151 }
152
153 pub async fn read_loaded_session(
162 &self,
163 session: S::Session,
164 options: SessionReadOptions,
165 ) -> AuthResult<SessionRead<S::Session>> {
166 self.read_loaded(session, options, |token, expires_at| async move {
167 self.database.refresh_session(&token, expires_at).await
168 })
169 .await
170 }
171
172 pub async fn read_loaded_session_record(
175 &self,
176 session: AdapterRecord<S::Session>,
177 options: SessionReadOptions,
178 ) -> AuthResult<SessionRead<AdapterRecord<S::Session>>> {
179 self.read_loaded(session, options, |token, expires_at| async move {
180 self.database
181 .refresh_session_record(&token, expires_at)
182 .await
183 })
184 .await
185 }
186
187 async fn read_loaded<T, F, Fut>(
188 &self,
189 session: T,
190 options: SessionReadOptions,
191 refresh: F,
192 ) -> AuthResult<SessionRead<T>>
193 where
194 T: AuthSession,
195 F: FnOnce(String, chrono::DateTime<chrono::Utc>) -> Fut,
196 Fut: std::future::Future<Output = AuthResult<Option<T>>>,
197 {
198 let token = session.token();
199 let now = Utc::now();
200 if session.expires_at() < now {
201 if options.cleanup_expired {
202 self.database.delete_session(token).await?;
203 }
204 return Ok(SessionRead::absent());
205 }
206 let needs_refresh = !self.config.session.disable_session_refresh
207 && self.config.session.update_age.is_none_or(|age| {
208 session.expires_at() - self.config.session.expires_in + age <= now
209 });
210 if needs_refresh && options.allow_refresh {
211 let refreshed_session =
212 refresh(token.to_owned(), now + self.config.session.expires_in).await?;
213 let refreshed = refreshed_session.is_some();
214 return Ok(SessionRead {
215 session: refreshed_session,
216 needs_refresh,
217 refreshed,
218 });
219 }
220 Ok(SessionRead {
221 session: Some(session),
222 needs_refresh,
223 refreshed: false,
224 })
225 }
226
227 #[must_use]
231 pub fn request_disables_refresh(&self, request: &impl SessionRequest) -> bool {
232 request.session_query_truthy("disableRefresh") || self.has_dont_remember_cookie(request)
233 }
234
235 #[must_use]
237 pub fn has_dont_remember_cookie(&self, request: &impl SessionRequest) -> bool {
238 let name = crate::utils::cookie_utils::related_cookie_name(&self.config, "dont_remember");
239 request
240 .session_headers()
241 .get("cookie")
242 .is_some_and(|header| {
243 cookie::Cookie::split_parse(header)
244 .flatten()
245 .find(|cookie| cookie.name() == name)
246 .and_then(|cookie| {
247 crate::utils::cookie_utils::verify_cookie_value(
248 cookie.value(),
249 self.config.current_secret(),
250 )
251 })
252 .is_some_and(|value| !value.is_empty())
253 })
254 }
255
256 pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
262 self.database.delete_session(token).await
263 }
264
265 pub async fn delete_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<()> {
271 self.database.delete_user_sessions(user_id.as_ref()).await
272 }
273
274 pub async fn list_user_sessions(
280 &self,
281 user_id: impl AsRef<str>,
282 ) -> AuthResult<Vec<S::Session>> {
283 let sessions = self.database.get_user_sessions(user_id.as_ref()).await?;
284 let now = Utc::now();
285 Ok(sessions
286 .into_iter()
287 .filter(|session| session.expires_at() > now)
288 .collect())
289 }
290
291 pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
297 if self.get_session(token).await?.is_none() {
298 return Ok(false);
299 }
300 self.delete_session(token).await?;
301 Ok(true)
302 }
303
304 pub async fn revoke_all_user_sessions(&self, user_id: impl AsRef<str>) -> AuthResult<usize> {
310 let user_id = user_id.as_ref();
311 let count = self.list_user_sessions(user_id).await?.len();
312 self.delete_user_sessions(user_id).await?;
313 Ok(count)
314 }
315
316 pub async fn revoke_other_user_sessions(
322 &self,
323 user_id: impl AsRef<str>,
324 current_token: &str,
325 ) -> AuthResult<usize> {
326 let sessions = self.list_user_sessions(user_id).await?;
327 let mut count = 0;
328
329 for session in sessions {
330 if session.token() != current_token {
331 self.delete_session(session.token()).await?;
332 count += 1;
333 }
334 }
335
336 Ok(count)
337 }
338
339 pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
345 match self.config.session.fresh_age {
346 Some(fresh_age) if fresh_age != chrono::Duration::zero() => {
347 session.created_at() + fresh_age > Utc::now()
348 }
349 _ => true,
350 }
351 }
352
353 #[must_use]
355 pub fn validate_token_format(&self, token: &str) -> bool {
356 token.len() == 32 && token.bytes().all(|byte| byte.is_ascii_alphanumeric())
357 }
358
359 #[must_use]
364 pub fn extract_session_token(&self, req: &impl SessionRequest) -> Option<String> {
365 let header = req.session_headers().get("cookie")?;
366 header
367 .split(';')
368 .find_map(|pair| {
369 let (name, value) = pair.split_once('=')?;
370 (crate::utils::javascript::trim(name) == self.config.session.cookie_name)
371 .then(|| crate::utils::javascript::trim(value))
372 })
373 .map(|value| {
374 if value.starts_with('"') {
377 value.get(1..value.len().saturating_sub(1)).unwrap_or("")
378 } else {
379 value
380 }
381 })
382 .and_then(|value| {
383 crate::utils::cookie_utils::verify_cookie_value(value, self.config.current_secret())
384 })
385 .filter(|token| !token.is_empty())
386 }
387}
388
389impl<T> std::fmt::Debug for SessionRead<T> {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 f.debug_struct("SessionRead").finish_non_exhaustive()
392 }
393}
394
395impl<S: AuthSchema> std::fmt::Debug for SessionManager<S> {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 f.debug_struct("SessionManager").finish_non_exhaustive()
398 }
399}
400
401#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::entity::AuthSession;
406 use crate::test_store::{BundledSchema, test_config, test_database};
407 use crate::types::AuthRequest;
408 use crate::types::HttpMethod;
409 use crate::wire::SessionView;
410 use chrono::Duration;
411
412 fn test_manager() -> SessionManager<BundledSchema> {
413 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
414 SessionManager::new(test_config(), runtime.block_on(test_database()))
415 }
416
417 #[test]
419 fn valid_token_format() {
420 let mgr = test_manager();
421 let token = "abcdefghijklmnopqrstuvwxyz123456";
422 assert!(mgr.validate_token_format(token));
423 }
424
425 #[test]
427 fn invalid_token_non_alphanumeric() {
428 let mgr = test_manager();
429 assert!(!mgr.validate_token_format("abcdefghijklmnopqrstuvwxy_123456"));
430 }
431
432 #[test]
434 fn invalid_token_too_short() {
435 let mgr = test_manager();
436 assert!(!mgr.validate_token_format("session_short"));
437 }
438
439 #[test]
442 fn extract_rejects_bare_bearer() {
443 let mgr = test_manager();
444 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
445 _ = req
446 .headers
447 .insert("authorization".into(), "Bearer my-token".into());
448 assert_eq!(mgr.extract_session_token(&req), None);
449 }
450
451 #[test]
452 fn extract_cookie_checks_signature_and_first_duplicate() {
453 let mgr = test_manager();
454 let signed = crate::utils::cookie_utils::sign_cookie_value("tok123", &mgr.config.secret);
455 let foreign =
456 crate::utils::cookie_utils::sign_cookie_value("tok123", "another-server-secret");
457 for (cookie, expected) in [
458 (
459 format!("better-auth.session_token={signed}; other=val"),
460 Some("tok123"),
461 ),
462 ("better-auth.session_token=tok123".to_owned(), None),
463 (format!("better-auth.session_token={foreign}"), None),
464 (
465 format!("better-auth.session_token={signed}; better-auth.session_token=invalid"),
466 Some("tok123"),
467 ),
468 (
469 format!("better-auth.session_token=invalid; better-auth.session_token={signed}"),
470 None,
471 ),
472 ] {
473 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
474 _ = req.headers.insert("cookie".into(), cookie);
475 assert_eq!(mgr.extract_session_token(&req).as_deref(), expected);
476 }
477 }
478
479 #[test]
480 fn extract_ignores_bearer_when_signed_cookie_exists() {
481 let mgr = test_manager();
482 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
483 _ = req
484 .headers
485 .insert("authorization".into(), "Bearer bearer-tok".into());
486 let signed =
487 crate::utils::cookie_utils::sign_cookie_value("cookie-tok", &mgr.config.secret);
488 _ = req.headers.insert(
489 "cookie".into(),
490 format!("better-auth.session_token={signed}"),
491 );
492 assert_eq!(mgr.extract_session_token(&req), Some("cookie-tok".into()));
493 }
494
495 #[test]
497 fn extract_returns_none_without_auth() {
498 let mgr = test_manager();
499 let req = AuthRequest::new(HttpMethod::Get, "/test");
500 assert_eq!(mgr.extract_session_token(&req), None);
501 }
502
503 #[test]
505 fn extract_skips_empty_cookie_value() {
506 let mgr = test_manager();
507 let mut req = AuthRequest::new(HttpMethod::Get, "/test");
508 _ = req
509 .headers
510 .insert("cookie".into(), "better-auth.session_token=".into());
511 assert_eq!(mgr.extract_session_token(&req), None);
512 }
513
514 #[test]
516 fn session_fresh_when_within_window() {
517 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
518 config.session.fresh_age = Some(Duration::minutes(10));
519 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
520 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
521
522 let session = SessionView {
524 omitted_fields: std::collections::BTreeSet::default(),
525 active_team_id: None,
526 extension_fields: std::collections::BTreeMap::default(),
527 id: "s1".into(),
528 expires_at: Utc::now() + Duration::hours(1),
529 token: "tok".into(),
530 created_at: Utc::now(),
531 updated_at: Utc::now(),
532 ip_address: None,
533 user_agent: None,
534 user_id: "u1".into(),
535 impersonated_by: None,
536 active_organization_id: None,
537 };
538 assert!(mgr.is_session_fresh(&session));
539 }
540
541 #[test]
543 fn session_not_fresh_when_old() {
544 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
545 config.session.fresh_age = Some(Duration::minutes(10));
546 let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
547 let mgr = SessionManager::new(Arc::new(config), runtime.block_on(test_database()));
548
549 let session = SessionView {
550 omitted_fields: std::collections::BTreeSet::default(),
551 active_team_id: None,
552 extension_fields: std::collections::BTreeMap::default(),
553 id: "s1".into(),
554 expires_at: Utc::now() + Duration::hours(1),
555 token: "tok".into(),
556 created_at: Utc::now() - Duration::minutes(20),
557 updated_at: Utc::now(),
558 ip_address: None,
559 user_agent: None,
560 user_id: "u1".into(),
561 impersonated_by: None,
562 active_organization_id: None,
563 };
564 assert!(!mgr.is_session_fresh(&session));
565 }
566
567 #[test]
569 fn disabled_freshness_allows_an_old_session() {
570 let mut config = (*test_config()).clone();
571 config.session.fresh_age = None;
572 let mgr = SessionManager::new(Arc::new(config), test_manager().database);
573 let session = SessionView {
574 omitted_fields: std::collections::BTreeSet::default(),
575 active_team_id: None,
576 extension_fields: std::collections::BTreeMap::default(),
577 id: "s1".into(),
578 expires_at: Utc::now() + Duration::hours(1),
579 token: "tok".into(),
580 created_at: Utc::now() - Duration::days(30),
581 updated_at: Utc::now(),
582 ip_address: None,
583 user_agent: None,
584 user_id: "u1".into(),
585 impersonated_by: None,
586 active_organization_id: None,
587 };
588 assert!(mgr.is_session_fresh(&session));
589 }
590
591 #[tokio::test]
593 async fn create_and_get_session() {
594 let db = test_database().await;
595 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
596
597 let user = db
599 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
600 .await
601 .unwrap();
602
603 let session = mgr.create_session(&user, None, None).await.unwrap();
604 let token = session.token().to_owned();
605
606 let retrieved = mgr.get_session(&token).await.unwrap();
607 assert!(retrieved.is_some());
608 }
609
610 #[tokio::test]
612 async fn refresh_returns_the_persisted_expiry() {
613 let db = test_database().await;
614 let mut config = AuthConfig::new("test-secret-min-32-chars-1234567");
615 config.session.update_age = None;
617 let mgr = SessionManager::new(Arc::new(config), Arc::clone(&db));
618
619 let user = db
620 .create_user(crate::types::CreateUser::new().with_email("refresh@test.com"))
621 .await
622 .unwrap();
623 let session = mgr.create_session(&user, None, None).await.unwrap();
624 let token = session.token().to_owned();
625
626 let stale = session.expires_at() - Duration::minutes(30);
628 db.update_session_expiry(&token, stale).await.unwrap();
629
630 let returned = mgr
631 .get_session(&token)
632 .await
633 .unwrap()
634 .expect("session should still be live");
635 let stored = db
636 .get_session(&token)
637 .await
638 .unwrap()
639 .expect("session should still be stored");
640
641 assert!(
642 returned.expires_at() > stale,
643 "refresh should have extended the expiry"
644 );
645 assert_eq!(
646 returned.expires_at(),
647 stored.expires_at(),
648 "returned session must reflect the persisted expiry, not the pre-refresh value"
649 );
650 }
651
652 #[test]
653 fn refresh_preferences_verify_the_first_cookie_and_use_javascript_truthiness() {
654 let manager = test_manager();
655 let name =
656 crate::utils::cookie_utils::related_cookie_name(&manager.config, "dont_remember");
657 let valid = crate::utils::cookie_utils::sign_cookie_value("true", &manager.config.secret);
658 let empty = crate::utils::cookie_utils::sign_cookie_value("", &manager.config.secret);
659 let wrong = crate::utils::cookie_utils::sign_cookie_value("true", "foreign-secret");
660 for (header, expected) in [
661 (format!("{name}={valid}"), true),
662 (format!("{name}={empty}"), false),
663 (format!("{name}={wrong}"), false),
664 (format!("{name}=invalid; {name}={valid}"), false),
665 (format!("{name}={valid}; {name}=invalid"), true),
666 ] {
667 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
668 _ = request.headers.insert("cookie".into(), header);
669 assert_eq!(manager.request_disables_refresh(&request), expected);
670 }
671 for (value, expected) in [("", false), ("false", true), ("0", true), ("true", true)] {
672 let mut request = AuthRequest::new(HttpMethod::Get, "/get-session");
673 _ = request.query.insert("disableRefresh".into(), value.into());
674 assert_eq!(manager.request_disables_refresh(&request), expected);
675 }
676 }
677
678 #[tokio::test]
679 async fn recent_sessions_refresh_at_the_expiry_based_half_second_boundary() {
680 let db = test_database().await;
681 let config = test_config();
682 let manager = SessionManager::new(Arc::clone(&config), Arc::clone(&db));
683 let user = db
684 .create_user(crate::types::CreateUser::new().with_email("half-second@test.com"))
685 .await
686 .unwrap();
687 for difference in [Duration::milliseconds(500), Duration::milliseconds(-500)] {
688 let session = manager.create_session(&user, None, None).await.unwrap();
689 let expiry = Utc::now() + config.session.expires_in
690 - config.session.update_age.unwrap()
691 + difference;
692 db.update_session_expiry(session.token(), expiry)
693 .await
694 .unwrap();
695 let returned = manager.get_session(session.token()).await.unwrap().unwrap();
696 let stored = db.get_session(session.token()).await.unwrap().unwrap();
697 assert_eq!(returned.expires_at(), stored.expires_at());
698 assert_eq!(returned.token(), session.token());
699 if difference > Duration::zero() {
700 assert_eq!(stored.expires_at(), expiry);
701 } else {
702 assert!(stored.expires_at() > expiry + Duration::hours(23));
703 }
704 }
705 }
706
707 #[tokio::test]
709 async fn create_session_without_metadata_uses_empty_strings() {
710 let db = test_database().await;
711 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
712
713 let user = db
714 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
715 .await
716 .unwrap();
717
718 let session = mgr.create_session(&user, None, None).await.unwrap();
719 assert_eq!(session.ip_address.as_deref(), Some(""));
720 assert_eq!(session.user_agent.as_deref(), Some(""));
721 }
722
723 #[tokio::test]
725 async fn delete_session_removes_it() {
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 let token = session.token().to_owned();
736
737 mgr.delete_session(&token).await.unwrap();
738 let retrieved = mgr.get_session(&token).await.unwrap();
739 assert!(retrieved.is_none());
740 }
741
742 #[tokio::test]
744 async fn revoke_session_returns_true_when_found() {
745 let db = test_database().await;
746 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
747
748 let user = db
749 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
750 .await
751 .unwrap();
752
753 let session = mgr.create_session(&user, None, None).await.unwrap();
754 let result = mgr.revoke_session(session.token()).await.unwrap();
755 assert!(result);
756 }
757
758 #[tokio::test]
760 async fn revoke_session_returns_false_when_not_found() {
761 let mgr = SessionManager::new(test_config(), test_database().await);
762 let result = mgr.revoke_session("nonexistent-token").await.unwrap();
763 assert!(!result);
764 }
765
766 #[tokio::test]
768 async fn list_user_sessions_excludes_expired() {
769 let db = test_database().await;
770 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
771
772 let user = db
773 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
774 .await
775 .unwrap();
776
777 _ = mgr.create_session(&user, None, None).await.unwrap();
779 _ = mgr.create_session(&user, None, None).await.unwrap();
780
781 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
782 assert_eq!(sessions.len(), 2);
783 }
784
785 #[tokio::test]
787 async fn revoke_all_user_sessions() {
788 let db = test_database().await;
789 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
790
791 let user = db
792 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
793 .await
794 .unwrap();
795
796 _ = mgr.create_session(&user, None, None).await.unwrap();
797 _ = mgr.create_session(&user, None, None).await.unwrap();
798
799 let count = mgr.revoke_all_user_sessions(user.id()).await.unwrap();
800 assert_eq!(count, 2);
801
802 let sessions = mgr.list_user_sessions(user.id()).await.unwrap();
803 assert_eq!(sessions, Vec::<SessionView>::new());
804 }
805
806 #[tokio::test]
808 async fn revoke_other_sessions_keeps_current() {
809 let db = test_database().await;
810 let mgr = SessionManager::new(test_config(), Arc::clone(&db));
811
812 let user = db
813 .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
814 .await
815 .unwrap();
816
817 let current = mgr.create_session(&user, None, None).await.unwrap();
818 _ = mgr.create_session(&user, None, None).await.unwrap();
819 _ = mgr.create_session(&user, None, None).await.unwrap();
820
821 let count = mgr
822 .revoke_other_user_sessions(user.id(), current.token())
823 .await
824 .unwrap();
825 assert_eq!(count, 2);
826
827 let remaining = mgr.list_user_sessions(user.id()).await.unwrap();
828 assert_eq!(remaining.len(), 1);
829 assert_eq!(
830 (*(remaining)
831 .first()
832 .expect("fixture contains the requested index"))
833 .token(),
834 current.token()
835 );
836 }
837}
838