better_auth/
lib.rs

1//! # Better Auth - Rust
2//! 
3//! A comprehensive authentication framework for Rust, inspired by Better-Auth.
4//! 
5//! ## Quick Start
6//! 
7//! ```rust,no_run
8//! use better_auth::{BetterAuth, AuthConfig};
9//! use better_auth::plugins::EmailPasswordPlugin;
10//! 
11//! #[tokio::main]
12//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
13//!     let config = AuthConfig::new("your-secret-key");
14//!     
15//!     let auth = BetterAuth::new(config)
16//!         .plugin(EmailPasswordPlugin::new())
17//!         .build()
18//!         .await?;
19//!         
20//!     Ok(())
21//! }
22//! ```
23
24pub mod core;
25pub mod plugins;
26pub mod adapters;
27pub mod handlers;
28pub mod error;
29pub mod types;
30
31// Re-export commonly used items
32pub use core::{BetterAuth, AuthBuilder, AuthConfig};
33pub use error::{AuthError, AuthResult};
34pub use types::{User, Session, Account, Verification, AuthRequest, AuthResponse, CreateVerification};
35
36#[cfg(feature = "axum")]
37pub use handlers::axum::AxumIntegration;
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use crate::adapters::MemoryDatabaseAdapter;
43    use crate::plugins::EmailPasswordPlugin;
44    use crate::types::{HttpMethod, CreateUser};
45    use serde_json::json;
46    use std::sync::Arc;
47
48    fn test_config() -> AuthConfig {
49        AuthConfig::new("test-secret-key-that-is-at-least-32-characters-long")
50            .base_url("http://localhost:3000")
51            .password_min_length(8)
52    }
53
54    async fn create_test_auth() -> BetterAuth {
55        BetterAuth::new(test_config())
56            .database(MemoryDatabaseAdapter::new())
57            .plugin(EmailPasswordPlugin::new().enable_signup(true))
58            .build()
59            .await
60            .expect("Failed to create test auth instance")
61    }
62
63    #[tokio::test]
64    async fn test_auth_builder() {
65        let auth = create_test_auth().await;
66        assert_eq!(auth.plugin_names(), vec!["email-password"]);
67        assert_eq!(auth.config().secret, "test-secret-key-that-is-at-least-32-characters-long");
68    }
69
70    #[tokio::test]
71    async fn test_signup_flow() {
72        let auth = create_test_auth().await;
73        
74        let signup_data = json!({
75            "email": "test@example.com",
76            "password": "password123",
77            "name": "Test User"
78        });
79        
80        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
81        request.body = Some(signup_data.to_string().into_bytes());
82        request.headers.insert("content-type".to_string(), "application/json".to_string());
83        
84        let response = auth.handle_request(request).await.expect("Signup request failed");
85        
86        assert_eq!(response.status, 200);
87        
88        let response_json: serde_json::Value = serde_json::from_slice(&response.body)
89            .expect("Failed to parse response JSON");
90        
91        assert!(response_json["user"]["id"].is_string());
92        assert_eq!(response_json["user"]["email"], "test@example.com");
93        assert_eq!(response_json["user"]["name"], "Test User");
94        assert!(response_json["token"].is_string());
95    }
96
97    #[tokio::test]
98    async fn test_signin_flow() {
99        let auth = create_test_auth().await;
100        
101        // First, create a user
102        let signup_data = json!({
103            "email": "signin@example.com",
104            "password": "password123",
105            "name": "Signin User"
106        });
107        
108        let mut signup_request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
109        signup_request.body = Some(signup_data.to_string().into_bytes());
110        signup_request.headers.insert("content-type".to_string(), "application/json".to_string());
111        
112        let signup_response = auth.handle_request(signup_request).await.expect("Signup failed");
113        assert_eq!(signup_response.status, 200);
114        
115        // Now test signin
116        let signin_data = json!({
117            "email": "signin@example.com",
118            "password": "password123"
119        });
120        
121        let mut signin_request = AuthRequest::new(HttpMethod::Post, "/sign-in/email");
122        signin_request.body = Some(signin_data.to_string().into_bytes());
123        signin_request.headers.insert("content-type".to_string(), "application/json".to_string());
124        
125        let signin_response = auth.handle_request(signin_request).await.expect("Signin failed");
126        assert_eq!(signin_response.status, 200);
127        
128        let response_json: serde_json::Value = serde_json::from_slice(&signin_response.body)
129            .expect("Failed to parse signin response");
130        
131        assert_eq!(response_json["user"]["email"], "signin@example.com");
132        assert!(response_json["token"].is_string());
133    }
134
135    #[tokio::test]
136    async fn test_duplicate_email_signup() {
137        let auth = create_test_auth().await;
138        
139        let signup_data = json!({
140            "name": "Duplicate User",
141            "email": "duplicate@example.com",
142            "password": "password123"
143        });
144        
145        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
146        request.body = Some(signup_data.to_string().into_bytes());
147        request.headers.insert("content-type".to_string(), "application/json".to_string());
148        
149        // First signup should succeed
150        let response1 = auth.handle_request(request.clone()).await.expect("First signup failed");
151        assert_eq!(response1.status, 200);
152        
153        // Second signup with same email should fail
154        let response2 = auth.handle_request(request).await.expect("Second signup request failed");
155        assert_eq!(response2.status, 409);
156    }
157
158    #[tokio::test]
159    async fn test_invalid_credentials_signin() {
160        let auth = create_test_auth().await;
161        
162        // Try to signin with non-existent user
163        let signin_data = json!({
164            "email": "nonexistent@example.com",
165            "password": "password123"
166        });
167        
168        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-in/email");
169        request.body = Some(signin_data.to_string().into_bytes());
170        request.headers.insert("content-type".to_string(), "application/json".to_string());
171        
172        let response = auth.handle_request(request).await.expect("Request should not panic");
173        assert_eq!(response.status, 401);
174    }
175
176    #[tokio::test]
177    async fn test_weak_password_validation() {
178        let auth = create_test_auth().await;
179        
180        let signup_data = json!({
181            "email": "weak@example.com",
182            "password": "123", // Too short
183            "name": "Weak Password User"
184        });
185        
186        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
187        request.body = Some(signup_data.to_string().into_bytes());
188        request.headers.insert("content-type".to_string(), "application/json".to_string());
189        
190        let response = auth.handle_request(request).await.expect("Request should not panic");
191        assert_eq!(response.status, 400);
192        
193        let response_json: serde_json::Value = serde_json::from_slice(&response.body)
194            .expect("Failed to parse response");
195        assert!(response_json["message"].as_str().unwrap_or("").contains("Password must be at least"));
196    }
197
198    #[tokio::test]
199    async fn test_session_management() {
200        let auth = create_test_auth().await;
201        let session_manager = auth.session_manager();
202        
203        // Create a test user first
204        let database = auth.database();
205        let create_user = CreateUser::new()
206            .with_email("session@example.com")
207            .with_name("Session User");
208        
209        let user = database.create_user(create_user).await.expect("Failed to create user");
210        
211        // Create a session
212        let session = session_manager.create_session(&user, None, None).await
213            .expect("Failed to create session");
214        
215        assert!(session.token.starts_with("session_"));
216        assert_eq!(session.user_id, user.id);
217        assert!(session.active);
218        
219        // Retrieve the session
220        let retrieved_session = session_manager.get_session(&session.token).await
221            .expect("Failed to get session")
222            .expect("Session not found");
223        
224        assert_eq!(retrieved_session.id, session.id);
225        assert_eq!(retrieved_session.user_id, user.id);
226        
227        // Delete the session
228        session_manager.delete_session(&session.token).await
229            .expect("Failed to delete session");
230        
231        // Verify session is deleted
232        let deleted_session = session_manager.get_session(&session.token).await
233            .expect("Failed to check deleted session");
234        assert!(deleted_session.is_none());
235    }
236
237    #[tokio::test]
238    async fn test_token_format_validation() {
239        let auth = create_test_auth().await;
240        let session_manager = auth.session_manager();
241        
242        // Valid token format: "session_" + base64 encoded 32 bytes = "session_" + 43 chars
243        assert!(session_manager.validate_token_format("session_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"));
244        assert!(!session_manager.validate_token_format("invalid_token"));
245        assert!(!session_manager.validate_token_format("session_short"));
246        assert!(!session_manager.validate_token_format(""));
247    }
248
249    #[tokio::test]
250    async fn test_health_check_route() {
251        let auth = create_test_auth().await;
252        
253        let request = AuthRequest::new(HttpMethod::Get, "/health");
254        let response = auth.handle_request(request).await.expect("Health check failed");
255        
256        // Health check should return 404 since no plugin handles it
257        // In a real Axum integration, this would be handled by the router
258        assert_eq!(response.status, 404);
259    }
260
261    #[tokio::test]
262    async fn test_config_validation() {
263        // Test empty secret
264        let config = AuthConfig::new("");
265        assert!(config.validate().is_err());
266        
267        // Test short secret
268        let config = AuthConfig::new("short");
269        assert!(config.validate().is_err());
270        
271        // Test valid secret but no database
272        let config = AuthConfig::new("this-is-a-valid-32-character-secret-key");
273        assert!(config.validate().is_err());
274        
275        // Test valid config
276        let mut config = AuthConfig::new("this-is-a-valid-32-character-secret-key");
277        config.database = Some(Arc::new(MemoryDatabaseAdapter::new()));
278        assert!(config.validate().is_ok());
279    }
280}