elif_testing/
auth.rs

1//! Authentication testing utilities
2//!
3//! Provides utilities for testing authentication and authorization
4//! in elif.rs applications, including JWT token generation,
5//! session management, and RBAC testing helpers.
6
7use std::collections::HashMap;
8use serde_json::{Value as JsonValue, json};
9use uuid::Uuid;
10use chrono::{DateTime, Utc, Duration};
11use crate::{TestError, TestResult, factories::{User, UserFactory, Factory}};
12
13/// Test authentication provider for generating test tokens and sessions
14pub struct TestAuthProvider {
15    jwt_secret: String,
16    session_store: HashMap<String, TestSession>,
17}
18
19impl TestAuthProvider {
20    /// Create a new test auth provider
21    pub fn new() -> Self {
22        Self {
23            jwt_secret: "test_jwt_secret_key_for_testing_only".to_string(),
24            session_store: HashMap::new(),
25        }
26    }
27    
28    /// Create with custom JWT secret
29    pub fn with_jwt_secret(secret: impl Into<String>) -> Self {
30        Self {
31            jwt_secret: secret.into(),
32            session_store: HashMap::new(),
33        }
34    }
35    
36    /// Generate a test JWT token for a user
37    pub fn generate_jwt_token(&self, user: &User) -> TestResult<String> {
38        let claims = TestJwtClaims {
39            sub: user.id.to_string(),
40            name: user.name.clone(),
41            email: user.email.clone(),
42            roles: vec!["user".to_string()], // Default role
43            permissions: vec![],
44            exp: (Utc::now() + Duration::hours(1)).timestamp() as usize,
45            iat: Utc::now().timestamp() as usize,
46        };
47        
48        // In a real implementation, this would use a JWT library
49        // For testing purposes, we'll create a mock token
50        let token = format!(
51            "test_jwt_token_{}_{}", 
52            user.id.to_string().replace('-', ""), 
53            claims.exp
54        );
55        
56        Ok(token)
57    }
58    
59    /// Generate a test JWT token with custom claims
60    pub fn generate_jwt_with_claims(&self, claims: TestJwtClaims) -> TestResult<String> {
61        let token = format!(
62            "test_jwt_token_{}_{}", 
63            claims.sub.replace('-', ""), 
64            claims.exp
65        );
66        Ok(token)
67    }
68    
69    /// Generate an admin JWT token
70    pub fn generate_admin_token(&self, user: &User) -> TestResult<String> {
71        let claims = TestJwtClaims {
72            sub: user.id.to_string(),
73            name: user.name.clone(),
74            email: user.email.clone(),
75            roles: vec!["admin".to_string()],
76            permissions: vec![
77                "users.create".to_string(),
78                "users.read".to_string(),
79                "users.update".to_string(),
80                "users.delete".to_string(),
81            ],
82            exp: (Utc::now() + Duration::hours(1)).timestamp() as usize,
83            iat: Utc::now().timestamp() as usize,
84        };
85        
86        self.generate_jwt_with_claims(claims)
87    }
88    
89    /// Create a test session
90    pub fn create_session(&mut self, user: &User) -> TestResult<String> {
91        let session_id = Uuid::new_v4().to_string();
92        let session = TestSession {
93            id: session_id.clone(),
94            user_id: user.id,
95            user_name: user.name.clone(),
96            user_email: user.email.clone(),
97            roles: vec!["user".to_string()],
98            permissions: vec![],
99            created_at: Utc::now(),
100            expires_at: Utc::now() + Duration::hours(2),
101            data: HashMap::new(),
102        };
103        
104        self.session_store.insert(session_id.clone(), session);
105        Ok(session_id)
106    }
107    
108    /// Create an admin session
109    pub fn create_admin_session(&mut self, user: &User) -> TestResult<String> {
110        let session_id = Uuid::new_v4().to_string();
111        let session = TestSession {
112            id: session_id.clone(),
113            user_id: user.id,
114            user_name: user.name.clone(),
115            user_email: user.email.clone(),
116            roles: vec!["admin".to_string()],
117            permissions: vec![
118                "users.create".to_string(),
119                "users.read".to_string(),
120                "users.update".to_string(),
121                "users.delete".to_string(),
122            ],
123            created_at: Utc::now(),
124            expires_at: Utc::now() + Duration::hours(2),
125            data: HashMap::new(),
126        };
127        
128        self.session_store.insert(session_id.clone(), session);
129        Ok(session_id)
130    }
131    
132    /// Get session by ID
133    pub fn get_session(&self, session_id: &str) -> Option<&TestSession> {
134        self.session_store.get(session_id)
135    }
136    
137    /// Validate JWT token (mock implementation)
138    pub fn validate_jwt_token(&self, token: &str) -> TestResult<TestJwtClaims> {
139        // Mock validation - in real implementation would decode and verify JWT
140        if !token.starts_with("test_jwt_token_") {
141            return Err(TestError::Authentication("Invalid token format".to_string()));
142        }
143        
144        let parts: Vec<&str> = token.split('_').collect();
145        if parts.len() < 4 {
146            return Err(TestError::Authentication("Invalid token structure".to_string()));
147        }
148        
149        // Extract user ID and expiration from token
150        let user_id = parts[3];
151        let exp_str = parts.get(4).copied().unwrap_or("0");
152        let exp = exp_str.parse::<usize>().unwrap_or(0);
153        
154        if exp < Utc::now().timestamp() as usize {
155            return Err(TestError::Authentication("Token expired".to_string()));
156        }
157        
158        Ok(TestJwtClaims {
159            sub: format!("{}-{}-{}-{}", &user_id[0..8], &user_id[8..12], &user_id[12..16], &user_id[16..20]),
160            name: "Test User".to_string(),
161            email: "test@example.com".to_string(),
162            roles: vec!["user".to_string()],
163            permissions: vec![],
164            exp,
165            iat: Utc::now().timestamp() as usize,
166        })
167    }
168}
169
170impl Default for TestAuthProvider {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176/// Test JWT claims structure
177#[derive(Debug, Clone)]
178pub struct TestJwtClaims {
179    pub sub: String,        // Subject (user ID)
180    pub name: String,       // User name
181    pub email: String,      // User email
182    pub roles: Vec<String>, // User roles
183    pub permissions: Vec<String>, // User permissions
184    pub exp: usize,         // Expiration time
185    pub iat: usize,         // Issued at time
186}
187
188impl TestJwtClaims {
189    /// Create claims for a regular user
190    pub fn user(user_id: impl Into<String>) -> Self {
191        Self {
192            sub: user_id.into(),
193            name: "Test User".to_string(),
194            email: "test@example.com".to_string(),
195            roles: vec!["user".to_string()],
196            permissions: vec![],
197            exp: (Utc::now() + Duration::hours(1)).timestamp() as usize,
198            iat: Utc::now().timestamp() as usize,
199        }
200    }
201    
202    /// Create claims for an admin user
203    pub fn admin(user_id: impl Into<String>) -> Self {
204        Self {
205            sub: user_id.into(),
206            name: "Admin User".to_string(),
207            email: "admin@example.com".to_string(),
208            roles: vec!["admin".to_string()],
209            permissions: vec![
210                "users.create".to_string(),
211                "users.read".to_string(),
212                "users.update".to_string(),
213                "users.delete".to_string(),
214            ],
215            exp: (Utc::now() + Duration::hours(1)).timestamp() as usize,
216            iat: Utc::now().timestamp() as usize,
217        }
218    }
219    
220    /// Add a role to the claims
221    pub fn with_role(mut self, role: impl Into<String>) -> Self {
222        self.roles.push(role.into());
223        self
224    }
225    
226    /// Add multiple roles
227    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
228        self.roles.extend(roles);
229        self
230    }
231    
232    /// Add a permission to the claims
233    pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
234        self.permissions.push(permission.into());
235        self
236    }
237    
238    /// Add multiple permissions
239    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
240        self.permissions.extend(permissions);
241        self
242    }
243    
244    /// Set custom expiration time
245    pub fn expires_in(mut self, duration: Duration) -> Self {
246        self.exp = (Utc::now() + duration).timestamp() as usize;
247        self
248    }
249}
250
251/// Test session structure
252#[derive(Debug, Clone)]
253pub struct TestSession {
254    pub id: String,
255    pub user_id: Uuid,
256    pub user_name: String,
257    pub user_email: String,
258    pub roles: Vec<String>,
259    pub permissions: Vec<String>,
260    pub created_at: DateTime<Utc>,
261    pub expires_at: DateTime<Utc>,
262    pub data: HashMap<String, JsonValue>,
263}
264
265impl TestSession {
266    /// Check if session is expired
267    pub fn is_expired(&self) -> bool {
268        Utc::now() > self.expires_at
269    }
270    
271    /// Check if user has a specific role
272    pub fn has_role(&self, role: &str) -> bool {
273        self.roles.contains(&role.to_string())
274    }
275    
276    /// Check if user has a specific permission
277    pub fn has_permission(&self, permission: &str) -> bool {
278        self.permissions.contains(&permission.to_string())
279    }
280    
281    /// Set session data
282    pub fn set_data(&mut self, key: impl Into<String>, value: JsonValue) {
283        self.data.insert(key.into(), value);
284    }
285    
286    /// Get session data
287    pub fn get_data(&self, key: &str) -> Option<&JsonValue> {
288        self.data.get(key)
289    }
290}
291
292/// Test user builder for authentication testing
293pub struct TestUserBuilder {
294    user: User,
295    roles: Vec<String>,
296    permissions: Vec<String>,
297}
298
299impl TestUserBuilder {
300    /// Create a new test user builder
301    pub fn new() -> TestResult<Self> {
302        let user = UserFactory::new().build()?;
303        Ok(Self {
304            user,
305            roles: vec!["user".to_string()],
306            permissions: vec![],
307        })
308    }
309    
310    /// Create an admin user
311    pub fn admin() -> TestResult<Self> {
312        let user = UserFactory::new().build()?;
313        Ok(Self {
314            user,
315            roles: vec!["admin".to_string()],
316            permissions: vec![
317                "users.create".to_string(),
318                "users.read".to_string(),
319                "users.update".to_string(),
320                "users.delete".to_string(),
321            ],
322        })
323    }
324    
325    /// Set user name
326    pub fn with_name(mut self, name: impl Into<String>) -> Self {
327        self.user.name = name.into();
328        self
329    }
330    
331    /// Set user email
332    pub fn with_email(mut self, email: impl Into<String>) -> Self {
333        self.user.email = email.into();
334        self
335    }
336    
337    /// Add a role
338    pub fn with_role(mut self, role: impl Into<String>) -> Self {
339        self.roles.push(role.into());
340        self
341    }
342    
343    /// Add multiple roles
344    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
345        self.roles.extend(roles);
346        self
347    }
348    
349    /// Add a permission
350    pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
351        self.permissions.push(permission.into());
352        self
353    }
354    
355    /// Add multiple permissions
356    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
357        self.permissions.extend(permissions);
358        self
359    }
360    
361    /// Build the user
362    pub fn build(self) -> (User, Vec<String>, Vec<String>) {
363        (self.user, self.roles, self.permissions)
364    }
365    
366    /// Generate JWT token for this user
367    pub fn generate_jwt_token(self) -> TestResult<(User, String)> {
368        let auth_provider = TestAuthProvider::new();
369        let (user, roles, permissions) = self.build();
370        
371        let claims = TestJwtClaims {
372            sub: user.id.to_string(),
373            name: user.name.clone(),
374            email: user.email.clone(),
375            roles,
376            permissions,
377            exp: (Utc::now() + Duration::hours(1)).timestamp() as usize,
378            iat: Utc::now().timestamp() as usize,
379        };
380        
381        let token = auth_provider.generate_jwt_with_claims(claims)?;
382        Ok((user, token))
383    }
384}
385
386impl Default for TestUserBuilder {
387    fn default() -> Self {
388        Self::new().expect("Failed to create default test user")
389    }
390}
391
392/// Authorization test helpers
393pub struct AuthTestHelpers;
394
395impl AuthTestHelpers {
396    /// Create a test user with JWT token
397    pub fn user_with_jwt() -> TestResult<(User, String)> {
398        TestUserBuilder::new()?.generate_jwt_token()
399    }
400    
401    /// Create an admin user with JWT token
402    pub fn admin_with_jwt() -> TestResult<(User, String)> {
403        TestUserBuilder::admin()?.generate_jwt_token()
404    }
405    
406    /// Create a user with specific roles and JWT token
407    pub fn user_with_roles_and_jwt(roles: Vec<String>) -> TestResult<(User, String)> {
408        TestUserBuilder::new()?.with_roles(roles).generate_jwt_token()
409    }
410    
411    /// Create a user with specific permissions and JWT token
412    pub fn user_with_permissions_and_jwt(permissions: Vec<String>) -> TestResult<(User, String)> {
413        TestUserBuilder::new()?.with_permissions(permissions).generate_jwt_token()
414    }
415    
416    /// Validate that a token has specific roles
417    pub fn assert_token_has_roles(token: &str, expected_roles: &[String]) -> TestResult<()> {
418        let auth_provider = TestAuthProvider::new();
419        let claims = auth_provider.validate_jwt_token(token)?;
420        
421        for role in expected_roles {
422            if !claims.roles.contains(role) {
423                return Err(TestError::Assertion {
424                    message: format!("Token does not contain required role: {}", role),
425                });
426            }
427        }
428        
429        Ok(())
430    }
431    
432    /// Validate that a token has specific permissions
433    pub fn assert_token_has_permissions(token: &str, expected_permissions: &[String]) -> TestResult<()> {
434        let auth_provider = TestAuthProvider::new();
435        let claims = auth_provider.validate_jwt_token(token)?;
436        
437        for permission in expected_permissions {
438            if !claims.permissions.contains(permission) {
439                return Err(TestError::Assertion {
440                    message: format!("Token does not contain required permission: {}", permission),
441                });
442            }
443        }
444        
445        Ok(())
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    
453    #[test]
454    fn test_jwt_claims_creation() {
455        let claims = TestJwtClaims::user("user123");
456        assert_eq!(claims.sub, "user123");
457        assert!(claims.roles.contains(&"user".to_string()));
458        assert!(claims.exp > Utc::now().timestamp() as usize);
459        
460        let admin_claims = TestJwtClaims::admin("admin123");
461        assert_eq!(admin_claims.sub, "admin123");
462        assert!(admin_claims.roles.contains(&"admin".to_string()));
463        assert!(!admin_claims.permissions.is_empty());
464    }
465    
466    #[test]
467    fn test_jwt_claims_modification() {
468        let claims = TestJwtClaims::user("user123")
469            .with_role("moderator")
470            .with_permission("posts.delete");
471            
472        assert!(claims.roles.contains(&"moderator".to_string()));
473        assert!(claims.permissions.contains(&"posts.delete".to_string()));
474    }
475    
476    #[test]
477    fn test_test_auth_provider() -> TestResult<()> {
478        let provider = TestAuthProvider::new();
479        let user = UserFactory::new().build()?;
480        
481        let token = provider.generate_jwt_token(&user)?;
482        assert!(token.starts_with("test_jwt_token_"));
483        
484        let claims = provider.validate_jwt_token(&token)?;
485        assert!(!claims.sub.is_empty());
486        
487        Ok(())
488    }
489    
490    #[test]
491    fn test_session_functionality() {
492        let mut provider = TestAuthProvider::new();
493        let user = UserFactory::new().build().unwrap();
494        
495        let session_id = provider.create_session(&user).unwrap();
496        let session = provider.get_session(&session_id).unwrap();
497        
498        assert_eq!(session.user_id, user.id);
499        assert!(session.has_role("user"));
500        assert!(!session.is_expired());
501    }
502    
503    #[test]
504    fn test_user_builder() -> TestResult<()> {
505        let builder = TestUserBuilder::new()?;
506        let (user, roles, permissions) = builder
507            .with_name("John Doe")
508            .with_role("moderator")
509            .with_permission("posts.create")
510            .build();
511            
512        assert_eq!(user.name, "John Doe");
513        assert!(roles.contains(&"moderator".to_string()));
514        assert!(permissions.contains(&"posts.create".to_string()));
515        
516        Ok(())
517    }
518    
519    #[test]
520    fn test_auth_helpers() -> TestResult<()> {
521        let (_user, token) = AuthTestHelpers::user_with_jwt()?;
522        assert!(!token.is_empty());
523        
524        let (_admin, admin_token) = AuthTestHelpers::admin_with_jwt()?;
525        assert!(!admin_token.is_empty());
526        
527        Ok(())
528    }
529}