use crate::security::{
ConfigInjector, ConfigValidator, EnvSecurityValidator, ErrorSanitizer, FilterResult,
InputValidator, SafeResult, SecureString, SecureStringBuilder, SensitiveDataDetector,
SensitiveDataFilter, SensitivityLevel,
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
static RUN_TESTS: AtomicUsize = AtomicUsize::new(0);
static PASSED_TESTS: AtomicUsize = AtomicUsize::new(0);
static FAILED_TESTS: AtomicUsize = AtomicUsize::new(0);
pub fn test_stats() -> (usize, usize, usize) {
(
RUN_TESTS.load(Ordering::SeqCst),
PASSED_TESTS.load(Ordering::SeqCst),
FAILED_TESTS.load(Ordering::SeqCst),
)
}
#[cfg(test)]
pub fn reset_test_counters() {
RUN_TESTS.store(0, Ordering::SeqCst);
PASSED_TESTS.store(0, Ordering::SeqCst);
FAILED_TESTS.store(0, Ordering::SeqCst);
}
macro_rules! run_test {
($name:ident, $test:expr) => {
#[test]
pub fn $name() {
RUN_TESTS.fetch_add(1, Ordering::SeqCst);
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $test)) {
Ok(_) => {
PASSED_TESTS.fetch_add(1, Ordering::SeqCst);
}
Err(_) => {
FAILED_TESTS.fetch_add(1, Ordering::SeqCst);
panic!("Test {} failed", stringify!($name));
}
}
}
};
}
mod sensitive_data_tests {
use super::*;
run_test!(test_secure_string_creation, {
let secret = SecureString::from("test-password");
assert_eq!(secret.len(), 13);
assert!(!secret.is_empty());
assert_eq!(secret.sensitivity(), SensitivityLevel::Critical);
});
run_test!(test_secure_string_sensitivity_levels, {
let low = SecureString::new("data", SensitivityLevel::Low);
let medium = SecureString::new("user", SensitivityLevel::Medium);
let high = SecureString::new("token", SensitivityLevel::High);
let critical = SecureString::new("secret", SensitivityLevel::Critical);
assert!(!low.is_highly_sensitive());
assert!(!medium.is_highly_sensitive());
assert!(high.is_highly_sensitive());
assert!(critical.is_highly_sensitive());
});
run_test!(test_secure_string_comparison, {
let secret1 = SecureString::from("password123");
let secret2 = SecureString::from("password123");
let secret3 = SecureString::from("different");
assert!(secret1.compare("password123").is_ok());
assert!(secret1.compare("wrong").is_err());
assert_eq!(secret1, secret2);
assert_ne!(secret1, secret3);
});
run_test!(test_secure_string_masking, {
let secret = SecureString::from("mySecretPassword");
let masked = secret.masked();
assert!(masked.contains('*'));
assert!(masked.len() < secret.len());
assert!(masked.starts_with("my"));
});
run_test!(test_secure_string_fingerprint, {
let secret = SecureString::from("password123");
let fp = secret.fingerprint(16);
assert_eq!(fp.len(), 16);
assert!(fp.chars().all(|c| c.is_ascii_hexdigit()));
});
run_test!(test_secure_string_builder, {
let secret = SecureStringBuilder::new()
.push_str("pass")
.push('w')
.push_str("ord")
.build();
assert_eq!(secret.as_str(), "password");
});
run_test!(test_secure_string_from_bytes, {
let data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let secret = SecureString::from_bytes(data.clone(), SensitivityLevel::High);
assert_eq!(secret.as_bytes(), &data[..]);
});
}
mod injection_attack_tests {
use super::*;
run_test!(test_sql_injection_patterns, {
let validator = InputValidator::new();
let attacks = vec![
"'; DROP TABLE users;--",
"' OR '1'='1",
"admin'--",
"UNION SELECT * FROM users",
"1; DELETE FROM products",
];
for attack in attacks {
let result = validator.validate_string(attack);
assert!(result.is_err(), "Should reject SQL injection: {}", attack);
}
});
run_test!(test_command_injection_patterns, {
let validator = InputValidator::new();
let attacks = vec![
"; cat /etc/passwd",
"| whoami",
"`id`",
"$(whoami)",
"&& rm -rf /",
"hello; world",
"test${HOME}",
];
for attack in attacks {
let result = validator.validate_string(attack);
assert!(
result.is_err(),
"Should reject command injection: {}",
attack
);
}
});
run_test!(test_xss_patterns, {
let validator = InputValidator::new();
let attacks = vec![
"<script>alert('xss')</script>",
"javascript:alert(1)",
"<img src=x onerror=alert(1)>",
"<iframe src='javascript:alert(1)'>",
];
for attack in attacks {
let _result = validator.validate_string(attack);
}
});
run_test!(test_path_traversal, {
let validator = InputValidator::new();
let attacks = vec![
"../../etc/passwd",
"..\\..\\windows\\system32",
"/var/www/../../../etc/passwd",
"file.txt/../../../etc/passwd",
];
for attack in attacks {
let result = validator.validate_string(attack);
assert!(result.is_err(), "Should reject path traversal: {}", attack);
}
});
run_test!(test_environment_injection_protection, {
let validator = EnvSecurityValidator::new();
let attacks = vec![
("PATH", "malicious"),
("HOME", "/tmp"),
("LD_PRELOAD", "libmalicious.so"),
("SECRET_KEY", "value"),
];
for (name, _value) in attacks {
let result = validator.validate_env_name(name, None);
assert!(
result.is_err(),
"Should reject environment variable: {}",
name
);
}
});
run_test!(test_shell_expansion_protection, {
let validator = EnvSecurityValidator::new();
let attacks = vec!["${USER}", "${HOME}", "`ls`", "$(whoami)"];
for value in attacks {
let result = validator.validate_env_value(value);
assert!(result.is_err(), "Should reject shell expansion: {}", value);
}
});
run_test!(test_null_byte_injection, {
let validator = EnvSecurityValidator::new();
let attacks = vec!["hello\0world", "pass\x00word"];
for value in attacks {
let result = validator.validate_env_value(value);
assert!(result.is_err(), "Should reject null byte injection");
}
});
}
mod boundary_condition_tests {
use super::*;
run_test!(test_empty_input, {
let validator = InputValidator::new();
assert!(validator.validate_string("").is_ok());
});
run_test!(test_very_long_input, {
let validator = InputValidator::new().with_max_string_length(100);
let long_input = "a".repeat(200);
let result = validator.validate_string(&long_input);
assert!(result.is_err());
let valid_input = "a".repeat(100);
assert!(validator.validate_string(&valid_input).is_ok());
});
run_test!(test_special_characters, {
let validator = InputValidator::new();
let valid = vec![
"hello-world",
"hello_world",
"hello.world",
"hello123",
"HELLO",
];
for s in valid {
assert!(validator.validate_string(s).is_ok(), "Should accept: {}", s);
}
});
run_test!(test_field_name_validation, {
let validator = InputValidator::new();
assert!(validator.validate_field_name("app_name").is_ok());
assert!(validator.validate_field_name("appPort").is_ok());
assert!(validator.validate_field_name("APP_123").is_ok());
assert!(validator.validate_field_name("app-name").is_ok());
assert!(validator.validate_field_name("123app").is_err());
assert!(validator.validate_field_name("app name").is_err());
assert!(validator.validate_field_name("").is_err());
});
run_test!(test_url_validation, {
let validator = InputValidator::new();
assert!(validator.validate_url("https://example.com").is_ok());
assert!(validator.validate_url("http://localhost:8080").is_ok());
assert!(validator.validate_url("ftp://example.com").is_err());
assert!(validator.validate_url("javascript:alert(1)").is_err());
assert!(validator.validate_url("file:///etc/passwd").is_err());
});
run_test!(test_email_validation, {
let validator = InputValidator::new();
assert!(validator.validate_email("user@example.com").is_ok());
assert!(validator.validate_email("user.name@example.co.uk").is_ok());
assert!(validator.validate_email("invalid").is_err());
assert!(validator.validate_email("@example.com").is_err());
assert!(validator.validate_email("user@").is_err());
});
run_test!(test_config_injector_boundaries, {
let injector = ConfigInjector::new();
assert!(injector.inject("APP_PORT", "8080").is_ok());
assert!(injector.inject("APP_NAME", "test-app").is_ok());
let long_value = "x".repeat(5000);
let result = injector.inject("APP_LONG", &long_value);
assert!(result.is_err()); });
run_test!(test_sanitizer_boundary_cases, {
let sanitizer = ErrorSanitizer::new();
assert!(!sanitizer.contains_sensitive(""));
assert!(!sanitizer.contains_sensitive("Hello world"));
let long_msg = "normal ".repeat(1000) + "password: secret";
let result = sanitizer.sanitize(&long_msg);
assert!(result.contains("***"));
});
}
mod memory_safety_tests {
use super::*;
run_test!(test_secure_string_zeroize, {
let mut secret = SecureString::from("sensitive-data");
let before_zeroize = secret.as_bytes().to_vec();
assert_eq!(before_zeroize, b"sensitive-data".to_vec());
secret.zeroize();
let after_zeroize = secret.as_bytes();
assert!(after_zeroize.iter().all(|&b| b == 0));
});
run_test!(test_secure_string_drop_zeroize, {
let original_count = crate::security::secure_string::allocated_secure_strings();
{
let _secret = SecureString::from("temporary-secret");
}
let after_count = crate::security::secure_string::deallocated_secure_strings();
assert!(after_count > original_count);
});
run_test!(test_clone_warning, {
let secret = SecureString::from("secret");
let _cloned = secret.clone();
assert_eq!(secret, _cloned);
});
run_test!(test_constant_time_comparison, {
let secret = SecureString::from("password123");
let mut correct_times = Vec::new();
let mut wrong_times = Vec::new();
for _ in 0..10 {
let start1 = std::time::Instant::now();
secret.compare("password123").unwrap();
correct_times.push(start1.elapsed());
let start2 = std::time::Instant::now();
secret.compare("wrongpassword").unwrap_err();
wrong_times.push(start2.elapsed());
}
let avg_correct: i64 = correct_times
.iter()
.map(|t| t.as_nanos() as i64)
.sum::<i64>()
/ 10;
let avg_wrong: i64 = wrong_times.iter().map(|t| t.as_nanos() as i64).sum::<i64>() / 10;
let time_diff = (avg_correct - avg_wrong).abs();
assert!(
time_diff < 5_000_000,
"Average comparison time difference too large: {}ns (correct: {}ns, wrong: {}ns)",
time_diff,
avg_correct,
avg_wrong
);
});
run_test!(test_memory_no_leak_in_comparison, {
let secret = SecureString::from("secret");
let _ = secret.compare("secret");
});
}
mod sensitive_data_detection_tests {
use super::*;
run_test!(test_api_key_detection, {
let sanitizer = ErrorSanitizer::new();
let messages = vec![
"API Key: sk-1234567890abcdef",
"Access Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
];
for msg in messages {
let contains = sanitizer.contains_sensitive(msg);
assert!(contains, "Should detect sensitive data in: {}", msg);
}
});
run_test!(test_password_detection, {
let sanitizer = ErrorSanitizer::new();
let messages = vec![
"Password: mySecretPassword123",
"DB_PASSWORD=secret123",
"auth: user:password@host",
];
for msg in messages {
let contains = sanitizer.contains_sensitive(msg);
assert!(contains, "Should detect password in: {}", msg);
}
});
run_test!(test_connection_string_detection, {
let sanitizer = ErrorSanitizer::new();
let messages = vec![
"mongodb://user:pass@localhost:27017/db",
"postgresql://user:password@localhost/db",
"Connection string: Server=myServer;Database=myDB;User Id=myUsername;Password=myPassword;",
];
for msg in messages {
let contains = sanitizer.contains_sensitive(msg);
assert!(contains, "Should detect connection string in: {}", msg);
}
});
run_test!(test_email_detection, {
let sanitizer = ErrorSanitizer::new();
let msg = "Contact user@example.com for support";
assert!(sanitizer.contains_sensitive(msg));
let sanitized = sanitizer.sanitize(msg);
assert!(!sanitized.contains("user@example.com"));
});
run_test!(test_strict_mode_masking, {
let sanitizer = ErrorSanitizer::new().with_strict_mode();
let msg = "The password is secret and the token is key data";
let sanitized = sanitizer.sanitize(msg);
assert!(!sanitized.contains("password"));
assert!(!sanitized.contains("secret"));
assert!(!sanitized.contains("token"));
assert!(!sanitized.contains("key"));
});
}
mod config_validation_tests {
use super::*;
run_test!(test_valid_config_validation, {
let validator = ConfigValidator::new();
let mut config = HashMap::new();
config.insert("app_name".to_string(), "my-app".to_string());
config.insert("app_port".to_string(), "8080".to_string());
config.insert("debug_mode".to_string(), "true".to_string());
let result = validator.validate(&config);
assert!(result.is_valid());
assert!(result.errors.is_empty());
});
run_test!(test_sensitive_config_detection, {
let validator = ConfigValidator::new();
let mut config = HashMap::new();
config.insert("app_name".to_string(), "my-app".to_string());
config.insert("database_password".to_string(), "secret123".to_string());
config.insert("api_token".to_string(), "token123".to_string());
let result = validator.validate(&config);
assert!(result.is_valid()); assert!(result.has_sensitive_data()); assert_eq!(result.sensitive_fields.len(), 2);
});
run_test!(test_invalid_config_detection, {
let validator = ConfigValidator::new();
let mut config = HashMap::new();
config.insert("123invalid".to_string(), "value".to_string()); config.insert("valid_field".to_string(), "hello;world".to_string());
let result = validator.validate(&config);
assert!(!result.is_valid());
assert!(!result.errors.is_empty());
});
run_test!(test_safe_validation, {
let validator = ConfigValidator::new();
let mut config = HashMap::new();
config.insert("app_name".to_string(), "my-app".to_string());
config.insert(
"dangerous_field".to_string(),
"'; DROP TABLE users;--".to_string(),
);
let result = validator.validate_safe(&config);
assert!(!result);
});
run_test!(test_config_validator_builder, {
let validator = ConfigValidator::builder()
.max_string_length(100)
.add_sensitive_field("custom_field")
.strict_mode()
.build();
let mut config = HashMap::new();
config.insert("app_name".to_string(), "my-app".to_string());
config.insert("custom_field".to_string(), "sensitive".to_string());
let result = validator.validate(&config);
assert!(result.has_sensitive_data());
});
}
mod filtering_tests {
use super::*;
run_test!(test_message_filtering, {
let mut filter = SensitiveDataFilter::new();
filter.add_blocked_pattern(r".*password.*").unwrap();
filter.add_allowed_pattern(r"^Safe:.*").unwrap();
let result = filter.filter("Contains password secret");
assert!(result.is_blocked());
let result = filter.filter("Safe: normal message");
assert!(result.is_allowed());
let result = filter.filter("API Key: sk-12345");
match result {
FilterResult::Sanitized(msg) => {
assert!(!msg.contains("sk-12345"));
}
_ => panic!("Expected sanitized result"),
}
});
run_test!(test_batch_filtering, {
let mut filter = SensitiveDataFilter::new();
filter.add_blocked_pattern(r"(?i)password").unwrap();
let messages = vec!["Normal message", "Password: secret", "API Key: sk-12345"];
let results = filter.filter_all(&messages);
assert!(results[0].1.is_allowed());
assert!(results[1].1.is_blocked());
match results[2].1 {
FilterResult::Sanitized(_) => {}
_ => panic!("Expected sanitized result"),
}
});
}
mod safe_result_tests {
use super::*;
run_test!(test_safe_result_success, {
let result = SafeResult::ok("value");
assert!(result.is_ok());
assert!(!result.is_err());
assert_eq!(result.value(), Some(&"value"));
assert!(!result.contained_sensitive());
});
run_test!(test_safe_result_error, {
let result: SafeResult<()> = SafeResult::err("Error with password: secret");
assert!(!result.is_ok());
assert!(result.is_err());
assert!(result.value().is_none());
assert!(result.error_message().is_some());
assert!(result.contained_sensitive());
let msg = result.error_message().unwrap();
assert!(!msg.contains("password"));
assert!(!msg.contains("secret"));
});
run_test!(test_safe_result_operations, {
let success: SafeResult<i32> = SafeResult::ok(42);
assert_eq!(success.clone().unwrap(), 42);
assert_eq!(success.unwrap_or(0), 42);
let failure = SafeResult::err("error");
assert_eq!(failure.unwrap_or(0), 0);
});
}
mod integration_tests {
use super::*;
run_test!(test_full_security_flow, {
let sanitizer = ErrorSanitizer::new();
let sensitive_value = "API Key: sk-12345";
let sanitized = sanitizer.sanitize(sensitive_value);
assert_ne!(sanitized, sensitive_value);
assert!(sanitized.contains("***"));
let detector = SensitiveDataDetector::new();
let sensitivity = detector.is_sensitive("APP_SECRET", "my-secret");
assert!(sensitivity.needs_protection());
});
run_test!(test_end_to_end_security, {
let secret = SecureString::from("super-secret-key");
assert!(secret.compare("super-secret-key").is_ok());
let detector = crate::security::input_validation::SensitiveDataDetector::new();
let sensitivity = detector.is_sensitive("API_TOKEN", "token-value");
assert!(sensitivity.needs_protection());
let validator = InputValidator::new();
assert!(validator.validate_string("valid input").is_ok());
assert!(validator.validate_string("'; DROP TABLE").is_err());
let sanitizer = ErrorSanitizer::new();
let error_msg = "Failed with password: secret123";
let sanitized = sanitizer.sanitize(error_msg);
assert!(!sanitized.contains("secret123"));
let logger = crate::security::error_sanitization::SecureLogger::new();
logger.error_with_context("Auth", "Login failed with password: secret");
});
run_test!(test_attack_scenario_coverage, {
let input_validator = InputValidator::new();
let sql_injection = "' OR '1'='1";
assert!(input_validator.validate_string(sql_injection).is_err());
let env_validator = EnvSecurityValidator::new();
let cmd_injection = "; rm -rf /";
assert!(env_validator.validate_env_value(cmd_injection).is_err());
let sanitizer = ErrorSanitizer::new();
let sensitive_error = "Database error: password=secret123";
let sanitized = sanitizer.sanitize(sensitive_error);
assert!(!sanitized.contains("secret123"));
let path_traversal = "../../../etc/passwd";
assert!(input_validator.validate_string(path_traversal).is_err());
let shell_expansion = "${HOME}/.bashrc";
assert!(env_validator.validate_env_value(shell_expansion).is_err());
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_all_security_tests() {
reset_test_counters();
sensitive_data_tests::test_secure_string_creation();
injection_attack_tests::test_sql_injection_patterns();
boundary_condition_tests::test_empty_input();
memory_safety_tests::test_secure_string_zeroize();
sensitive_data_detection_tests::test_api_key_detection();
config_validation_tests::test_valid_config_validation();
filtering_tests::test_message_filtering();
safe_result_tests::test_safe_result_success();
integration_tests::test_full_security_flow();
let (run, passed, failed) = test_stats();
println!(
"Security Tests: {} run, {} passed, {} failed",
run, passed, failed
);
assert_eq!(failed, 0, "All security tests should pass");
}
}