1use indexmap::IndexMap;
29use rand::{CryptoRng, RngCore};
30
31use parse_rust_core::{new_object_id, ErrorCode, ParseDate, ParseError, ParseMap, ParseValue};
32use parse_rust_schema::default_schema;
33use parse_rust_storage::{
34 ClassSchema, Comparison, Constraint, Query, QueryOptions, Row, StorageAdapter,
35};
36
37pub const SESSION_TOKEN_PREFIX: &str = "r:";
43
44const TOKEN_BYTES: usize = 16;
46
47const SESSION_CLASS: &str = "_Session";
49
50fn fill_secure<R: RngCore + CryptoRng>(rng: &mut R, buf: &mut [u8]) {
58 rng.fill_bytes(buf);
59}
60
61pub fn new_session_token() -> String {
66 let mut bytes = [0u8; TOKEN_BYTES];
67 fill_secure(&mut rand::thread_rng(), &mut bytes);
68
69 let mut token = String::with_capacity(SESSION_TOKEN_PREFIX.len() + TOKEN_BYTES * 2);
70 token.push_str(SESSION_TOKEN_PREFIX);
71 for b in bytes {
72 token.push(char::from(HEX[(b >> 4) as usize]));
75 token.push(char::from(HEX[(b & 0x0f) as usize]));
76 }
77 token
78}
79
80const HEX: &[u8; 16] = b"0123456789abcdef";
81
82#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum SessionAction {
92 Signup,
93 Login,
94 Upgrade,
97 Create,
99}
100
101impl SessionAction {
102 pub fn as_str(&self) -> &'static str {
103 match self {
104 SessionAction::Signup => "signup",
105 SessionAction::Login => "login",
106 SessionAction::Upgrade => "upgrade",
107 SessionAction::Create => "create",
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct CreatedWith {
115 pub action: SessionAction,
116 pub auth_provider: Option<String>,
119}
120
121impl CreatedWith {
122 pub fn signup(auth_provider: Option<&str>) -> Self {
125 Self {
126 action: SessionAction::Signup,
127 auth_provider: Some(auth_provider.unwrap_or("password").to_string()),
128 }
129 }
130
131 pub fn login(auth_provider: Option<&str>) -> Self {
133 Self {
134 action: SessionAction::Login,
135 auth_provider: Some(auth_provider.unwrap_or("password").to_string()),
136 }
137 }
138
139 pub fn upgrade() -> Self {
141 Self {
142 action: SessionAction::Upgrade,
143 auth_provider: None,
144 }
145 }
146
147 pub fn create() -> Self {
149 Self {
150 action: SessionAction::Create,
151 auth_provider: None,
152 }
153 }
154
155 fn to_value(&self) -> ParseValue {
156 let mut map = ParseMap::new();
157 map.insert(
158 "action".to_string(),
159 ParseValue::String(self.action.as_str().to_string()),
160 );
161 if let Some(provider) = &self.auth_provider {
162 map.insert(
163 "authProvider".to_string(),
164 ParseValue::String(provider.clone()),
165 );
166 }
167 ParseValue::Object(map)
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct SessionConfig {
179 pub session_length_secs: i64,
180 pub expire_inactive_sessions: bool,
181}
182
183impl Default for SessionConfig {
184 fn default() -> Self {
185 Self {
186 session_length_secs: 31_536_000,
187 expire_inactive_sessions: true,
188 }
189 }
190}
191
192impl SessionConfig {
193 pub fn generate_expires_at(&self, now: ParseDate) -> Option<ParseDate> {
198 if !self.expire_inactive_sessions {
199 return None;
200 }
201 let millis = now
202 .timestamp_millis()
203 .checked_add(self.session_length_secs.saturating_mul(1000))?;
204 chrono::DateTime::from_timestamp_millis(millis).map(ParseDate::from_datetime)
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct NewSession<'a> {
211 pub user_object_id: &'a str,
212 pub created_with: Option<CreatedWith>,
218 pub installation_id: Option<&'a str>,
221}
222
223#[derive(Debug, Clone)]
225pub struct CreatedSession {
226 pub session_token: String,
227 pub object_id: String,
228 pub expires_at: Option<ParseDate>,
230 pub created_at: ParseDate,
231}
232
233#[derive(Debug, Clone)]
235pub struct ResolvedSession {
236 pub object_id: String,
237 pub user_object_id: String,
238 pub session_token: String,
239 pub installation_id: Option<String>,
240 pub expires_at: Option<ParseDate>,
242 pub row: Row,
245}
246
247fn invalid_session_token() -> ParseError {
248 ParseError::new(ErrorCode::InvalidSessionToken, "Invalid session token")
251}
252
253fn session_expired() -> ParseError {
254 ParseError::new(ErrorCode::InvalidSessionToken, "Session token is expired.")
255}
256
257fn role_prefixed_object_id() -> ParseError {
260 ParseError::new(ErrorCode::InternalServerError, "Invalid object ID.")
261}
262
263fn session_schema() -> ClassSchema {
271 default_schema(SESSION_CLASS)
272}
273
274pub async fn ensure_session_schema<S: StorageAdapter>(storage: &S) -> Result<(), ParseError> {
287 let existing = storage.all_schemas().await?;
288 if existing.iter().any(|s| s.class_name == SESSION_CLASS) {
289 return Ok(());
290 }
291 storage.upsert_schema(&session_schema()).await
292}
293
294pub async fn create_session<S: StorageAdapter>(
309 storage: &S,
310 config: &SessionConfig,
311 new: NewSession<'_>,
312) -> Result<CreatedSession, ParseError> {
313 let schema = session_schema();
314 let now = ParseDate::now();
315 let token = new_session_token();
316 let expires_at = config.generate_expires_at(now);
317 let object_id = new_object_id();
318
319 let user = ParseValue::Pointer {
320 class_name: "_User".to_string(),
321 object_id: new.user_object_id.to_string(),
322 };
323
324 let mut row: Row = IndexMap::new();
325 row.insert(
326 "sessionToken".to_string(),
327 ParseValue::String(token.clone()),
328 );
329 row.insert("user".to_string(), user.clone());
330 if let Some(created_with) = &new.created_with {
331 row.insert("createdWith".to_string(), created_with.to_value());
332 }
333 if let Some(expires_at) = expires_at {
341 row.insert("expiresAt".to_string(), ParseValue::Date(expires_at));
342 }
343 if let Some(installation_id) = new.installation_id {
344 row.insert(
345 "installationId".to_string(),
346 ParseValue::String(installation_id.to_string()),
347 );
348 }
349 row.insert("updatedAt".to_string(), ParseValue::Date(now));
350 row.insert("createdAt".to_string(), ParseValue::Date(now));
351 row.insert(
352 "objectId".to_string(),
353 ParseValue::String(object_id.clone()),
354 );
355
356 destroy_duplicated_sessions(storage, &schema, &user, new.installation_id, &token).await?;
361
362 ensure_session_schema(storage).await?;
363 storage.create(&schema, &row).await?;
364
365 Ok(CreatedSession {
366 session_token: token,
367 object_id,
368 expires_at,
369 created_at: now,
370 })
371}
372
373async fn destroy_duplicated_sessions<S: StorageAdapter>(
384 storage: &S,
385 schema: &ClassSchema,
386 user: &ParseValue,
387 installation_id: Option<&str>,
388 session_token: &str,
389) -> Result<(), ParseError> {
390 let Some(installation_id) = installation_id else {
391 return Ok(());
392 };
393
394 let query = Query::from_constraints(vec![
395 Constraint::equal("user", user.clone()),
396 Constraint::equal(
397 "installationId",
398 ParseValue::String(installation_id.to_string()),
399 ),
400 Constraint {
401 field: "sessionToken".to_string(),
402 comparison: Comparison::NotEqual(ParseValue::String(session_token.to_string())),
403 },
404 ]);
405 storage.delete(schema, &query).await?;
406 Ok(())
407}
408
409pub async fn resolve_session<S: StorageAdapter>(
430 storage: &S,
431 session_token: &str,
432) -> Result<ResolvedSession, ParseError> {
433 let schema = session_schema();
434 let query = Query::from_constraints(vec![Constraint::equal(
435 "sessionToken",
436 ParseValue::String(session_token.to_string()),
437 )]);
438 let options = QueryOptions {
439 limit: Some(1),
440 skip: None,
441 order: Vec::new(),
442 keys: None,
443 case_insensitive: false,
444 };
445
446 let rows = storage.find(&schema, &query, &options).await?;
447 let Some(row) = rows.into_iter().next() else {
448 return Err(invalid_session_token());
449 };
450
451 let user_object_id = match row.get("user") {
454 Some(ParseValue::Pointer { object_id, .. }) => object_id.clone(),
455 _ => return Err(invalid_session_token()),
456 };
457
458 let expires_at = match row.get("expiresAt") {
459 Some(ParseValue::Date(d)) => Some(*d),
460 _ => None,
465 };
466 if let Some(expires_at) = expires_at {
467 if expires_at.timestamp_millis() < ParseDate::now().timestamp_millis() {
468 return Err(session_expired());
469 }
470 }
471
472 if user_object_id.starts_with("role:") {
473 return Err(role_prefixed_object_id());
474 }
475
476 let object_id = match row.get("objectId") {
477 Some(ParseValue::String(id)) => id.clone(),
478 _ => return Err(invalid_session_token()),
479 };
480 let installation_id = match row.get("installationId") {
481 Some(ParseValue::String(id)) => Some(id.clone()),
482 _ => None,
483 };
484
485 Ok(ResolvedSession {
486 object_id,
487 user_object_id,
488 session_token: session_token.to_string(),
489 installation_id,
490 expires_at,
491 row,
492 })
493}
494
495pub async fn revoke<S: StorageAdapter>(
497 storage: &S,
498 session_token: &str,
499) -> Result<bool, ParseError> {
500 let query = Query::from_constraints(vec![Constraint::equal(
501 "sessionToken",
502 ParseValue::String(session_token.to_string()),
503 )]);
504 let deleted = storage.delete(&session_schema(), &query).await?;
505 Ok(deleted > 0)
506}
507
508pub async fn revoke_all_for_user<S: StorageAdapter>(
518 storage: &S,
519 user_object_id: &str,
520) -> Result<u64, ParseError> {
521 let query = Query::from_constraints(vec![Constraint::equal(
522 "user",
523 ParseValue::Pointer {
524 class_name: "_User".to_string(),
525 object_id: user_object_id.to_string(),
526 },
527 )]);
528 storage.delete(&session_schema(), &query).await
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::testing::FakeStorage;
535 use std::collections::HashSet;
536
537 fn cfg() -> SessionConfig {
538 SessionConfig::default()
539 }
540
541 #[test]
542 fn a_token_is_r_plus_thirty_two_lowercase_hex() {
543 let t = new_session_token();
544 assert_eq!(t.len(), 34, "r: plus 32 hex characters: {t}");
545 let hex = t.strip_prefix("r:").expect("the r: prefix is load-bearing");
546 assert_eq!(hex.len(), 32);
547 assert!(
548 hex.bytes()
549 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
550 "lowercase hex only, matching Buffer.toString('hex'): {hex}"
551 );
552 }
553
554 #[test]
557 fn tokens_do_not_use_the_object_id_alphabet() {
558 let mut seen: HashSet<char> = HashSet::new();
559 for _ in 0..500 {
560 seen.extend(new_session_token()[2..].chars());
561 }
562 assert_eq!(seen.len(), 16, "a hex token uses exactly 16 characters");
563 assert!(seen
564 .iter()
565 .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()));
566 }
567
568 #[test]
569 fn tokens_do_not_repeat() {
570 let tokens: HashSet<String> = (0..1000).map(|_| new_session_token()).collect();
571 assert_eq!(tokens.len(), 1000);
572 }
573
574 #[tokio::test]
575 async fn the_row_is_upstreams_columns_in_upstreams_order() {
576 let s = FakeStorage::new();
577 let created = create_session(
578 &s,
579 &cfg(),
580 NewSession {
581 user_object_id: "user000001",
582 created_with: Some(CreatedWith::signup(None)),
583 installation_id: Some("install-1"),
584 },
585 )
586 .await
587 .expect("create");
588
589 let rows = s.rows("_Session");
590 assert_eq!(rows.len(), 1);
591 let row = &rows[0];
592 let keys: Vec<&str> = row.keys().map(String::as_str).collect();
593 assert_eq!(
594 keys,
595 vec![
596 "sessionToken",
597 "user",
598 "createdWith",
599 "expiresAt",
600 "installationId",
601 "updatedAt",
602 "createdAt",
603 "objectId",
604 ]
605 );
606
607 assert!(
608 matches!(row.get("user"), Some(ParseValue::Pointer { class_name, object_id })
609 if class_name == "_User" && object_id == "user000001")
610 );
611 assert!(
612 matches!(row.get("objectId"), Some(ParseValue::String(id)) if id == &created.object_id)
613 );
614 assert!(
615 matches!(row.get("sessionToken"), Some(ParseValue::String(t)) if t == &created.session_token)
616 );
617 }
618
619 #[tokio::test]
620 async fn created_with_carries_action_and_provider_for_signup_and_login() {
621 for (built, action, provider) in [
622 (CreatedWith::signup(None), "signup", Some("password")),
623 (CreatedWith::login(None), "login", Some("password")),
624 (
625 CreatedWith::login(Some("facebook")),
626 "login",
627 Some("facebook"),
628 ),
629 ] {
630 let ParseValue::Object(map) = built.to_value() else {
631 panic!("createdWith is an object");
632 };
633 let keys: Vec<&str> = map.keys().map(String::as_str).collect();
634 assert_eq!(keys, vec!["action", "authProvider"]);
635 assert!(matches!(map.get("action"), Some(ParseValue::String(a)) if a == action));
636 assert!(
637 matches!(map.get("authProvider"), Some(ParseValue::String(p)) if Some(p.as_str()) == provider)
638 );
639 }
640 }
641
642 #[tokio::test]
645 async fn upgrade_and_create_omit_the_auth_provider_key_entirely() {
646 for (built, action) in [
647 (CreatedWith::upgrade(), "upgrade"),
648 (CreatedWith::create(), "create"),
649 ] {
650 assert_eq!(built.auth_provider, None);
651 let ParseValue::Object(map) = built.to_value() else {
652 panic!("createdWith is an object");
653 };
654 let keys: Vec<&str> = map.keys().map(String::as_str).collect();
655 assert_eq!(
656 keys,
657 vec!["action"],
658 "authProvider must be absent, not empty"
659 );
660 assert!(matches!(map.get("action"), Some(ParseValue::String(a)) if a == action));
661 }
662 }
663
664 #[tokio::test]
665 async fn a_session_row_has_no_acl() {
666 let s = FakeStorage::new();
667 create_session(
668 &s,
669 &cfg(),
670 NewSession {
671 user_object_id: "u1",
672 created_with: Some(CreatedWith::login(None)),
673 installation_id: None,
674 },
675 )
676 .await
677 .expect("create");
678 let rows = s.rows("_Session");
679 assert!(
680 rows[0].get("ACL").is_none(),
681 "upstream refuses an ACL on _Session and adds none of its own"
682 );
683 }
684
685 #[tokio::test]
686 async fn creating_a_session_writes_the_session_schema() {
687 let s = FakeStorage::new();
688 create_session(
689 &s,
690 &cfg(),
691 NewSession {
692 user_object_id: "u1",
693 created_with: Some(CreatedWith::login(None)),
694 installation_id: None,
695 },
696 )
697 .await
698 .expect("create");
699 assert!(s.schema("_Session").is_some());
702 }
703
704 #[tokio::test]
705 async fn expiry_is_now_plus_session_length_and_is_absent_when_disabled() {
706 let s = FakeStorage::new();
707 let created = create_session(
708 &s,
709 &cfg(),
710 NewSession {
711 user_object_id: "u1",
712 created_with: Some(CreatedWith::login(None)),
713 installation_id: None,
714 },
715 )
716 .await
717 .expect("create");
718 let expires = created.expires_at.expect("default config expires sessions");
719 let delta = expires.timestamp_millis() - created.created_at.timestamp_millis();
720 assert_eq!(delta, 31_536_000 * 1000, "one year, in milliseconds");
721
722 let never = SessionConfig {
723 expire_inactive_sessions: false,
724 ..SessionConfig::default()
725 };
726 let created = create_session(
727 &s,
728 &never,
729 NewSession {
730 user_object_id: "u2",
731 created_with: Some(CreatedWith::login(None)),
732 installation_id: None,
733 },
734 )
735 .await
736 .expect("create");
737 assert_eq!(created.expires_at, None);
738 let row = s
739 .rows("_Session")
740 .into_iter()
741 .find(|r| matches!(r.get("user"), Some(ParseValue::Pointer { object_id, .. }) if object_id == "u2"))
742 .expect("row");
743 assert!(row.get("expiresAt").is_none());
744 }
745
746 #[tokio::test]
747 async fn a_minted_token_resolves_to_its_user() {
748 let s = FakeStorage::new();
749 let created = create_session(
750 &s,
751 &cfg(),
752 NewSession {
753 user_object_id: "user000001",
754 created_with: Some(CreatedWith::signup(None)),
755 installation_id: Some("install-1"),
756 },
757 )
758 .await
759 .expect("create");
760
761 let resolved = resolve_session(&s, &created.session_token)
762 .await
763 .expect("resolve");
764 assert_eq!(resolved.user_object_id, "user000001");
765 assert_eq!(resolved.object_id, created.object_id);
766 assert_eq!(resolved.installation_id.as_deref(), Some("install-1"));
767 assert_eq!(resolved.expires_at, created.expires_at);
768 }
769
770 #[tokio::test]
771 async fn an_unknown_token_is_invalid_session_token() {
772 let s = FakeStorage::new();
773 let e = resolve_session(&s, "r:nope").await.unwrap_err();
774 assert_eq!(e.code, ErrorCode::InvalidSessionToken);
775 assert_eq!(e.message, "Invalid session token");
776 }
777
778 #[tokio::test]
779 async fn a_row_with_no_user_is_invalid_session_token() {
780 let s = FakeStorage::new();
781 s.insert_row(
782 "_Session",
783 vec![
784 ("objectId", ParseValue::String("s1".into())),
785 ("sessionToken", ParseValue::String("r:orphan".into())),
786 ],
787 );
788 let e = resolve_session(&s, "r:orphan").await.unwrap_err();
789 assert_eq!(e.code, ErrorCode::InvalidSessionToken);
790 assert_eq!(e.message, "Invalid session token");
791 }
792
793 #[tokio::test]
796 async fn the_three_failures_are_checked_in_upstreams_order() {
797 let s = FakeStorage::new();
798 let past = ParseDate::parse_iso("2000-01-01T00:00:00.000Z").expect("date");
799
800 s.insert_row(
802 "_Session",
803 vec![
804 ("objectId", ParseValue::String("s1".into())),
805 ("sessionToken", ParseValue::String("r:both".into())),
806 (
807 "user",
808 ParseValue::Pointer {
809 class_name: "_User".into(),
810 object_id: "role:Admins".into(),
811 },
812 ),
813 ("expiresAt", ParseValue::Date(past)),
814 ],
815 );
816 let e = resolve_session(&s, "r:both").await.unwrap_err();
817 assert_eq!(e.code, ErrorCode::InvalidSessionToken);
818 assert_eq!(e.message, "Session token is expired.");
819
820 s.insert_row(
822 "_Session",
823 vec![
824 ("objectId", ParseValue::String("s2".into())),
825 ("sessionToken", ParseValue::String("r:role".into())),
826 (
827 "user",
828 ParseValue::Pointer {
829 class_name: "_User".into(),
830 object_id: "role:Admins".into(),
831 },
832 ),
833 ],
834 );
835 let e = resolve_session(&s, "r:role").await.unwrap_err();
836 assert_eq!(e.code, ErrorCode::InternalServerError);
837 assert_eq!(e.message, "Invalid object ID.");
838
839 s.insert_row(
841 "_Session",
842 vec![
843 ("objectId", ParseValue::String("s3".into())),
844 ("sessionToken", ParseValue::String("r:nouser".into())),
845 ("expiresAt", ParseValue::Date(past)),
846 ],
847 );
848 let e = resolve_session(&s, "r:nouser").await.unwrap_err();
849 assert_eq!(e.message, "Invalid session token");
850 }
851
852 #[tokio::test]
859 async fn upstream_quirk_a_session_with_no_expiry_never_expires() {
860 let s = FakeStorage::new();
861 s.insert_row(
862 "_Session",
863 vec![
864 ("objectId", ParseValue::String("s1".into())),
865 ("sessionToken", ParseValue::String("r:legacy".into())),
866 (
867 "user",
868 ParseValue::Pointer {
869 class_name: "_User".into(),
870 object_id: "u1".into(),
871 },
872 ),
873 ],
874 );
875 let resolved = resolve_session(&s, "r:legacy").await.expect("resolve");
876 assert_eq!(resolved.user_object_id, "u1");
877 assert_eq!(resolved.expires_at, None);
878 }
879
880 #[tokio::test]
881 async fn duplicate_destruction_is_per_user_and_per_installation() {
882 let s = FakeStorage::new();
883 let mk = |user: &'static str, install: Option<&'static str>| NewSession {
884 user_object_id: user,
885 created_with: Some(CreatedWith::login(None)),
886 installation_id: install,
887 };
888
889 let phone_a = create_session(&s, &cfg(), mk("alice", Some("phone")))
890 .await
891 .expect("create");
892 let tablet_a = create_session(&s, &cfg(), mk("alice", Some("tablet")))
893 .await
894 .expect("create");
895 let phone_b = create_session(&s, &cfg(), mk("bob", Some("phone")))
896 .await
897 .expect("create");
898
899 let phone_a2 = create_session(&s, &cfg(), mk("alice", Some("phone")))
901 .await
902 .expect("create");
903
904 assert!(resolve_session(&s, &phone_a.session_token).await.is_err());
905 assert!(resolve_session(&s, &phone_a2.session_token).await.is_ok());
906 assert!(
907 resolve_session(&s, &tablet_a.session_token).await.is_ok(),
908 "two devices, two sessions: the match is on user AND installationId"
909 );
910 assert!(
911 resolve_session(&s, &phone_b.session_token).await.is_ok(),
912 "another user's session on the same installationId must survive"
913 );
914 }
915
916 #[tokio::test]
917 async fn without_an_installation_id_nothing_is_destroyed() {
918 let s = FakeStorage::new();
919 let mk = || NewSession {
920 user_object_id: "alice",
921 created_with: Some(CreatedWith::login(None)),
922 installation_id: None,
923 };
924 let first = create_session(&s, &cfg(), mk()).await.expect("create");
925 let second = create_session(&s, &cfg(), mk()).await.expect("create");
926 assert!(resolve_session(&s, &first.session_token).await.is_ok());
929 assert!(resolve_session(&s, &second.session_token).await.is_ok());
930 }
931
932 #[tokio::test]
933 async fn revoke_removes_one_session_and_revoke_all_removes_the_users() {
934 let s = FakeStorage::new();
935 let mk = |user: &'static str| NewSession {
936 user_object_id: user,
937 created_with: Some(CreatedWith::login(None)),
938 installation_id: None,
939 };
940 let a1 = create_session(&s, &cfg(), mk("alice")).await.expect("c");
941 let a2 = create_session(&s, &cfg(), mk("alice")).await.expect("c");
942 let b1 = create_session(&s, &cfg(), mk("bob")).await.expect("c");
943
944 assert!(revoke(&s, &a1.session_token).await.expect("revoke"));
945 assert!(
946 !revoke(&s, &a1.session_token).await.expect("revoke"),
947 "revoking twice reports the second as a miss"
948 );
949 assert!(resolve_session(&s, &a2.session_token).await.is_ok());
950
951 assert_eq!(revoke_all_for_user(&s, "alice").await.expect("revoke"), 1);
952 assert!(resolve_session(&s, &a2.session_token).await.is_err());
953 assert!(
954 resolve_session(&s, &b1.session_token).await.is_ok(),
955 "another user's sessions must survive"
956 );
957 }
958
959 #[tokio::test]
960 async fn resolution_reads_at_most_one_row() {
961 let s = FakeStorage::new();
962 let created = create_session(
963 &s,
964 &cfg(),
965 NewSession {
966 user_object_id: "u1",
967 created_with: Some(CreatedWith::login(None)),
968 installation_id: None,
969 },
970 )
971 .await
972 .expect("create");
973 s.reset_find_count();
974 resolve_session(&s, &created.session_token)
975 .await
976 .expect("resolve");
977 assert_eq!(
978 s.find_count(),
979 1,
980 "session resolution is on every authenticated request; it stays one query"
981 );
982 assert_eq!(s.last_find_limit(), Some(Some(1)));
983 }
984}