1use std::collections::HashMap;
4use std::env;
5use std::ffi::OsString;
6use std::sync::{Arc, Mutex, OnceLock};
7
8pub 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 pub fn new() -> Self {
24 Self {
25 original_vars: HashMap::new(),
26 test_vars: HashMap::new(),
27 }
28 }
29
30 pub fn set_var(&mut self, key: &str, value: &str) {
32 if !self.original_vars.contains_key(key) {
34 self.original_vars.insert(key.to_string(), env::var_os(key));
35 }
36
37 self.test_vars.insert(key.to_string(), value.to_string());
39 unsafe {
40 env::set_var(key, value);
41 }
42 }
43
44 pub fn with_jwt_secret(mut self, secret: &str) -> Self {
46 self.set_var("JWT_SECRET", secret);
47 self
48 }
49
50 pub fn with_database_url(mut self, url: &str) -> Self {
52 self.set_var("DATABASE_URL", url);
53 self
54 }
55
56 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 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
77pub 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#[cfg(feature = "docker-tests")]
119pub mod containers {
120 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 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
205pub 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 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 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 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#[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
300static TEST_MUTEX: OnceLock<Arc<Mutex<()>>> = OnceLock::new();
302
303pub 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 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 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 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 assert_ne!(token1.token_id, token2.token_id);
352
353 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); assert_eq!(str2.len(), 64);
365 }
366}
367
368