use std::sync::Arc;
use std::time::{Duration, Instant};
use url_preview::{
CacheStrategy, ContentLimits, Fetcher, FetcherConfig, PreviewError, PreviewService, PreviewServiceConfig,
UrlValidationConfig,
};
#[tokio::test]
async fn test_concurrent_security_validation() {
let mut url_config = UrlValidationConfig::default();
url_config.allowed_domains.insert("example.com".to_string());
let fetcher_config = FetcherConfig {
url_validation: url_config,
..Default::default()
};
let custom_fetcher = Fetcher::with_config(fetcher_config);
let service_config = PreviewServiceConfig::new(1000)
.with_cache_strategy(CacheStrategy::UseCache)
.with_max_concurrent_requests(5)
.with_default_fetcher(custom_fetcher);
let service = Arc::new(PreviewService::new_with_config(service_config));
let urls = vec![
("https://example.com", true),
("https://blocked.com", false),
("http://localhost", false),
("http://192.168.1.1", false),
("https://sub.example.com", true),
("file:///etc/passwd", false),
("https://another-blocked.com", false),
("https://example.com/page", true),
];
let mut handles = vec![];
for (url, should_succeed) in urls {
let service_clone = Arc::clone(&service);
let url = url.to_string();
let handle = tokio::spawn(async move {
let result = service_clone.generate_preview(&url).await;
(url, should_succeed, result)
});
handles.push(handle);
}
for handle in handles {
let (url, should_succeed, result) = handle.await.unwrap();
if should_succeed {
match result {
Ok(_) => println!("✓ {} - Allowed as expected", url),
Err(e) => println!("ℹ {} - Failed (might be network): {}", url, e),
}
} else {
match result {
Err(PreviewError::DomainNotAllowed(_))
| Err(PreviewError::LocalhostBlocked)
| Err(PreviewError::PrivateIpBlocked(_))
| Err(PreviewError::InvalidUrlScheme(_)) => {
println!("✓ {} - Blocked as expected", url);
}
Ok(_) => panic!("✗ {} - Should have been blocked!", url),
Err(e) => println!("✗ {} - Wrong error type: {}", url, e),
}
}
}
}
#[tokio::test]
async fn test_security_with_caching() {
let mut url_config = UrlValidationConfig::default();
url_config.blocked_domains.insert("blocked.com".to_string());
let fetcher_config = FetcherConfig {
url_validation: url_config,
..Default::default()
};
let custom_fetcher = Fetcher::with_config(fetcher_config);
let service_config = PreviewServiceConfig::new(100)
.with_cache_strategy(CacheStrategy::UseCache)
.with_default_fetcher(custom_fetcher);
let service = PreviewService::new_with_config(service_config);
let result1 = service.generate_preview("https://blocked.com").await;
assert!(matches!(result1, Err(PreviewError::DomainBlocked(_))));
let result2 = service.generate_preview("https://blocked.com").await;
assert!(matches!(result2, Err(PreviewError::DomainBlocked(_))));
if let Ok(_) = service.generate_preview("https://example.com").await {
let start = Instant::now();
let _ = service.generate_preview("https://example.com").await;
let cached_duration = start.elapsed();
let start = Instant::now();
let _ = service
.generate_preview_no_cache("https://example.com")
.await;
let fresh_duration = start.elapsed();
println!(
"Cached request: {:?}, Fresh request: {:?}",
cached_duration, fresh_duration
);
}
}
#[tokio::test]
async fn test_security_error_propagation() {
let service = PreviewService::new();
let test_cases = vec![
("http://localhost", "LocalhostBlocked"),
("http://127.0.0.1", "LocalhostBlocked"),
("http://192.168.1.1", "PrivateIpBlocked"),
("file:///etc/passwd", "InvalidUrlScheme"),
("ftp://example.com", "InvalidUrlScheme"),
("javascript:alert(1)", "InvalidUrlScheme"),
];
for (url, expected_error) in test_cases {
match service.generate_preview(url).await {
Err(e) => {
let error_type = match &e {
PreviewError::LocalhostBlocked => "LocalhostBlocked",
PreviewError::PrivateIpBlocked(_) => "PrivateIpBlocked",
PreviewError::InvalidUrlScheme(_) => "InvalidUrlScheme",
_ => "Other",
};
assert_eq!(error_type, expected_error, "URL: {}", url);
println!("{}: {}", url, e);
}
Ok(_) => panic!("{} should have been blocked", url),
}
}
}
#[tokio::test]
async fn test_progressive_security_levels() {
let mut config1 = UrlValidationConfig::default();
config1.block_private_ips = false;
config1.block_localhost = false;
let custom_fetcher1 = Fetcher::with_config(FetcherConfig {
url_validation: config1,
..Default::default()
});
let service1 = PreviewService::new_with_config(
PreviewServiceConfig::new(1000)
.with_cache_strategy(CacheStrategy::UseCache)
.with_default_fetcher(custom_fetcher1)
);
match service1.generate_preview("http://localhost").await {
Ok(_) | Err(_) => { }
}
let service2 = PreviewService::new();
assert!(matches!(
service2.generate_preview("http://localhost").await,
Err(PreviewError::LocalhostBlocked)
));
let mut config3 = UrlValidationConfig::default();
config3.allowed_schemes.clear();
config3.allowed_schemes.insert("https".to_string());
config3.allowed_domains.insert("trusted.com".to_string());
let custom_fetcher3 = Fetcher::with_config(FetcherConfig {
url_validation: config3,
content_limits: ContentLimits {
max_content_size: 1024 * 1024, max_download_time: 5,
..Default::default()
},
timeout: Duration::from_secs(5),
..Default::default()
});
let service3 = PreviewService::new_with_config(
PreviewServiceConfig::new(1000)
.with_cache_strategy(CacheStrategy::UseCache)
.with_max_concurrent_requests(10)
.with_default_fetcher(custom_fetcher3)
);
assert!(matches!(
service3.generate_preview("http://trusted.com").await,
Err(PreviewError::InvalidUrlScheme(_))
));
assert!(matches!(
service3.generate_preview("https://untrusted.com").await,
Err(PreviewError::DomainNotAllowed(_))
));
}
#[tokio::test]
async fn test_content_security_streaming() {
let mut limits = ContentLimits::default();
limits.max_content_size = 1024 * 10; limits.max_download_time = 2;
let custom_fetcher = Fetcher::with_config(FetcherConfig {
content_limits: limits,
timeout: Duration::from_secs(3),
..Default::default()
});
let service = PreviewService::new_with_config(
PreviewServiceConfig::new(1000)
.with_cache_strategy(CacheStrategy::UseCache)
.with_default_fetcher(custom_fetcher)
);
match service
.generate_preview("https://en.wikipedia.org/wiki/Rust_(programming_language)")
.await
{
Ok(_) => println!("Page was under size limit"),
Err(PreviewError::ContentSizeExceeded { size, limit }) => {
println!("✓ Content size limit enforced: {} > {}", size, limit);
assert!(size > limit);
assert_eq!(limit, 1024 * 10);
}
Err(PreviewError::DownloadTimeExceeded { elapsed, limit }) => {
println!("✓ Download time limit enforced: {}s > {}s", elapsed, limit);
assert!(elapsed >= limit);
assert_eq!(limit, 2);
}
Err(e) => println!("Other error: {}", e),
}
}
#[tokio::test]
async fn test_security_with_rate_limiting() {
let service = PreviewService::new_with_config(
PreviewServiceConfig::new(1000)
.with_cache_strategy(CacheStrategy::UseCache)
.with_max_concurrent_requests(1)
);
let start = Instant::now();
let result = service.generate_preview("http://localhost").await;
let duration = start.elapsed();
assert!(matches!(result, Err(PreviewError::LocalhostBlocked)));
assert!(
duration < Duration::from_millis(100),
"Security check should be instant"
);
}
#[tokio::test]
async fn test_mixed_security_batch() {
let service = PreviewService::new();
let urls = vec![
"https://example.com",
"http://localhost",
"https://www.rust-lang.org",
"http://192.168.1.1",
"file:///etc/passwd",
];
let mut results = vec![];
for url in &urls {
results.push(service.generate_preview(url).await);
}
assert!(results[0].is_ok() || results[0].is_err()); assert!(matches!(results[1], Err(PreviewError::LocalhostBlocked)));
assert!(results[2].is_ok() || results[2].is_err()); assert!(matches!(results[3], Err(PreviewError::PrivateIpBlocked(_))));
assert!(matches!(results[4], Err(PreviewError::InvalidUrlScheme(_))));
}