auth_framework/testing/
test_infrastructure.rs

1// test_infrastructure.rs - Bulletproof test isolation and container infrastructure
2
3use std::collections::HashMap;
4use std::env;
5use std::ffi::OsString;
6use std::sync::{Arc, Mutex, OnceLock};
7
8/// Environment variable isolation for tests
9/// This ensures tests cannot interfere with each other or the host system
10pub struct TestEnvironment {
11    original_vars: HashMap<String, Option<OsString>>,
12    test_vars: HashMap<String, String>,
13}
14
15impl Default for TestEnvironment {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl TestEnvironment {
22    /// Create a new isolated test environment
23    pub fn new() -> Self {
24        Self {
25            original_vars: HashMap::new(),
26            test_vars: HashMap::new(),
27        }
28    }
29
30    /// Set an environment variable for this test only
31    pub fn set_var(&mut self, key: &str, value: &str) {
32        // Store original value for restoration
33        if !self.original_vars.contains_key(key) {
34            self.original_vars.insert(key.to_string(), env::var_os(key));
35        }
36
37        // Set the test value
38        self.test_vars.insert(key.to_string(), value.to_string());
39        unsafe {
40            env::set_var(key, value);
41        }
42    }
43
44    /// Set the standard JWT_SECRET for tests
45    pub fn with_jwt_secret(mut self, secret: &str) -> Self {
46        self.set_var("JWT_SECRET", secret);
47        self
48    }
49
50    /// Set database URL for integration tests
51    pub fn with_database_url(mut self, url: &str) -> Self {
52        self.set_var("DATABASE_URL", url);
53        self
54    }
55
56    /// Set Redis URL for session tests
57    pub fn with_redis_url(mut self, url: &str) -> Self {
58        self.set_var("REDIS_URL", url);
59        self
60    }
61}
62
63impl Drop for TestEnvironment {
64    /// Restore all environment variables to their original state
65    fn drop(&mut self) {
66        for (key, original_value) in &self.original_vars {
67            unsafe {
68                match original_value {
69                    Some(value) => env::set_var(key, value),
70                    None => env::remove_var(key),
71                }
72            }
73        }
74    }
75}
76
77/// RAII guard for test environment isolation
78/// Usage: let _env = TestEnvironmentGuard::new().with_jwt_secret("test-secret");
79pub struct TestEnvironmentGuard {
80    _env: TestEnvironment,
81}
82
83impl Default for TestEnvironmentGuard {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl TestEnvironmentGuard {
90    pub fn new() -> Self {
91        Self {
92            _env: TestEnvironment::new(),
93        }
94    }
95
96    pub fn with_jwt_secret(mut self, secret: &str) -> Self {
97        self._env.set_var("JWT_SECRET", secret);
98        self
99    }
100
101    pub fn with_database_url(mut self, url: &str) -> Self {
102        self._env.set_var("DATABASE_URL", url);
103        self
104    }
105
106    pub fn with_redis_url(mut self, url: &str) -> Self {
107        self._env.set_var("REDIS_URL", url);
108        self
109    }
110
111    pub fn with_custom_var(mut self, key: &str, value: &str) -> Self {
112        self._env.set_var(key, value);
113        self
114    }
115}
116
117/// Container-based test infrastructure for complex integration tests
118#[cfg(feature = "docker-tests")]
119pub mod containers {
120    // use std::collections::HashMap; // Currently unused
121    use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner};
122
123    pub struct TestDatabase {
124        _container: ContainerAsync<GenericImage>,
125        connection_string: String,
126    }
127
128    impl TestDatabase {
129        pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
130            let postgres_image = GenericImage::new("postgres", "14")
131                .with_env_var("POSTGRES_DB", "auth_test")
132                .with_env_var("POSTGRES_USER", "test_user")
133                .with_env_var("POSTGRES_PASSWORD", "test_password");
134
135            let container = postgres_image.start().await?;
136            let port = container.get_host_port_ipv4(5432).await?;
137
138            let connection_string = format!(
139                "postgresql://test_user:test_password@localhost:{}/auth_test",
140                port
141            );
142
143            Ok(Self {
144                _container: container,
145                connection_string,
146            })
147        }
148
149        pub fn connection_string(&self) -> &str {
150            &self.connection_string
151        }
152    }
153
154    pub struct TestRedis {
155        _container: ContainerAsync<GenericImage>,
156        connection_string: String,
157    }
158
159    impl TestRedis {
160        pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
161            let redis_image = GenericImage::new("redis", "7-alpine");
162
163            let container = redis_image.start().await?;
164            let port = container.get_host_port_ipv4(6379).await?;
165
166            let connection_string = format!("redis://localhost:{}", port);
167
168            Ok(Self {
169                _container: container,
170                connection_string,
171            })
172        }
173
174        pub fn connection_string(&self) -> &str {
175            &self.connection_string
176        }
177    }
178
179    /// Complete isolated test environment with containers
180    pub struct ContainerTestEnvironment {
181        pub database: TestDatabase,
182        pub redis: TestRedis,
183        pub env_guard: super::TestEnvironmentGuard,
184    }
185
186    impl ContainerTestEnvironment {
187        pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
188            let database = TestDatabase::new().await?;
189            let redis = TestRedis::new().await?;
190
191            let env_guard = super::TestEnvironmentGuard::new()
192                .with_jwt_secret("test-jwt-secret-for-container-tests")
193                .with_database_url(database.connection_string())
194                .with_redis_url(redis.connection_string());
195
196            Ok(Self {
197                database,
198                redis,
199                env_guard,
200            })
201        }
202    }
203}
204
205/// Test utilities for creating secure test data
206pub mod test_data {
207    use crate::storage::SessionData;
208    use crate::tokens::AuthToken;
209    use chrono::Utc;
210    use ring::rand::{SecureRandom, SystemRandom};
211
212    /// Generate cryptographically secure test data
213    pub fn secure_test_token(user_id: &str) -> AuthToken {
214        let rng = SystemRandom::new();
215        let mut token_bytes = [0u8; 32];
216        rng.fill(&mut token_bytes)
217            .expect("Failed to generate secure random token");
218
219        let token_id = hex::encode(token_bytes);
220        let access_token = hex::encode(&token_bytes[..16]);
221
222        AuthToken {
223            token_id,
224            user_id: user_id.to_string(),
225            access_token,
226            token_type: Some("bearer".to_string()),
227            subject: Some(user_id.to_string()),
228            issuer: Some("auth-framework-test".to_string()),
229            refresh_token: None,
230            issued_at: Utc::now(),
231            expires_at: Utc::now() + chrono::Duration::seconds(3600),
232            scopes: vec!["read".to_string(), "write".to_string()],
233            auth_method: "test".to_string(),
234            client_id: Some("test-client".to_string()),
235            user_profile: None,
236            permissions: vec!["read:all".to_string(), "write:all".to_string()],
237            roles: vec!["test_user".to_string()],
238            metadata: crate::tokens::TokenMetadata::default(),
239        }
240    }
241
242    /// Generate secure test session
243    pub fn secure_test_session(user_id: &str) -> SessionData {
244        let rng = SystemRandom::new();
245        let mut session_bytes = [0u8; 32];
246        rng.fill(&mut session_bytes)
247            .expect("Failed to generate secure random session");
248
249        let session_id = hex::encode(session_bytes);
250
251        SessionData {
252            session_id,
253            user_id: user_id.to_string(),
254            created_at: Utc::now(),
255            last_activity: Utc::now(),
256            expires_at: Utc::now() + chrono::Duration::seconds(7200),
257            ip_address: Some("127.0.0.1".to_string()),
258            user_agent: Some("Test Agent".to_string()),
259            data: std::collections::HashMap::new(),
260        }
261    }
262
263    /// Generate secure random string for testing
264    pub fn secure_random_string(length: usize) -> String {
265        let rng = SystemRandom::new();
266        let mut bytes = vec![0u8; length];
267        rng.fill(&mut bytes)
268            .expect("Failed to generate secure random bytes");
269        hex::encode(bytes)
270    }
271}
272
273/// Macros for simplified test environment setup
274#[macro_export]
275macro_rules! test_with_env {
276    ($test_name:ident, $jwt_secret:expr, $body:block) => {
277        #[tokio::test]
278        async fn $test_name() {
279            let _env = $crate::test_infrastructure::TestEnvironmentGuard::new()
280                .with_jwt_secret($jwt_secret);
281            $body
282        }
283    };
284}
285
286#[macro_export]
287macro_rules! test_with_containers {
288    ($test_name:ident, $body:block) => {
289        #[cfg(feature = "docker-tests")]
290        #[tokio::test]
291        async fn $test_name() {
292            let _test_env =
293                $crate::test_infrastructure::containers::ContainerTestEnvironment::new()
294                    .expect("Failed to setup container test environment");
295            $body
296        }
297    };
298}
299
300/// Thread-safe test execution coordination
301static TEST_MUTEX: OnceLock<Arc<Mutex<()>>> = OnceLock::new();
302
303/// Ensure only one test that modifies global state runs at a time
304pub fn with_global_lock<F, R>(f: F) -> R
305where
306    F: FnOnce() -> R,
307{
308    let mutex = TEST_MUTEX.get_or_init(|| Arc::new(Mutex::new(())));
309    let _guard = mutex.lock().unwrap();
310    f()
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_environment_isolation() {
319        // Ensure environment variables are properly isolated
320        let original_value = env::var("TEST_VAR").ok();
321
322        {
323            let mut test_env = TestEnvironment::new();
324            test_env.set_var("TEST_VAR", "test_value");
325            assert_eq!(env::var("TEST_VAR").unwrap(), "test_value");
326        }
327
328        // Variable should be restored
329        assert_eq!(env::var("TEST_VAR").ok(), original_value);
330    }
331
332    #[test]
333    fn test_environment_guard() {
334        let original_value = env::var("JWT_SECRET").ok();
335
336        {
337            let _guard = TestEnvironmentGuard::new().with_jwt_secret("isolated-test-secret");
338            assert_eq!(env::var("JWT_SECRET").unwrap(), "isolated-test-secret");
339        }
340
341        // Should be restored automatically
342        assert_eq!(env::var("JWT_SECRET").ok(), original_value);
343    }
344
345    #[test]
346    fn test_secure_test_data_generation() {
347        let token1 = test_data::secure_test_token("user1");
348        let token2 = test_data::secure_test_token("user1");
349
350        // Tokens should be different even for same user
351        assert_ne!(token1.token_id, token2.token_id);
352
353        // But should have same user_id
354        assert_eq!(token1.user_id, token2.user_id);
355    }
356
357    #[test]
358    fn test_secure_random_generation() {
359        let str1 = test_data::secure_random_string(32);
360        let str2 = test_data::secure_random_string(32);
361
362        assert_ne!(str1, str2);
363        assert_eq!(str1.len(), 64); // hex encoding doubles the length
364        assert_eq!(str2.len(), 64);
365    }
366}
367
368