Skip to main content

better_auth_api/plugins/
session_management.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use validator::Validate;
4
5use better_auth_core::config::AuthConfig;
6use better_auth_core::entity::{AuthSession, AuthUser};
7use better_auth_core::wire::{SessionView, UserView};
8use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
9
10use better_auth_core::AuthResult;
11use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
12
13use super::StatusResponse;
14use super::helpers::{admin_plugin_enabled, delete_session_cookie_headers, get_cookie};
15use better_auth_core::SuccessResponse;
16
17/// Session management plugin for handling session operations
18pub struct SessionManagementPlugin {
19    config: SessionManagementConfig,
20}
21
22#[derive(Debug, Clone, better_auth_core::PluginConfig)]
23#[plugin(name = "SessionManagementPlugin")]
24pub struct SessionManagementConfig {
25    #[config(default = true)]
26    pub enable_session_listing: bool,
27    #[config(default = true)]
28    pub enable_session_revocation: bool,
29    #[config(default = true)]
30    pub require_authentication: bool,
31}
32
33// Request structures for session endpoints
34#[derive(Debug, Deserialize, Validate)]
35struct RevokeSessionRequest {
36    #[validate(length(min = 1, message = "Token is required"))]
37    token: String,
38}
39
40#[derive(Debug, Serialize)]
41struct GetSessionResponse<S: Serialize, U: Serialize> {
42    session: S,
43    user: U,
44}
45
46#[async_trait]
47impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for SessionManagementPlugin {
48    fn name(&self) -> &'static str {
49        "session-management"
50    }
51
52    fn routes(&self) -> Vec<AuthRoute> {
53        vec![
54            AuthRoute::get("/get-session", "get_session"),
55            AuthRoute::post("/sign-out", "sign_out"),
56            AuthRoute::get("/list-sessions", "list_sessions"),
57            AuthRoute::post("/revoke-session", "revoke_session"),
58            AuthRoute::post("/revoke-sessions", "revoke_sessions"),
59            AuthRoute::post("/revoke-other-sessions", "revoke_other_sessions"),
60        ]
61    }
62
63    async fn on_request(
64        &self,
65        req: &AuthRequest,
66        ctx: &AuthContext<S>,
67    ) -> AuthResult<Option<AuthResponse>> {
68        match (req.method(), req.path()) {
69            (HttpMethod::Get, "/get-session") => Ok(Some(self.handle_get_session(req, ctx).await?)),
70            (HttpMethod::Post, "/sign-out") => Ok(Some(self.handle_sign_out(req, ctx).await?)),
71            (HttpMethod::Get, "/list-sessions") if self.config.enable_session_listing => {
72                Ok(Some(self.handle_list_sessions(req, ctx).await?))
73            }
74            (HttpMethod::Post, "/revoke-session") if self.config.enable_session_revocation => {
75                Ok(Some(self.handle_revoke_session(req, ctx).await?))
76            }
77            (HttpMethod::Post, "/revoke-sessions") if self.config.enable_session_revocation => {
78                Ok(Some(self.handle_revoke_sessions(req, ctx).await?))
79            }
80            (HttpMethod::Post, "/revoke-other-sessions")
81                if self.config.enable_session_revocation =>
82            {
83                Ok(Some(self.handle_revoke_other_sessions(req, ctx).await?))
84            }
85            _ => Ok(None),
86        }
87    }
88}
89
90// ---------------------------------------------------------------------------
91// Core functions — framework-agnostic business logic
92// ---------------------------------------------------------------------------
93
94pub(crate) async fn sign_out_core(
95    session: &impl AuthSession,
96    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
97) -> AuthResult<SuccessResponse> {
98    ctx.database.delete_session(session.token()).await?;
99    Ok(SuccessResponse { success: true })
100}
101
102pub(crate) async fn list_sessions_core(
103    user_id: impl AsRef<str>,
104    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
105) -> AuthResult<Vec<SessionView>> {
106    let sessions = ctx.session_manager().list_user_sessions(user_id).await?;
107    Ok(sessions.iter().map(SessionView::from).collect())
108}
109
110pub(crate) async fn revoke_session_core(
111    user: &impl AuthUser,
112    token: &str,
113    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
114) -> AuthResult<StatusResponse> {
115    let session_manager = ctx.session_manager();
116    if let Some(session_to_revoke) = session_manager.get_session(token).await?
117        && session_to_revoke.user_id() == user.id()
118    {
119        ctx.database.delete_session(token).await?;
120    }
121    Ok(StatusResponse { status: true })
122}
123
124pub(crate) async fn revoke_sessions_core(
125    user_id: impl AsRef<str>,
126    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
127) -> AuthResult<StatusResponse> {
128    ctx.database.delete_user_sessions(user_id.as_ref()).await?;
129    Ok(StatusResponse { status: true })
130}
131
132pub(crate) async fn revoke_other_sessions_core(
133    user_id: impl AsRef<str>,
134    current_session: &impl AuthSession,
135    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
136) -> AuthResult<StatusResponse> {
137    let all_sessions = ctx.session_manager().list_user_sessions(user_id).await?;
138    for session in all_sessions {
139        if session.token() != current_session.token() {
140            ctx.database.delete_session(session.token()).await?;
141        }
142    }
143    Ok(StatusResponse { status: true })
144}
145
146// ---------------------------------------------------------------------------
147// Old handler methods — delegate to core functions
148// ---------------------------------------------------------------------------
149
150impl SessionManagementPlugin {
151    async fn handle_get_session(
152        &self,
153        req: &AuthRequest,
154        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
155    ) -> AuthResult<AuthResponse> {
156        // Returns 200 with null body when unauthenticated (never an error status).
157        match ctx.require_session(req).await {
158            Ok((user, session)) => {
159                let response = GetSessionResponse {
160                    session: SessionView::from(&session),
161                    user: UserView::from(&user),
162                };
163                Ok(AuthResponse::json(200, &response)?)
164            }
165            Err(_) => {
166                let mut response = AuthResponse::json(200, &serde_json::Value::Null)?;
167                if get_cookie(req, &ctx.config.session.cookie_name)
168                    .is_some_and(|value| !value.is_empty())
169                {
170                    for cookie in delete_session_cookie_headers(&ctx.config) {
171                        response.headers.append("Set-Cookie", cookie);
172                    }
173                }
174                Ok(response)
175            }
176        }
177    }
178
179    async fn handle_sign_out(
180        &self,
181        req: &AuthRequest,
182        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
183    ) -> AuthResult<AuthResponse> {
184        if let Ok((_user, session)) = ctx.require_session(req).await {
185            let _ = sign_out_core(&session, ctx).await;
186        }
187
188        let mut response = AuthResponse::json(200, &SuccessResponse { success: true })?;
189        for cookie in sign_out_cookies(&ctx.config) {
190            response.headers.append("Set-Cookie", cookie);
191        }
192        Ok(response)
193    }
194
195    async fn handle_list_sessions(
196        &self,
197        req: &AuthRequest,
198        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
199    ) -> AuthResult<AuthResponse> {
200        let (user, _) = ctx.require_session(req).await?;
201        let mut sessions = list_sessions_core(user.id(), ctx).await?;
202        if admin_plugin_enabled(ctx) {
203            sessions.retain(|session| session.impersonated_by.is_none());
204        }
205        Ok(AuthResponse::json(200, &sessions)?)
206    }
207
208    async fn handle_revoke_session(
209        &self,
210        req: &AuthRequest,
211        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
212    ) -> AuthResult<AuthResponse> {
213        let (user, _) = ctx.require_session(req).await?;
214
215        let revoke_req: RevokeSessionRequest = match better_auth_core::validate_request_body(req) {
216            Ok(v) => v,
217            Err(resp) => return Ok(resp),
218        };
219
220        let response = revoke_session_core(&user, &revoke_req.token, ctx).await?;
221        Ok(AuthResponse::json(200, &response)?)
222    }
223
224    async fn handle_revoke_sessions(
225        &self,
226        req: &AuthRequest,
227        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
228    ) -> AuthResult<AuthResponse> {
229        let (user, _) = ctx.require_session(req).await?;
230        let response = revoke_sessions_core(user.id(), ctx).await?;
231        Ok(AuthResponse::json(200, &response)?)
232    }
233
234    async fn handle_revoke_other_sessions(
235        &self,
236        req: &AuthRequest,
237        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
238    ) -> AuthResult<AuthResponse> {
239        let (user, current_session) = ctx.require_session(req).await?;
240        let response = revoke_other_sessions_core(user.id(), &current_session, ctx).await?;
241        Ok(AuthResponse::json(200, &response)?)
242    }
243}
244
245fn related_cookie_name(config: &AuthConfig, suffix: &str) -> String {
246    config
247        .session
248        .cookie_name
249        .strip_suffix("session_token")
250        .map(|prefix| format!("{}{}", prefix, suffix))
251        .unwrap_or_else(|| format!("better-auth.{}", suffix))
252}
253
254fn sign_out_cookies(config: &AuthConfig) -> Vec<String> {
255    let mut cookies = vec![
256        better_auth_core::utils::cookie_utils::create_clear_session_cookie(config),
257        better_auth_core::utils::cookie_utils::create_clear_cookie(
258            &related_cookie_name(config, "session_data"),
259            config,
260        ),
261        better_auth_core::utils::cookie_utils::create_clear_cookie(
262            &related_cookie_name(config, "dont_remember"),
263            config,
264        ),
265    ];
266
267    if config.account.store_account_cookie {
268        cookies.push(better_auth_core::utils::cookie_utils::create_clear_cookie(
269            &related_cookie_name(config, "account_data"),
270            config,
271        ));
272    }
273
274    cookies
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::plugins::test_helpers;
281    use better_auth_core::config::AccountConfig;
282    use better_auth_core::wire::SessionView;
283    use better_auth_core::{CreateSession, CreateUser};
284    use chrono::{Duration, Utc};
285
286    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
287    #[tokio::test]
288    async fn test_get_session_success() {
289        let plugin = SessionManagementPlugin::new();
290        let (ctx, _user, session) = test_helpers::create_test_context_with_user(
291            CreateUser::new()
292                .with_email("test@example.com")
293                .with_name("Test User"),
294            Duration::hours(24),
295        )
296        .await;
297
298        let req = test_helpers::create_auth_request_no_query(
299            HttpMethod::Get,
300            "/get-session",
301            Some(&session.token),
302            None,
303        );
304        let response = plugin.handle_get_session(&req, &ctx).await.unwrap();
305
306        assert_eq!(response.status, 200);
307
308        let body_str = String::from_utf8(response.body).unwrap();
309        let response_data: serde_json::Value = serde_json::from_str(&body_str).unwrap();
310        assert_eq!(
311            response_data["session"]["token"].as_str().unwrap(),
312            session.token
313        );
314        assert_eq!(
315            response_data["user"]["email"]
316                .as_str()
317                .map(|s| s.to_string()),
318            Some("test@example.com".to_string())
319        );
320    }
321
322    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
323    #[tokio::test]
324    async fn test_get_session_unauthorized() {
325        // /get-session returns 200 with null body when unauthenticated.
326        let plugin = SessionManagementPlugin::new();
327        let (ctx, _user, _session) = test_helpers::create_test_context_with_user(
328            CreateUser::new()
329                .with_email("test@example.com")
330                .with_name("Test User"),
331            Duration::hours(24),
332        )
333        .await;
334
335        let req =
336            test_helpers::create_auth_request_no_query(HttpMethod::Get, "/get-session", None, None);
337        let response = plugin.handle_get_session(&req, &ctx).await.unwrap();
338        assert_eq!(response.status, 200);
339        let body: serde_json::Value = serde_json::from_slice(&response.body).expect("valid JSON");
340        assert!(body.is_null());
341    }
342
343    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
344    #[tokio::test]
345    async fn test_sign_out_success() {
346        let plugin = SessionManagementPlugin::new();
347        let (ctx, _user, session) = test_helpers::create_test_context_with_user(
348            CreateUser::new()
349                .with_email("test@example.com")
350                .with_name("Test User"),
351            Duration::hours(24),
352        )
353        .await;
354
355        let req = test_helpers::create_auth_request_no_query(
356            HttpMethod::Post,
357            "/sign-out",
358            Some(&session.token),
359            Some(b"{}".to_vec()),
360        );
361        let response = plugin.handle_sign_out(&req, &ctx).await.unwrap();
362
363        assert_eq!(response.status, 200);
364
365        let body_str = String::from_utf8(response.body).unwrap();
366        let response_data: SuccessResponse = serde_json::from_str(&body_str).unwrap();
367        assert!(response_data.success);
368
369        let session_check = ctx.database.get_session(&session.token).await.unwrap();
370        assert!(session_check.is_none());
371    }
372
373    #[tokio::test]
374    async fn test_sign_out_clears_account_cookie_when_enabled() {
375        let plugin = SessionManagementPlugin::new();
376        let config = test_helpers::create_test_config().account(AccountConfig {
377            store_account_cookie: true,
378            ..Default::default()
379        });
380        let ctx = test_helpers::create_test_context_with_config(config).await;
381        let (_user, session) = test_helpers::create_user_and_session(
382            &ctx,
383            CreateUser::new()
384                .with_email("test@example.com")
385                .with_name("Test User"),
386            Duration::hours(24),
387        )
388        .await;
389
390        let req = test_helpers::create_auth_request_no_query(
391            HttpMethod::Post,
392            "/sign-out",
393            Some(&session.token),
394            Some(b"{}".to_vec()),
395        );
396        let response = plugin.handle_sign_out(&req, &ctx).await.unwrap();
397
398        let account_cookie_name = format!("{}=", related_cookie_name(&ctx.config, "account_data"));
399        assert!(
400            response
401                .headers
402                .get_all("Set-Cookie")
403                .any(|cookie| cookie.starts_with(&account_cookie_name)),
404            "sign-out should clear the account_data cookie when store_account_cookie is enabled"
405        );
406    }
407
408    #[tokio::test]
409    async fn test_sign_out_does_not_emit_account_cookie_when_disabled() {
410        let plugin = SessionManagementPlugin::new();
411        let (ctx, _user, session) = test_helpers::create_test_context_with_user(
412            CreateUser::new()
413                .with_email("test@example.com")
414                .with_name("Test User"),
415            Duration::hours(24),
416        )
417        .await;
418
419        let req = test_helpers::create_auth_request_no_query(
420            HttpMethod::Post,
421            "/sign-out",
422            Some(&session.token),
423            Some(b"{}".to_vec()),
424        );
425        let response = plugin.handle_sign_out(&req, &ctx).await.unwrap();
426
427        let account_cookie_name = format!("{}=", related_cookie_name(&ctx.config, "account_data"));
428        assert!(
429            !response
430                .headers
431                .get_all("Set-Cookie")
432                .any(|cookie| cookie.starts_with(&account_cookie_name)),
433            "sign-out should not emit account_data clearing cookies when store_account_cookie is disabled"
434        );
435    }
436
437    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
438    #[tokio::test]
439    async fn test_list_sessions_success() {
440        let plugin = SessionManagementPlugin::new();
441        let (ctx, user, session) = test_helpers::create_test_context_with_user(
442            CreateUser::new()
443                .with_email("test@example.com")
444                .with_name("Test User"),
445            Duration::hours(24),
446        )
447        .await;
448
449        let create_session2 = CreateSession {
450            user_id: user.id.clone(),
451            expires_at: Utc::now() + Duration::hours(24),
452            ip_address: Some("192.168.1.1".to_string()),
453            user_agent: Some("another-agent".to_string()),
454            impersonated_by: None,
455            active_organization_id: None,
456        };
457        ctx.database.create_session(create_session2).await.unwrap();
458
459        let req = test_helpers::create_auth_request_no_query(
460            HttpMethod::Get,
461            "/list-sessions",
462            Some(&session.token),
463            None,
464        );
465        let response = plugin.handle_list_sessions(&req, &ctx).await.unwrap();
466
467        assert_eq!(response.status, 200);
468
469        let body_str = String::from_utf8(response.body).unwrap();
470        let sessions: Vec<SessionView> = serde_json::from_str(&body_str).unwrap();
471        assert_eq!(sessions.len(), 2);
472    }
473
474    #[tokio::test]
475    async fn test_list_sessions_filters_impersonated_sessions_when_admin_plugin_is_enabled() {
476        let plugin = SessionManagementPlugin::new();
477        let (mut ctx, user, session) = test_helpers::create_test_context_with_user(
478            CreateUser::new()
479                .with_email("test@example.com")
480                .with_name("Test User"),
481            Duration::hours(24),
482        )
483        .await;
484        ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
485
486        let direct_session = CreateSession {
487            user_id: user.id.clone(),
488            expires_at: Utc::now() + Duration::hours(24),
489            ip_address: Some("192.168.1.1".to_string()),
490            user_agent: Some("another-agent".to_string()),
491            impersonated_by: None,
492            active_organization_id: None,
493        };
494        ctx.database.create_session(direct_session).await.unwrap();
495
496        let impersonated_session = CreateSession {
497            user_id: user.id.clone(),
498            expires_at: Utc::now() + Duration::hours(24),
499            ip_address: Some("10.0.0.5".to_string()),
500            user_agent: Some("impersonated-agent".to_string()),
501            impersonated_by: Some("admin-user".to_string()),
502            active_organization_id: None,
503        };
504        let impersonated = ctx
505            .database
506            .create_session(impersonated_session)
507            .await
508            .unwrap();
509
510        let req = test_helpers::create_auth_request_no_query(
511            HttpMethod::Get,
512            "/list-sessions",
513            Some(&session.token),
514            None,
515        );
516        let response = plugin.handle_list_sessions(&req, &ctx).await.unwrap();
517
518        assert_eq!(response.status, 200);
519
520        let body_str = String::from_utf8(response.body).unwrap();
521        let sessions: Vec<SessionView> = serde_json::from_str(&body_str).unwrap();
522        assert_eq!(sessions.len(), 2);
523        assert!(
524            sessions
525                .iter()
526                .all(|session| session.token != impersonated.token),
527            "impersonated sessions should not be returned from /list-sessions"
528        );
529    }
530
531    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
532    #[tokio::test]
533    async fn test_revoke_session_success() {
534        let plugin = SessionManagementPlugin::new();
535        let (ctx, user, session) = test_helpers::create_test_context_with_user(
536            CreateUser::new()
537                .with_email("test@example.com")
538                .with_name("Test User"),
539            Duration::hours(24),
540        )
541        .await;
542
543        let create_session2 = CreateSession {
544            user_id: user.id.clone(),
545            expires_at: Utc::now() + Duration::hours(24),
546            ip_address: Some("192.168.1.1".to_string()),
547            user_agent: Some("another-agent".to_string()),
548            impersonated_by: None,
549            active_organization_id: None,
550        };
551        let session2 = ctx.database.create_session(create_session2).await.unwrap();
552
553        let body = serde_json::json!({ "token": session2.token });
554        let req = test_helpers::create_auth_request_no_query(
555            HttpMethod::Post,
556            "/revoke-session",
557            Some(&session.token),
558            Some(body.to_string().into_bytes()),
559        );
560
561        let response = plugin.handle_revoke_session(&req, &ctx).await.unwrap();
562        assert_eq!(response.status, 200);
563
564        let session2_check = ctx.database.get_session(&session2.token).await.unwrap();
565        assert!(session2_check.is_none());
566
567        let session1_check = ctx.database.get_session(&session.token).await.unwrap();
568        assert!(session1_check.is_some());
569    }
570
571    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
572    #[tokio::test]
573    async fn test_revoke_session_forbidden_different_user() {
574        let plugin = SessionManagementPlugin::new();
575        let (ctx, _user1, session1) = test_helpers::create_test_context_with_user(
576            CreateUser::new()
577                .with_email("test@example.com")
578                .with_name("Test User"),
579            Duration::hours(24),
580        )
581        .await;
582
583        let create_user2 = CreateUser::new()
584            .with_email("user2@example.com")
585            .with_name("User Two");
586        let user2 = ctx.database.create_user(create_user2).await.unwrap();
587
588        let create_session2 = CreateSession {
589            user_id: user2.id,
590            expires_at: Utc::now() + Duration::hours(24),
591            ip_address: Some("192.168.1.1".to_string()),
592            user_agent: Some("another-agent".to_string()),
593            impersonated_by: None,
594            active_organization_id: None,
595        };
596        let session2 = ctx.database.create_session(create_session2).await.unwrap();
597
598        let body = serde_json::json!({ "token": session2.token });
599        let req = test_helpers::create_auth_request_no_query(
600            HttpMethod::Post,
601            "/revoke-session",
602            Some(&session1.token),
603            Some(body.to_string().into_bytes()),
604        );
605
606        let response = plugin.handle_revoke_session(&req, &ctx).await.unwrap();
607        assert_eq!(response.status, 200);
608
609        let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
610        assert_eq!(body["status"], true);
611
612        let still_exists = ctx.database.get_session(&session2.token).await.unwrap();
613        assert!(
614            still_exists.is_some(),
615            "other user's session must not be revoked"
616        );
617    }
618
619    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
620    #[tokio::test]
621    async fn test_revoke_sessions_success() {
622        let plugin = SessionManagementPlugin::new();
623        let (ctx, user, session1) = test_helpers::create_test_context_with_user(
624            CreateUser::new()
625                .with_email("test@example.com")
626                .with_name("Test User"),
627            Duration::hours(24),
628        )
629        .await;
630
631        let create_session2 = CreateSession {
632            user_id: user.id.clone(),
633            expires_at: Utc::now() + Duration::hours(24),
634            ip_address: Some("192.168.1.1".to_string()),
635            user_agent: Some("another-agent".to_string()),
636            impersonated_by: None,
637            active_organization_id: None,
638        };
639        ctx.database.create_session(create_session2).await.unwrap();
640
641        let req = test_helpers::create_auth_request_no_query(
642            HttpMethod::Post,
643            "/revoke-sessions",
644            Some(&session1.token),
645            Some(b"{}".to_vec()),
646        );
647        let response = plugin.handle_revoke_sessions(&req, &ctx).await.unwrap();
648
649        assert_eq!(response.status, 200);
650
651        let user_sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
652        assert_eq!(user_sessions.len(), 0);
653    }
654
655    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
656    #[tokio::test]
657    async fn test_plugin_routes() {
658        let plugin = SessionManagementPlugin::new();
659        let routes = AuthPlugin::<
660            better_auth_seaorm::store::__private_test_support::bundled_schema::BundledSchema,
661        >::routes(&plugin);
662
663        assert_eq!(routes.len(), 6);
664        assert!(
665            routes
666                .iter()
667                .any(|r| r.path == "/get-session" && r.method == HttpMethod::Get)
668        );
669        assert!(
670            !routes
671                .iter()
672                .any(|r| r.path == "/get-session" && r.method == HttpMethod::Post)
673        );
674        assert!(
675            routes
676                .iter()
677                .any(|r| r.path == "/sign-out" && r.method == HttpMethod::Post)
678        );
679        assert!(
680            routes
681                .iter()
682                .any(|r| r.path == "/list-sessions" && r.method == HttpMethod::Get)
683        );
684        assert!(
685            routes
686                .iter()
687                .any(|r| r.path == "/revoke-session" && r.method == HttpMethod::Post)
688        );
689        assert!(
690            routes
691                .iter()
692                .any(|r| r.path == "/revoke-sessions" && r.method == HttpMethod::Post)
693        );
694    }
695
696    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
697    #[tokio::test]
698    async fn test_plugin_on_request_routing() {
699        let plugin = SessionManagementPlugin::new();
700        let (ctx, _user, session) = test_helpers::create_test_context_with_user(
701            CreateUser::new()
702                .with_email("test@example.com")
703                .with_name("Test User"),
704            Duration::hours(24),
705        )
706        .await;
707
708        // Test valid route
709        let req = test_helpers::create_auth_request_no_query(
710            HttpMethod::Get,
711            "/get-session",
712            Some(&session.token),
713            None,
714        );
715        let response = plugin.on_request(&req, &ctx).await.unwrap();
716        assert!(response.is_some());
717        assert_eq!(response.unwrap().status, 200);
718
719        let req = test_helpers::create_auth_request_no_query(
720            HttpMethod::Post,
721            "/get-session",
722            Some(&session.token),
723            Some(b"{}".to_vec()),
724        );
725        let response = plugin.on_request(&req, &ctx).await.unwrap();
726        assert!(response.is_none());
727
728        // Test invalid route
729        let req = test_helpers::create_auth_request_no_query(
730            HttpMethod::Get,
731            "/invalid-route",
732            Some(&session.token),
733            None,
734        );
735        let response = plugin.on_request(&req, &ctx).await.unwrap();
736        assert!(response.is_none());
737    }
738
739    // Upstream reference: packages/better-auth/src/api/routes/session-api.test.ts :: describe("session") and packages/better-auth/src/api/routes/sign-out.test.ts :: describe("sign-out"); adapted to the Rust session-management plugin.
740    #[tokio::test]
741    async fn test_configuration() {
742        let plugin = SessionManagementPlugin::new()
743            .enable_session_listing(false)
744            .enable_session_revocation(false)
745            .require_authentication(false);
746
747        assert!(!plugin.config.enable_session_listing);
748        assert!(!plugin.config.enable_session_revocation);
749        assert!(!plugin.config.require_authentication);
750
751        let (ctx, _user, session) = test_helpers::create_test_context_with_user(
752            CreateUser::new()
753                .with_email("test@example.com")
754                .with_name("Test User"),
755            Duration::hours(24),
756        )
757        .await;
758
759        let req = test_helpers::create_auth_request_no_query(
760            HttpMethod::Get,
761            "/list-sessions",
762            Some(&session.token),
763            None,
764        );
765        let response = plugin.on_request(&req, &ctx).await.unwrap();
766        assert!(response.is_none());
767
768        let req = test_helpers::create_auth_request_no_query(
769            HttpMethod::Post,
770            "/revoke-session",
771            Some(&session.token),
772            Some(b"{}".to_vec()),
773        );
774        let response = plugin.on_request(&req, &ctx).await.unwrap();
775        assert!(response.is_none());
776    }
777}