#![cfg(feature = "http_client")]
use bashkit::{Bash, NetworkAllowlist};
mod allowlist {
use super::*;
#[tokio::test]
async fn threat_empty_allowlist_blocks_all() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl https://example.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_allowlist_blocks_unlisted() {
let allowlist = NetworkAllowlist::new().allow("https://allowed.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl https://blocked.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_ip_bypass_blocked() {
let allowlist = NetworkAllowlist::new().allow("https://example.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl https://93.184.216.34").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_port_bypass_blocked() {
let allowlist = NetworkAllowlist::new().allow("https://example.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl https://example.com:8443").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_scheme_downgrade_blocked() {
let allowlist = NetworkAllowlist::new().allow("https://example.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl http://example.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_subdomain_bypass_blocked() {
let allowlist = NetworkAllowlist::new().allow("https://example.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl https://evil.example.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_path_prefix_enforced() {
let allowlist = NetworkAllowlist::new().allow("https://api.example.com/v1");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl https://api.example.com/v2").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn threat_wget_respects_allowlist() {
let allowlist = NetworkAllowlist::new().allow("https://allowed.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("wget https://blocked.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("access denied"));
}
}
mod private_ip_blocking {
use bashkit::NetworkAllowlist;
#[test]
fn threat_private_ip_loopback_blocked() {
let allowlist = NetworkAllowlist::new().allow("http://127.0.0.1:8080");
assert!(
!allowlist.is_allowed("http://127.0.0.1:8080/"),
"Requests to 127.0.0.1 should be blocked by default"
);
}
#[test]
fn threat_private_ip_link_local_blocked() {
let allowlist = NetworkAllowlist::new().allow("http://169.254.169.254");
assert!(
!allowlist.is_allowed("http://169.254.169.254/latest/meta-data/"),
"Requests to 169.254.169.254 (cloud metadata) should be blocked"
);
}
#[test]
fn threat_private_ip_rfc1918_blocked() {
let allowlist = NetworkAllowlist::new().allow("http://10.0.0.1");
assert!(!allowlist.is_allowed("http://10.0.0.1/"));
let allowlist = NetworkAllowlist::new().allow("http://172.16.0.1");
assert!(!allowlist.is_allowed("http://172.16.0.1/"));
let allowlist = NetworkAllowlist::new().allow("http://192.168.1.1");
assert!(!allowlist.is_allowed("http://192.168.1.1/"));
}
#[test]
fn private_ip_blocking_can_be_disabled() {
let allowlist = NetworkAllowlist::new()
.block_private_ips(false)
.allow("http://127.0.0.1:8080");
assert!(
allowlist.is_allowed("http://127.0.0.1:8080/"),
"Private IP should be allowed when blocking is disabled"
);
}
#[test]
fn public_ip_is_allowed() {
let allowlist = NetworkAllowlist::new().allow("http://8.8.8.8");
assert!(
allowlist.is_allowed("http://8.8.8.8/"),
"Public IPs should be allowed when in allowlist"
);
}
}
mod no_network {
use super::*;
#[tokio::test]
async fn curl_without_network_config() {
let mut bash = Bash::new();
let result = bash.exec("curl https://example.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("network access not configured"));
}
#[tokio::test]
async fn wget_without_network_config() {
let mut bash = Bash::new();
let result = bash.exec("wget https://example.com").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("network access not configured"));
}
}
mod curl_args {
use super::*;
#[tokio::test]
async fn curl_requires_url() {
let mut bash = Bash::new();
let result = bash.exec("curl").await.unwrap();
assert_eq!(result.exit_code, 3);
assert!(result.stderr.contains("no URL specified"));
}
#[tokio::test]
async fn curl_method_parsing() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl -X POST https://example.com").await.unwrap();
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_data_implies_post() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -d 'data=test' https://example.com")
.await
.unwrap();
assert!(result.stderr.contains("access denied"));
}
}
mod wget_args {
use super::*;
#[tokio::test]
async fn wget_requires_url() {
let mut bash = Bash::new();
let result = bash.exec("wget").await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("missing URL"));
}
#[tokio::test]
async fn wget_quiet_mode() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("wget -q https://example.com").await.unwrap();
assert!(result.stderr.contains("access denied"));
}
}
mod script_integration {
use super::*;
#[tokio::test]
async fn curl_in_pipeline() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl https://blocked.com 2>&1 | grep -c denied")
.await
.unwrap();
assert!(result.stdout.trim() == "1" || result.exit_code != 0);
}
#[tokio::test]
async fn curl_exit_code_in_condition() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec(
r#"
if curl https://blocked.com 2>/dev/null; then
echo "success"
else
echo "failed"
fi
"#,
)
.await
.unwrap();
assert!(result.stdout.contains("failed"));
}
#[tokio::test]
async fn wget_output_redirect() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -O output.txt https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
}
mod security_edge_cases {
use super::*;
#[tokio::test]
async fn curl_malformed_url() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl not-a-url").await.unwrap();
assert_ne!(result.exit_code, 0);
}
#[tokio::test]
async fn curl_very_long_url() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let long_path = "a".repeat(10000);
let script = format!("curl https://example.com/{}", long_path);
let result = bash.exec(&script).await.unwrap();
assert_ne!(result.exit_code, 0);
}
#[tokio::test]
async fn curl_url_with_special_chars() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl 'https://example.com/path?a=1&b=2'")
.await
.unwrap();
assert!(result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_in_command_substitution() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("echo $(curl https://blocked.com 2>&1)")
.await
.unwrap();
assert!(result.stdout.contains("access denied"));
}
#[tokio::test]
async fn curl_with_variable_url() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec(
r#"
URL="https://blocked.com"
curl $URL 2>&1
"#,
)
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
}
mod allowlist_unit {
use bashkit::NetworkAllowlist;
#[test]
fn test_empty_allowlist() {
let allowlist = NetworkAllowlist::new();
assert!(!allowlist.is_allowed("https://example.com"));
}
#[test]
fn test_allow_all() {
let allowlist = NetworkAllowlist::allow_all();
assert!(allowlist.is_allowed("https://example.com"));
assert!(allowlist.is_allowed("http://any.domain.com:8080/path"));
}
#[test]
fn test_specific_host() {
let allowlist = NetworkAllowlist::new().allow("https://api.example.com");
assert!(allowlist.is_allowed("https://api.example.com"));
assert!(allowlist.is_allowed("https://api.example.com/any/path"));
assert!(!allowlist.is_allowed("https://other.example.com"));
}
#[test]
fn test_path_prefix() {
let allowlist = NetworkAllowlist::new().allow("https://api.example.com/v1");
assert!(allowlist.is_allowed("https://api.example.com/v1"));
assert!(allowlist.is_allowed("https://api.example.com/v1/users"));
assert!(!allowlist.is_allowed("https://api.example.com/v2"));
assert!(!allowlist.is_allowed("https://api.example.com/"));
}
#[test]
fn test_multiple_allowed() {
let allowlist = NetworkAllowlist::new()
.allow("https://api.example.com")
.allow("https://cdn.example.com");
assert!(allowlist.is_allowed("https://api.example.com"));
assert!(allowlist.is_allowed("https://cdn.example.com"));
assert!(!allowlist.is_allowed("https://other.example.com"));
}
#[test]
fn test_port_matching() {
let allowlist = NetworkAllowlist::new().allow("https://example.com:8443");
assert!(allowlist.is_allowed("https://example.com:8443"));
assert!(!allowlist.is_allowed("https://example.com")); assert!(!allowlist.is_allowed("https://example.com:443"));
}
#[test]
fn test_scheme_matching() {
let allowlist = NetworkAllowlist::new().allow("https://example.com");
assert!(allowlist.is_allowed("https://example.com"));
assert!(!allowlist.is_allowed("http://example.com"));
}
}
mod curl_flags {
use super::*;
#[tokio::test]
async fn curl_compressed_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl --compressed https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_user_auth_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -u user:pass https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl --user admin:secret https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_user_agent_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -A 'Mozilla/5.0' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl --user-agent 'CustomAgent/1.0' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_referer_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -e 'https://example.com' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_verbose_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash.exec("curl -v https://blocked.com 2>&1").await.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_max_time_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -m 10 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl --max-time 30 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_max_time_various_values() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -m 1 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl -m 300 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl -m 0 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.exit_code != 0);
}
#[tokio::test]
async fn curl_max_time_with_other_flags() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -s -m 5 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl -m 5 -d 'data=test' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl -m 5 -H 'X-Custom: value' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_timeout_error_exit_code() {
let allowlist = NetworkAllowlist::new().allow("https://example.com");
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -m 5 https://notinallowlist.com 2>&1; echo \"exit:$?\"")
.await
.unwrap();
assert!(result.stdout.contains("exit:7") || result.stdout.contains("access denied"));
}
#[tokio::test]
async fn curl_connect_timeout_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl --connect-timeout 5 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl --connect-timeout 30 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_both_timeout_flags() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -m 30 --connect-timeout 5 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl --connect-timeout 60 -m 10 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_timeout_safety_limits() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -m 999999 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl -m 0 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("curl --connect-timeout 999999 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn curl_combined_flags() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl -s --compressed -u user:pass -A 'Agent' -e 'http://ref.com' -m 30 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
}
mod wget_flags {
use super::*;
#[tokio::test]
async fn wget_header_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget --header 'X-Custom: value' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_user_agent_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -U 'CustomAgent' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("wget --user-agent 'Mozilla/5.0' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_post_data_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget --post-data 'key=value' https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_tries_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -t 3 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("wget --tries 5 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_combined_flags() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -q --header 'Accept: application/json' -U 'Agent' -t 3 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_timeout_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -T 10 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("wget --timeout 30 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_connect_timeout_flag() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget --connect-timeout 5 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_both_timeout_flags() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -T 30 --connect-timeout 5 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
#[tokio::test]
async fn wget_timeout_safety_limits() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("wget -T 999999 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
let result = bash
.exec("wget -T 0 https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
}
mod decompression_security {
use super::*;
#[tokio::test]
async fn compressed_respects_allowlist() {
let allowlist = NetworkAllowlist::new();
let mut bash = Bash::builder().network(allowlist).build();
let result = bash
.exec("curl --compressed https://blocked.com 2>&1")
.await
.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
}
mod custom_handler {
use super::*;
use bashkit::{HttpHandler, HttpResponse as Response};
use std::sync::{Arc, Mutex};
struct MockHandler;
#[async_trait::async_trait]
impl HttpHandler for MockHandler {
async fn request(
&self,
_method: &str,
_url: &str,
_body: Option<&[u8]>,
_headers: &[(String, String)],
) -> std::result::Result<Response, String> {
Ok(Response {
status: 200,
headers: vec![("content-type".to_string(), "text/plain".to_string())],
body: b"mocked-response".to_vec(),
})
}
}
#[tokio::test]
async fn custom_handler_intercepts_requests() {
let allowlist = NetworkAllowlist::allow_all();
let mut bash = Bash::builder()
.network(allowlist)
.http_handler(Box::new(MockHandler))
.build();
let result = bash.exec("curl -s https://example.com").await.unwrap();
assert_eq!(result.stdout.trim(), "mocked-response");
}
struct HeaderCaptureHandler {
headers: Arc<Mutex<Vec<(String, String)>>>,
}
#[async_trait::async_trait]
impl HttpHandler for HeaderCaptureHandler {
async fn request(
&self,
_method: &str,
_url: &str,
_body: Option<&[u8]>,
headers: &[(String, String)],
) -> std::result::Result<Response, String> {
*self.headers.lock().unwrap() = headers.to_vec();
Ok(Response {
status: 200,
headers: vec![("content-type".to_string(), "text/plain".to_string())],
body: b"mocked-response".to_vec(),
})
}
}
fn captured_user_agent(headers: &Arc<Mutex<Vec<(String, String)>>>) -> Option<String> {
headers
.lock()
.unwrap()
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case("user-agent"))
.map(|(_, value)| value.clone())
}
#[tokio::test]
async fn curl_sends_curl_user_agent_by_default() {
let headers = Arc::new(Mutex::new(Vec::new()));
let mut bash = Bash::builder()
.network(NetworkAllowlist::allow_all())
.http_handler(Box::new(HeaderCaptureHandler {
headers: headers.clone(),
}))
.build();
let result = bash.exec("curl -s https://example.com").await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(captured_user_agent(&headers).as_deref(), Some("curl/8.7.1"));
}
#[tokio::test]
async fn curl_user_agent_flags_override_default() {
let headers = Arc::new(Mutex::new(Vec::new()));
let mut bash = Bash::builder()
.network(NetworkAllowlist::allow_all())
.http_handler(Box::new(HeaderCaptureHandler {
headers: headers.clone(),
}))
.build();
let result = bash
.exec("curl -s -A 'CustomAgent/1.0' https://example.com")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(
captured_user_agent(&headers).as_deref(),
Some("CustomAgent/1.0")
);
}
#[tokio::test]
async fn curl_user_agent_header_overrides_default() {
let headers = Arc::new(Mutex::new(Vec::new()));
let mut bash = Bash::builder()
.network(NetworkAllowlist::allow_all())
.http_handler(Box::new(HeaderCaptureHandler {
headers: headers.clone(),
}))
.build();
let result = bash
.exec("curl -s -H 'User-Agent: HeaderAgent/1.0' https://example.com")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(
captured_user_agent(&headers).as_deref(),
Some("HeaderAgent/1.0")
);
}
#[tokio::test]
async fn custom_handler_allowlist_still_enforced() {
let allowlist = NetworkAllowlist::new(); let mut bash = Bash::builder()
.network(allowlist)
.http_handler(Box::new(MockHandler))
.build();
let result = bash.exec("curl -s https://example.com 2>&1").await.unwrap();
assert!(result.stdout.contains("access denied") || result.stderr.contains("access denied"));
}
}