1use std::collections::HashMap;
4use std::env;
5use std::ffi::OsString;
6use std::sync::{Mutex, MutexGuard, OnceLock};
7
8pub struct TestEnvironment {
17 original_vars: HashMap<String, Option<OsString>>,
18 test_vars: HashMap<String, String>,
19}
20
21impl Default for TestEnvironment {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl TestEnvironment {
28 pub fn new() -> Self {
35 Self {
36 original_vars: HashMap::new(),
37 test_vars: HashMap::new(),
38 }
39 }
40
41 pub fn set_var(&mut self, key: &str, value: &str) {
49 if !self.original_vars.contains_key(key) {
51 self.original_vars.insert(key.to_string(), env::var_os(key));
52 }
53
54 self.test_vars.insert(key.to_string(), value.to_string());
56 unsafe {
59 env::set_var(key, value);
60 }
61 }
62
63 pub fn with_jwt_secret(mut self, secret: &str) -> Self {
70 self.set_var("JWT_SECRET", secret);
71 self
72 }
73
74 pub fn with_database_url(mut self, url: &str) -> Self {
81 self.set_var("DATABASE_URL", url);
82 self
83 }
84
85 pub fn with_redis_url(mut self, url: &str) -> Self {
92 self.set_var("REDIS_URL", url);
93 self
94 }
95}
96
97impl Drop for TestEnvironment {
98 fn drop(&mut self) {
100 for (key, original_value) in &self.original_vars {
101 unsafe {
104 match original_value {
105 Some(value) => env::set_var(key, value),
106 None => env::remove_var(key),
107 }
108 }
109 }
110 }
111}
112
113static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
116
117pub struct TestEnvironmentGuard {
129 _env: TestEnvironment,
131 _lock: MutexGuard<'static, ()>,
133}
134
135impl Default for TestEnvironmentGuard {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl TestEnvironmentGuard {
142 pub fn new() -> Self {
143 let _lock = ENV_LOCK
148 .get_or_init(|| Mutex::new(()))
149 .lock()
150 .unwrap_or_else(|poisoned| poisoned.into_inner());
153
154 let mut env = TestEnvironment::new();
155 env.set_var("RUST_TEST", "1");
159 Self { _env: env, _lock }
160 }
161
162 pub fn with_jwt_secret(mut self, secret: &str) -> Self {
163 self._env.set_var("JWT_SECRET", secret);
164 self
165 }
166
167 pub fn with_database_url(mut self, url: &str) -> Self {
168 self._env.set_var("DATABASE_URL", url);
169 self
170 }
171
172 pub fn with_redis_url(mut self, url: &str) -> Self {
173 self._env.set_var("REDIS_URL", url);
174 self
175 }
176
177 pub fn with_custom_var(mut self, key: &str, value: &str) -> Self {
178 self._env.set_var(key, value);
179 self
180 }
181}
182
183#[cfg(feature = "docker-tests")]
185pub mod containers {
186 use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner};
188
189 pub struct TestDatabase {
190 _container: ContainerAsync<GenericImage>,
191 connection_string: String,
192 }
193
194 impl TestDatabase {
195 pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
196 let postgres_image = GenericImage::new("postgres", "14")
197 .with_env_var("POSTGRES_DB", "auth_test")
198 .with_env_var("POSTGRES_USER", "test_user")
199 .with_env_var("POSTGRES_PASSWORD", "test_password");
200
201 let container = postgres_image.start().await?;
202 let port = container.get_host_port_ipv4(5432).await?;
203
204 let connection_string = format!(
205 "postgresql://test_user:test_password@localhost:{}/auth_test",
206 port
207 );
208
209 Ok(Self {
210 _container: container,
211 connection_string,
212 })
213 }
214
215 pub fn connection_string(&self) -> &str {
216 &self.connection_string
217 }
218 }
219
220 pub struct TestRedis {
221 _container: ContainerAsync<GenericImage>,
222 connection_string: String,
223 }
224
225 impl TestRedis {
226 pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
227 let redis_image = GenericImage::new("redis", "7-alpine");
228
229 let container = redis_image.start().await?;
230 let port = container.get_host_port_ipv4(6379).await?;
231
232 let connection_string = format!("redis://localhost:{}", port);
233
234 Ok(Self {
235 _container: container,
236 connection_string,
237 })
238 }
239
240 pub fn connection_string(&self) -> &str {
241 &self.connection_string
242 }
243 }
244
245 pub struct ContainerTestEnvironment {
247 pub database: TestDatabase,
248 pub redis: TestRedis,
249 pub env_guard: super::TestEnvironmentGuard,
250 }
251
252 impl ContainerTestEnvironment {
253 pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
254 let database = TestDatabase::new().await?;
255 let redis = TestRedis::new().await?;
256
257 let env_guard = super::TestEnvironmentGuard::new()
258 .with_jwt_secret("test-jwt-secret-for-container-tests")
259 .with_database_url(database.connection_string())
260 .with_redis_url(redis.connection_string());
261
262 Ok(Self {
263 database,
264 redis,
265 env_guard,
266 })
267 }
268 }
269}
270
271pub mod test_data {
273 use crate::storage::SessionData;
274 use crate::tokens::AuthToken;
275 use chrono::Utc;
276 use ring::rand::{SecureRandom, SystemRandom};
277
278 pub fn secure_test_token(user_id: &str) -> AuthToken {
286 let rng = SystemRandom::new();
287 let mut token_bytes = [0u8; 32];
288 rng.fill(&mut token_bytes)
289 .expect("Failed to generate secure random token");
290
291 let token_id = hex::encode(token_bytes);
292 let access_token = hex::encode(&token_bytes[..16]);
293
294 AuthToken {
295 token_id,
296 user_id: user_id.to_string(),
297 access_token,
298 token_type: Some("bearer".to_string()),
299 subject: Some(user_id.to_string()),
300 issuer: Some("auth-framework-test".to_string()),
301 refresh_token: None,
302 issued_at: Utc::now(),
303 expires_at: Utc::now() + chrono::Duration::seconds(3600),
304 scopes: vec!["read".to_string(), "write".to_string()].into(),
305 auth_method: "test".to_string(),
306 client_id: Some("test-client".to_string()),
307 user_profile: None,
308 permissions: vec!["read:all".to_string(), "write:all".to_string()].into(),
309 roles: vec!["test_user".to_string()].into(),
310 metadata: crate::tokens::TokenMetadata::default(),
311 }
312 }
313
314 pub fn secure_test_session(user_id: &str) -> SessionData {
322 let rng = SystemRandom::new();
323 let mut session_bytes = [0u8; 32];
324 rng.fill(&mut session_bytes)
325 .expect("Failed to generate secure random session");
326
327 let session_id = hex::encode(session_bytes);
328
329 SessionData {
330 session_id,
331 user_id: user_id.to_string(),
332 created_at: Utc::now(),
333 last_activity: Utc::now(),
334 expires_at: Utc::now() + chrono::Duration::seconds(7200),
335 ip_address: Some("127.0.0.1".to_string()),
336 user_agent: Some("Test Agent".to_string()),
337 data: std::collections::HashMap::new(),
338 }
339 }
340
341 pub fn secure_random_string(length: usize) -> String {
349 let rng = SystemRandom::new();
350 let mut bytes = vec![0u8; length];
351 rng.fill(&mut bytes)
352 .expect("Failed to generate secure random bytes");
353 hex::encode(bytes)
354 }
355}
356
357#[macro_export]
359macro_rules! test_with_env {
360 ($test_name:ident, $jwt_secret:expr, $body:block) => {
361 #[tokio::test]
362 async fn $test_name() {
363 let _env = $crate::test_infrastructure::TestEnvironmentGuard::new()
364 .with_jwt_secret($jwt_secret);
365 $body
366 }
367 };
368}
369
370#[macro_export]
371macro_rules! test_with_containers {
372 ($test_name:ident, $body:block) => {
373 #[cfg(feature = "docker-tests")]
374 #[tokio::test]
375 async fn $test_name() {
376 let _test_env =
377 $crate::test_infrastructure::containers::ContainerTestEnvironment::new()
378 .expect("Failed to setup container test environment");
379 $body
380 }
381 };
382}
383
384pub fn with_global_lock<F, R>(f: F) -> R
389where
390 F: FnOnce() -> R,
391{
392 let _guard = ENV_LOCK
393 .get_or_init(|| Mutex::new(()))
394 .lock()
395 .unwrap_or_else(|poisoned| poisoned.into_inner());
396 f()
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_environment_isolation() {
405 let original_value = env::var("TEST_VAR").ok();
407
408 {
409 let mut test_env = TestEnvironment::new();
410 test_env.set_var("TEST_VAR", "test_value");
411 assert_eq!(env::var("TEST_VAR").unwrap(), "test_value");
412 }
413
414 assert_eq!(env::var("TEST_VAR").ok(), original_value);
416 }
417
418 #[test]
419 fn test_environment_guard() {
420 const TEST_KEY: &str = "AUTH_FW_GUARD_ISOLATION_TEST_ONLY";
423
424 unsafe { env::remove_var(TEST_KEY) };
427 assert!(
428 env::var(TEST_KEY).is_err(),
429 "TEST_KEY should not exist before test"
430 );
431
432 {
433 let _guard = TestEnvironmentGuard::new().with_custom_var(TEST_KEY, "isolated-value");
434 assert_eq!(env::var(TEST_KEY).unwrap(), "isolated-value");
435 }
436
437 assert!(
439 env::var(TEST_KEY).is_err(),
440 "TEST_KEY should be absent after guard is dropped"
441 );
442 }
443
444 #[test]
445 fn test_secure_test_data_generation() {
446 let token1 = test_data::secure_test_token("user1");
447 let token2 = test_data::secure_test_token("user1");
448
449 assert_ne!(token1.token_id, token2.token_id);
451
452 assert_eq!(token1.user_id, token2.user_id);
454 }
455
456 #[test]
457 fn test_secure_random_generation() {
458 let str1 = test_data::secure_random_string(32);
459 let str2 = test_data::secure_random_string(32);
460
461 assert_ne!(str1, str2);
462 assert_eq!(str1.len(), 64); assert_eq!(str2.len(), 64);
464 }
465}