Skip to main content

plexus_core/plexus/
test_validator.rs

1//! Test SessionValidator for E2E testing without Keycloak
2//!
3//! This validator accepts simple cookie formats for testing:
4//! - Simple: "session=<user_id>"
5//! - Advanced: "test_user=<user_id>|tenant=<tenant>|roles=<role1>,<role2>"
6
7use super::auth::{AuthContext, SessionValidator};
8use async_trait::async_trait;
9use serde_json::json;
10
11/// Test session validator that accepts simple cookie formats
12///
13/// This validator is intended for E2E testing without requiring Keycloak.
14/// It accepts two cookie formats:
15///
16/// 1. Simple format: `session=<user_id>`
17///    - Creates AuthContext with user_id and default tenant/roles
18///    - Example: `session=alice` → user_id="alice", tenant="test-tenant", roles=["user"]
19///
20/// 2. Advanced format: `test_user=<user_id>|tenant=<tenant>|roles=<role1>,<role2>`
21///    - Allows specifying tenant and roles for testing multi-tenancy and RBAC
22///    - Example: `test_user=bob|tenant=acme|roles=admin,user`
23///
24/// # Security
25///
26/// **WARNING**: This validator should NEVER be used in production. It accepts
27/// any user_id without verification. Use feature flags or environment variables
28/// to ensure it's only available in test/dev builds.
29pub struct TestSessionValidator;
30
31impl TestSessionValidator {
32    pub fn new() -> Self {
33        Self
34    }
35}
36
37impl Default for TestSessionValidator {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43#[async_trait]
44impl SessionValidator for TestSessionValidator {
45    async fn validate(&self, cookie: &str) -> Option<AuthContext> {
46        tracing::debug!("TestSessionValidator validating cookie: {}", cookie);
47
48        // Parse simple format: session=<user_id>
49        if let Some(user_id) = cookie.strip_prefix("session=") {
50            tracing::info!("Test auth: Simple format - user_id={}", user_id);
51            return Some(AuthContext {
52                user_id: user_id.to_string(),
53                session_id: format!("test-session-{}", user_id),
54                roles: vec!["user".to_string()],
55                metadata: json!({
56                    "tenant_id": "test-tenant",
57                    "email": format!("{}@test.com", user_id),
58                    "test_mode": true
59                }),
60            });
61        }
62
63        // Parse advanced format: test_user=<user_id>|tenant=<tenant>|roles=<role1>,<role2>
64        if let Some(params) = cookie.strip_prefix("test_user=") {
65            let parts: Vec<&str> = params.split('|').collect();
66            if parts.is_empty() {
67                tracing::warn!("Test auth: Invalid advanced format - no user_id");
68                return None;
69            }
70
71            let user_id = parts[0].to_string();
72            let mut tenant = "test-tenant".to_string();
73            let mut roles = vec!["user".to_string()];
74
75            // Parse additional parameters
76            for part in parts.iter().skip(1) {
77                if let Some((key, value)) = part.split_once('=') {
78                    match key {
79                        "tenant" => tenant = value.to_string(),
80                        "roles" => roles = value.split(',').map(|s| s.trim().to_string()).collect(),
81                        _ => tracing::warn!("Test auth: Unknown parameter: {}", key),
82                    }
83                }
84            }
85
86            tracing::info!(
87                "Test auth: Advanced format - user_id={}, tenant={}, roles={:?}",
88                user_id, tenant, roles
89            );
90
91            return Some(AuthContext {
92                user_id: user_id.clone(),
93                session_id: format!("test-session-{}", user_id),
94                roles,
95                metadata: json!({
96                    "tenant_id": tenant,
97                    "email": format!("{}@test.com", user_id),
98                    "test_mode": true
99                }),
100            });
101        }
102
103        // Cookie doesn't match any format
104        tracing::debug!("Test auth: Cookie format not recognized");
105        None
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[tokio::test]
114    async fn test_simple_format() {
115        let validator = TestSessionValidator::new();
116
117        let result = validator.validate("session=alice").await;
118        assert!(result.is_some());
119
120        let auth = result.unwrap();
121        assert_eq!(auth.user_id, "alice");
122        assert_eq!(auth.session_id, "test-session-alice");
123        assert!(auth.has_role("user"));
124        assert_eq!(auth.tenant(), Some("test-tenant".to_string()));
125        assert!(auth.is_authenticated());
126    }
127
128    #[tokio::test]
129    async fn test_advanced_format_with_tenant() {
130        let validator = TestSessionValidator::new();
131
132        let result = validator.validate("test_user=bob|tenant=acme").await;
133        assert!(result.is_some());
134
135        let auth = result.unwrap();
136        assert_eq!(auth.user_id, "bob");
137        assert_eq!(auth.tenant(), Some("acme".to_string()));
138        assert!(auth.has_role("user"));
139    }
140
141    #[tokio::test]
142    async fn test_advanced_format_with_roles() {
143        let validator = TestSessionValidator::new();
144
145        let result = validator.validate("test_user=charlie|roles=admin,editor,user").await;
146        assert!(result.is_some());
147
148        let auth = result.unwrap();
149        assert_eq!(auth.user_id, "charlie");
150        assert!(auth.has_role("admin"));
151        assert!(auth.has_role("editor"));
152        assert!(auth.has_role("user"));
153        assert!(!auth.has_role("superuser"));
154    }
155
156    #[tokio::test]
157    async fn test_advanced_format_complete() {
158        let validator = TestSessionValidator::new();
159
160        let result = validator.validate("test_user=dave|tenant=globex|roles=admin,user").await;
161        assert!(result.is_some());
162
163        let auth = result.unwrap();
164        assert_eq!(auth.user_id, "dave");
165        assert_eq!(auth.tenant(), Some("globex".to_string()));
166        assert!(auth.has_role("admin"));
167        assert!(auth.has_role("user"));
168    }
169
170    #[tokio::test]
171    async fn test_invalid_format() {
172        let validator = TestSessionValidator::new();
173
174        // Invalid/unknown format
175        let result = validator.validate("invalid-cookie").await;
176        assert!(result.is_none());
177
178        // Empty
179        let result = validator.validate("").await;
180        assert!(result.is_none());
181
182        // Garbage
183        let result = validator.validate("random=garbage").await;
184        assert!(result.is_none());
185    }
186
187    #[tokio::test]
188    async fn test_metadata_includes_test_mode() {
189        let validator = TestSessionValidator::new();
190
191        let result = validator.validate("session=testuser").await;
192        let auth = result.unwrap();
193
194        // test_mode is stored as boolean in metadata
195        assert_eq!(auth.metadata.get("test_mode").and_then(|v| v.as_bool()), Some(true));
196    }
197}