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