use crate::security::patterns::SENSITIVE_DETECTION_PATTERNS;
use crate::security::{EnvSecurityError, EnvSecurityValidator};
use regex::Regex;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
const DEFAULT_RATE_LIMIT_MAX_REQUESTS: usize = 100;
const DEFAULT_RATE_LIMIT_WINDOW_SECONDS: u64 = 60;
const ENV_RATE_LIMIT_MAX_REQUESTS: &str = "CONFERS_RATE_LIMIT_MAX_REQUESTS";
const ENV_RATE_LIMIT_WINDOW_SECONDS: &str = "CONFERS_RATE_LIMIT_WINDOW_SECONDS";
fn get_rate_limit_max_requests() -> usize {
std::env::var(ENV_RATE_LIMIT_MAX_REQUESTS)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_RATE_LIMIT_MAX_REQUESTS)
}
fn get_rate_limit_window_seconds() -> u64 {
std::env::var(ENV_RATE_LIMIT_WINDOW_SECONDS)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_RATE_LIMIT_WINDOW_SECONDS)
}
#[derive(Debug, Clone)]
pub(crate) struct InjectionRateLimiter {
window_counter: Arc<AtomicU64>,
window_start: Arc<AtomicU64>,
max_requests: usize,
window_seconds: u64,
rate_limiting_enabled: bool,
}
impl Default for InjectionRateLimiter {
fn default() -> Self {
Self::new()
}
}
impl InjectionRateLimiter {
pub fn new() -> Self {
Self {
window_counter: Arc::new(AtomicU64::new(0)),
window_start: Arc::new(AtomicU64::new(0)),
max_requests: get_rate_limit_max_requests(),
window_seconds: get_rate_limit_window_seconds(),
rate_limiting_enabled: true,
}
}
#[allow(dead_code)]
pub fn disabled() -> Self {
Self {
window_counter: Arc::new(AtomicU64::new(0)),
window_start: Arc::new(AtomicU64::new(0)),
max_requests: usize::MAX,
window_seconds: u64::MAX,
rate_limiting_enabled: false,
}
}
#[allow(dead_code)]
pub fn with_limits(max_requests: usize, window_seconds: u64) -> Self {
Self {
window_counter: Arc::new(AtomicU64::new(0)),
window_start: Arc::new(AtomicU64::new(0)),
max_requests,
window_seconds,
rate_limiting_enabled: true,
}
}
pub fn check_rate_limit(&self) -> Result<(), u64> {
if !self.rate_limiting_enabled {
return Ok(());
}
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
for _ in 0..8 {
let window_start = self.window_start.load(Ordering::SeqCst);
if window_start == 0 {
if self
.window_start
.compare_exchange(0, now_secs, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
return Ok(());
}
continue;
}
if now_secs - window_start < self.window_seconds {
let current = self.window_counter.fetch_add(1, Ordering::SeqCst);
if current as usize >= self.max_requests {
self.window_counter.fetch_sub(1, Ordering::SeqCst);
let retry_after = self.window_seconds - (now_secs - window_start);
return Err(retry_after);
}
return Ok(());
}
if self
.window_start
.compare_exchange(window_start, now_secs, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
self.window_counter.store(1, Ordering::SeqCst);
return Ok(());
}
}
Err(self.window_seconds)
}
#[allow(dead_code)]
pub fn usage_stats(&self) -> (usize, u64, f64) {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let window_start = self.window_start.load(Ordering::SeqCst);
let counter = self.window_counter.load(Ordering::SeqCst);
let elapsed = now_secs.saturating_sub(window_start);
let remaining = self.window_seconds.saturating_sub(elapsed);
let usage_percent = (counter as f64 / self.max_requests as f64) * 100.0;
(counter as usize, remaining, usage_percent)
}
}
#[allow(dead_code)]
pub(crate) static GLOBAL_RATE_LIMITER: OnceLock<InjectionRateLimiter> = OnceLock::new();
#[cfg(test)]
pub(crate) static TEST_RATE_LIMITER: OnceLock<InjectionRateLimiter> = OnceLock::new();
#[allow(dead_code)]
pub(crate) static GLOBAL_INJECTOR: OnceLock<Arc<RwLock<ConfigInjector>>> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct ConfigInjector {
values: Arc<RwLock<HashMap<String, String>>>,
validator: EnvSecurityValidator,
sensitive_patterns: Vec<Regex>,
injection_history: Arc<RwLock<Vec<InjectionRecord>>>,
}
impl Default for ConfigInjector {
fn default() -> Self {
Self::new()
}
}
impl ConfigInjector {
pub fn new() -> Self {
Self::with_validator(EnvSecurityValidator::default())
}
pub fn with_validator(validator: EnvSecurityValidator) -> Self {
Self {
values: Arc::new(RwLock::new(HashMap::new())),
validator,
sensitive_patterns: Self::default_sensitive_patterns(),
injection_history: Arc::new(RwLock::new(Vec::new())),
}
}
fn default_sensitive_patterns() -> Vec<regex::Regex> {
SENSITIVE_DETECTION_PATTERNS.clone()
}
fn validate_injection(&self, name: &str, value: &str) -> Result<(), ConfigInjectionError> {
debug_assert!(!name.is_empty());
self.validator.validate_env_name(name, Some(value))?;
self.validator.validate_env_value(value)?;
Ok(())
}
pub fn inject(&self, name: &str, value: &str) -> Result<(), ConfigInjectionError> {
#[cfg(test)]
if let Err(retry_after) = TEST_RATE_LIMITER
.get_or_init(InjectionRateLimiter::disabled)
.check_rate_limit()
{
return Err(ConfigInjectionError::RateLimited {
retry_after_seconds: retry_after,
});
}
#[cfg(not(test))]
if let Err(retry_after) = GLOBAL_RATE_LIMITER
.get_or_init(InjectionRateLimiter::new)
.check_rate_limit()
{
return Err(ConfigInjectionError::RateLimited {
retry_after_seconds: retry_after,
});
}
self.validate_injection(name, value)?;
let is_sensitive = self.is_sensitive_field(name);
{
let mut values = self
.values
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
values.insert(name.to_string(), value.to_string());
}
{
let mut history = self
.injection_history
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
history.push(InjectionRecord {
name: name.to_string(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
is_sensitive,
});
}
Ok(())
}
#[allow(clippy::type_complexity)]
pub fn inject_all(
&self,
config: &HashMap<String, String>,
) -> Result<(Vec<String>, Vec<(String, String)>), ConfigInjectionError> {
let mut success = Vec::new();
let mut failures = Vec::new();
let mut valid_injections: Vec<(String, String, bool)> = Vec::new();
for (name, value) in config {
match self.validate_injection(name, value) {
Ok(()) => {
let is_sensitive = self.is_sensitive_field(name);
valid_injections.push((name.clone(), value.clone(), is_sensitive));
}
Err(e) => failures.push((name.clone(), e.to_string())),
}
}
if !valid_injections.is_empty() {
let mut values = self
.values
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
let mut history = self
.injection_history
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
for (name, value, is_sensitive) in valid_injections {
values.insert(name.clone(), value);
history.push(InjectionRecord {
name: name.clone(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
is_sensitive,
});
success.push(name);
}
}
Ok((success, failures))
}
pub fn get(&self, name: &str) -> Option<String> {
let values = self.values.read().ok()?;
values.get(name).cloned()
}
pub fn get_safe(&self, name: &str) -> Option<String> {
let value = self.get(name)?;
if self.is_sensitive_field(name) {
Some(Self::mask_value(&value))
} else {
Some(value)
}
}
pub fn get_all(&self) -> Result<HashMap<String, String>, ConfigInjectionError> {
let values = self
.values
.read()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
Ok(values.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
}
pub fn get_all_safe(&self) -> Result<HashMap<String, String>, ConfigInjectionError> {
let values = self
.values
.read()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
Ok(values
.iter()
.map(|(k, v)| {
let safe_value = if self.is_sensitive_field(k) {
Self::mask_value(v)
} else {
v.clone()
};
(k.clone(), safe_value)
})
.collect())
}
pub fn contains(&self, name: &str) -> Result<bool, ConfigInjectionError> {
let values = self
.values
.read()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
Ok(values.contains_key(name))
}
pub fn remove(&self, name: &str) -> Result<Option<String>, ConfigInjectionError> {
let mut values = self
.values
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
Ok(values.remove(name))
}
pub fn clear(&self) -> Result<(), ConfigInjectionError> {
let mut values = self
.values
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
values.clear();
let mut history = self
.injection_history
.write()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
history.clear();
Ok(())
}
#[allow(dead_code)]
pub(crate) fn get_injection_history(
&self,
) -> Result<Vec<InjectionRecord>, ConfigInjectionError> {
let history = self
.injection_history
.read()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
Ok(history.clone())
}
fn is_sensitive_field(&self, name: &str) -> bool {
let name_lower = name.to_lowercase();
self.sensitive_patterns
.iter()
.any(|pattern| pattern.is_match(&name_lower))
}
fn mask_value(value: &str) -> String {
let char_count = value.chars().count();
if char_count <= 4 {
"*".repeat(char_count)
} else {
let visible = std::cmp::min(2, char_count / 4);
let prefix: String = value.chars().take(visible).collect();
format!("{}{}", prefix, "*".repeat(char_count - visible))
}
}
pub fn len(&self) -> Result<usize, ConfigInjectionError> {
let values = self
.values
.read()
.map_err(|_| ConfigInjectionError::PoisonedLock)?;
Ok(values.len())
}
pub fn is_empty(&self) -> Result<bool, ConfigInjectionError> {
Ok(self.len()? == 0)
}
pub fn validator(&self) -> &EnvSecurityValidator {
&self.validator
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct InjectionRecord {
pub name: String,
pub timestamp: u64,
pub is_sensitive: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigInjectionError {
SecurityValidation(EnvSecurityError),
PoisonedLock,
InvalidName(String),
InvalidValue(String),
RateLimited { retry_after_seconds: u64 },
}
impl std::fmt::Display for ConfigInjectionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigInjectionError::SecurityValidation(e) => {
write!(f, "Security validation failed: {}", e)
}
ConfigInjectionError::PoisonedLock => {
write!(f, "Configuration lock poisoned")
}
ConfigInjectionError::InvalidName(name) => {
write!(f, "Invalid configuration name: {}", name)
}
ConfigInjectionError::InvalidValue(value) => {
write!(f, "Invalid configuration value: {}", value)
}
ConfigInjectionError::RateLimited {
retry_after_seconds,
} => {
write!(
f,
"Rate limited. Retry after {} seconds",
retry_after_seconds
)
}
}
}
}
impl std::error::Error for ConfigInjectionError {}
impl From<EnvSecurityError> for ConfigInjectionError {
fn from(e: EnvSecurityError) -> Self {
ConfigInjectionError::SecurityValidation(e)
}
}
#[derive(Debug, Clone)]
pub struct EnvironmentConfig<'a> {
injector: &'a ConfigInjector,
}
impl<'a> EnvironmentConfig<'a> {
pub fn from_injector(injector: &'a ConfigInjector) -> Self {
Self { injector }
}
pub fn get<T>(&self, name: &str, default: T) -> T
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
let value = self.injector.get(name);
match value {
Some(v) => v.parse().ok().unwrap_or(default),
None => default,
}
}
pub fn get_required<T>(&self, name: &str) -> Result<T, ConfigInjectionError>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
let value = self
.injector
.get(name)
.ok_or_else(|| ConfigInjectionError::InvalidName(name.to_string()))?;
value
.parse()
.map_err(|e| ConfigInjectionError::InvalidValue(format!("{}: {}", name, e)))
}
pub fn get_string(&self, name: &str, default: &str) -> String {
self.injector
.get(name)
.unwrap_or_else(|| default.to_string())
}
pub fn get_bool(&self, name: &str, default: bool) -> bool {
self.get::<bool>(name, default)
}
pub fn get_number<T>(&self, name: &str, default: T) -> T
where
T: std::str::FromStr + std::clone::Clone,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
self.get::<T>(name, default)
}
}
pub(crate) mod macros {
#[macro_export]
macro_rules! safe_inject {
($injector:expr, { $($name:expr => $value:expr),+ }) => {
vec![
$(
$injector.inject($name, $value).map_err(|e| ($name, e))
),+
]
.into_iter()
.filter_map(|r| r.err())
.collect::<Vec<_>>()
};
}
#[macro_export]
macro_rules! inject_from_env {
($injector:expr, $prefix:expr, [$($name:expr),+]) => {
vec![
$(
if let Ok(value) = std::env::var(format!("{}{}", $prefix, $name)) {
$injector.inject(&format!("{}{}", $prefix, $name), &value).map_err(|e| (concat!($prefix, $name), e))
} else {
Ok(())
}
),+
]
.into_iter()
.filter_map(|r| r.err())
.collect::<Vec<_>>()
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_injector_basic() {
let injector = ConfigInjector::new();
assert!(injector.inject("APP_PORT", "8080").is_ok());
assert!(injector.inject("APP_DEBUG", "true").is_ok());
assert_eq!(injector.get("APP_PORT"), Some("8080".to_string()));
assert_eq!(injector.get("APP_DEBUG"), Some("true".to_string()));
assert!(injector.contains("APP_PORT").unwrap());
assert!(!injector.contains("APP_NONEXISTENT").unwrap());
}
#[test]
fn test_sensitive_field_detection() {
let injector = ConfigInjector::new();
assert!(injector.is_sensitive_field("APP_SECRET"));
assert!(injector.is_sensitive_field("API_TOKEN"));
assert!(injector.is_sensitive_field("DATABASE_PASSWORD"));
assert!(!injector.is_sensitive_field("APP_PORT"));
assert!(!injector.is_sensitive_field("APP_HOST"));
assert!(!injector.is_sensitive_field("DEBUG_MODE"));
}
#[test]
fn test_safe_retrieval() {
let validator = EnvSecurityValidator::lenient();
let injector = ConfigInjector::with_validator(validator);
injector.inject("APP_SECRET", "my-secret-value").unwrap();
injector.inject("APP_PORT", "8080").unwrap();
let secret = injector.get_safe("APP_SECRET").unwrap();
assert!(secret.contains('*'));
assert_ne!(secret, "my-secret-value");
let port = injector.get_safe("APP_PORT").unwrap();
assert_eq!(port, "8080");
}
#[test]
fn test_batch_injection() {
let validator = EnvSecurityValidator::lenient();
let injector = ConfigInjector::with_validator(validator);
let mut config = HashMap::new();
config.insert("APP_PORT".to_string(), "8080".to_string());
config.insert("APP_HOST".to_string(), "localhost".to_string());
config.insert("APP_SECRET".to_string(), "secret".to_string());
let (success, failures) = injector.inject_all(&config).unwrap();
assert_eq!(success.len(), 3);
assert!(failures.is_empty());
}
#[test]
fn test_injection_history() {
let validator = EnvSecurityValidator::lenient();
let injector = ConfigInjector::with_validator(validator);
injector.inject("APP_PORT", "8080").unwrap();
injector.inject("APP_SECRET", "secret").unwrap();
let history = injector.get_injection_history().unwrap();
assert_eq!(history.len(), 2);
assert!(!history[0].is_sensitive);
assert!(history[1].is_sensitive);
}
#[test]
fn test_environment_config() {
let injector = ConfigInjector::new();
injector.inject("APP_PORT", "8080").unwrap();
injector.inject("APP_DEBUG", "true").unwrap();
injector.inject("APP_NAME", "test-app").unwrap();
let config = EnvironmentConfig::from_injector(&injector);
assert_eq!(config.get::<u16>("APP_PORT", 8080), 8080);
assert!(config.get::<bool>("APP_DEBUG", false));
assert_eq!(config.get_string("APP_NAME", "default"), "test-app");
}
#[test]
fn test_clear_and_remove() {
let injector = ConfigInjector::new();
injector.inject("APP_PORT", "8080").unwrap();
injector
.inject("APP_CONFIG_VALUE", "secret-key-value")
.unwrap();
assert_eq!(injector.len().unwrap(), 2);
let removed = injector.remove("APP_PORT").unwrap();
assert_eq!(removed, Some("8080".to_string()));
assert_eq!(injector.len().unwrap(), 1);
injector.clear().unwrap();
assert!(injector.is_empty().unwrap());
}
#[test]
fn test_validation_failure() {
let injector = ConfigInjector::new();
assert!(injector.inject("path", "value").is_err());
assert!(injector.inject("HOME", "value").is_err());
assert!(injector.inject("APP_TEST", "hello;world").is_err());
assert!(injector.inject("APP_TEST", "hello${world}").is_err());
}
#[test]
fn test_inject_blocked_and_invalid_name_errors() {
let injector = ConfigInjector::new();
let err = injector.inject("PATH", "value").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::BlockedName { .. })
));
let err = injector.inject("SHELL", "value").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::BlockedName { .. })
));
let err = injector.inject("lowercase", "value").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::InvalidNameFormat { .. })
));
let err = injector.inject("APP;NAME", "value").unwrap_err();
assert!(err.to_string().contains("blocked") || err.to_string().contains("Invalid"));
}
#[test]
fn test_inject_value_dangerous_patterns() {
let injector = ConfigInjector::new();
let err = injector.inject("APP_TEST", "hello;world").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::CommandInjection { .. })
));
let err = injector.inject("APP_TEST", "hello${world}").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::ShellExpansion)
));
let err = injector.inject("APP_TEST", "abc\x01def").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::CommandInjection { .. })
));
let err = injector.inject("APP_TEST", "a|b").unwrap_err();
assert!(err.to_string().contains("dangerous") || err.to_string().contains("Security"));
}
#[test]
fn test_inject_length_errors() {
let injector = ConfigInjector::new();
let long_name = "A".repeat(300);
let err = injector.inject(&long_name, "value").unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::NameTooLong { .. })
));
let long_value = "a".repeat(5000);
let err = injector.inject("APP_TEST", &long_value).unwrap_err();
assert!(matches!(
err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::ValueTooLong { .. })
));
}
#[test]
fn test_sensitive_field_masking() {
let injector = ConfigInjector::with_validator(EnvSecurityValidator::lenient());
injector.inject("APP_SECRET", "super-secret-value").unwrap(); injector.inject("API_TOKEN", "token-1234567890").unwrap(); injector.inject("APP_PORT", "8080").unwrap();
let secret = injector.get_safe("APP_SECRET").unwrap();
assert!(secret.contains('*'));
assert_ne!(secret, "super-secret-value");
let token = injector.get_safe("API_TOKEN").unwrap();
assert!(token.contains('*'));
assert_ne!(token, "token-1234567890");
assert_eq!(injector.get_safe("APP_PORT").unwrap(), "8080");
let all_safe = injector.get_all_safe().unwrap();
assert!(all_safe.get("APP_SECRET").unwrap().contains('*'));
assert!(all_safe.get("API_TOKEN").unwrap().contains('*'));
assert_eq!(all_safe.get("APP_PORT").unwrap(), "8080");
let all = injector.get_all().unwrap();
assert_eq!(all.get("APP_SECRET").unwrap(), "super-secret-value"); }
#[test]
fn test_mask_value_short_and_long() {
let injector = ConfigInjector::with_validator(EnvSecurityValidator::lenient());
injector.inject("APP_SECRET", "ab").unwrap(); let masked = injector.get_safe("APP_SECRET").unwrap();
assert_eq!(masked, "**");
injector.inject("APP_KEY", "abcde").unwrap(); let masked = injector.get_safe("APP_KEY").unwrap();
assert_eq!(masked, "a****");
}
#[test]
fn test_contains_remove_nonexistent() {
let injector = ConfigInjector::new();
injector.inject("APP_PORT", "8080").unwrap();
assert!(!injector.contains("MISSING").unwrap());
let removed = injector.remove("MISSING").unwrap();
assert_eq!(removed, None);
let removed = injector.remove("APP_PORT").unwrap();
assert_eq!(removed, Some("8080".to_string()));
assert!(!injector.contains("APP_PORT").unwrap());
}
#[test]
fn test_len_is_empty_and_validator_ref() {
let injector = ConfigInjector::new();
assert!(injector.is_empty().unwrap());
assert_eq!(injector.len().unwrap(), 0);
injector.inject("APP_PORT", "8080").unwrap();
injector.inject("APP_HOST", "localhost").unwrap();
assert_eq!(injector.len().unwrap(), 2);
assert!(!injector.is_empty().unwrap());
assert!(injector.validator().should_allow_env_var("APP_OK"));
}
#[test]
fn test_inject_all_mixed_success_and_failures() {
let injector = ConfigInjector::new();
let mut config = HashMap::new();
config.insert("APP_PORT".to_string(), "8080".to_string());
config.insert("lowercase".to_string(), "v".to_string());
config.insert("APP_TEST".to_string(), "a;b".to_string());
let (success, failures) = injector.inject_all(&config).unwrap();
assert_eq!(success.len(), 1);
assert!(success.contains(&"APP_PORT".to_string()));
assert_eq!(failures.len(), 2);
let failed_names: Vec<&str> = failures.iter().map(|(n, _)| n.as_str()).collect();
assert!(failed_names.contains(&"lowercase"));
assert!(failed_names.contains(&"APP_TEST"));
for (_, msg) in &failures {
assert!(!msg.is_empty());
}
}
#[test]
fn test_inject_history_after_operations() {
let injector = ConfigInjector::with_validator(EnvSecurityValidator::lenient());
injector.inject("APP_PORT", "8080").unwrap();
injector.inject("APP_SECRET", "secret").unwrap(); let history = injector.get_injection_history().unwrap();
assert_eq!(history.len(), 2);
assert!(!history[0].is_sensitive);
assert!(history[1].is_sensitive);
injector.clear().unwrap();
assert!(injector.get_injection_history().unwrap().is_empty());
assert!(injector.is_empty().unwrap());
}
#[test]
fn test_environment_config_get_required() {
let injector = ConfigInjector::new();
injector.inject("APP_PORT", "8080").unwrap();
injector.inject("APP_BAD", "notanumber").unwrap();
let config = EnvironmentConfig::from_injector(&injector);
assert_eq!(config.get_required::<u16>("APP_PORT").unwrap(), 8080);
let err = config.get_required::<u16>("MISSING").unwrap_err();
assert!(matches!(err, ConfigInjectionError::InvalidName(_)));
let err = config.get_required::<u16>("APP_BAD").unwrap_err();
assert!(matches!(err, ConfigInjectionError::InvalidValue(_)));
}
#[test]
fn test_environment_config_get_with_default() {
let injector = ConfigInjector::new();
injector.inject("APP_PORT", "8080").unwrap();
injector.inject("APP_DEBUG", "true").unwrap();
injector.inject("APP_BAD", "notanumber").unwrap();
let config = EnvironmentConfig::from_injector(&injector);
assert_eq!(config.get::<u16>("APP_PORT", 99), 8080);
assert_eq!(config.get::<u16>("APP_BAD", 99), 99);
assert_eq!(config.get::<u16>("MISSING", 99), 99);
assert_eq!(config.get_string("APP_PORT", "default"), "8080");
assert_eq!(config.get_string("MISSING", "default"), "default");
assert!(config.get_bool("APP_DEBUG", false));
assert!(!config.get_bool("MISSING", false));
assert_eq!(config.get_number::<u32>("APP_PORT", 0), 8080);
assert_eq!(config.get_number::<u32>("MISSING", 7), 7);
}
#[test]
fn test_config_injection_error_display_all_variants() {
let s = format!(
"{}",
ConfigInjectionError::SecurityValidation(EnvSecurityError::NullByte)
);
assert!(s.contains("Security validation failed"));
assert_eq!(
format!("{}", ConfigInjectionError::PoisonedLock),
"Configuration lock poisoned"
);
assert_eq!(
format!("{}", ConfigInjectionError::InvalidName("nm".to_string())),
"Invalid configuration name: nm"
);
assert_eq!(
format!("{}", ConfigInjectionError::InvalidValue("vl".to_string())),
"Invalid configuration value: vl"
);
assert_eq!(
format!(
"{}",
ConfigInjectionError::RateLimited {
retry_after_seconds: 30
}
),
"Rate limited. Retry after 30 seconds"
);
}
#[test]
fn test_config_injection_error_from_env_security() {
let env_err = EnvSecurityError::NullByte;
let inj_err: ConfigInjectionError = env_err.into();
assert!(matches!(
inj_err,
ConfigInjectionError::SecurityValidation(EnvSecurityError::NullByte)
));
}
#[test]
fn test_config_injector_default_and_with_validator() {
let injector = ConfigInjector::default();
assert!(injector.inject("APP_PORT", "8080").is_ok());
let strict_injector = ConfigInjector::with_validator(EnvSecurityValidator::strict());
assert!(strict_injector.inject("APP_PORT", "8080").is_ok());
}
#[test]
fn test_rate_limiter_disabled_always_ok() {
let limiter = InjectionRateLimiter::disabled();
for _ in 0..10 {
assert!(limiter.check_rate_limit().is_ok());
}
}
#[test]
fn test_rate_limiter_enforces_limit() {
let limiter = InjectionRateLimiter::with_limits(1, 3600);
assert!(limiter.check_rate_limit().is_ok());
assert!(limiter.check_rate_limit().is_ok());
assert!(limiter.check_rate_limit().is_err());
}
#[test]
fn test_rate_limiter_usage_stats() {
let limiter = InjectionRateLimiter::with_limits(100, 60);
let (counter, remaining, usage) = limiter.usage_stats();
assert_eq!(counter, 0);
assert_eq!(remaining, 0);
assert_eq!(usage, 0.0);
assert!(limiter.check_rate_limit().is_ok());
let (counter, remaining, usage) = limiter.usage_stats();
assert_eq!(counter, 0);
assert_eq!(usage, 0.0);
assert!(remaining > 0 && remaining <= 60);
}
}