use interactsh::{ClientConfig, InteractshClient, InteractionContext, Error, ConfigProblem, ConfigField};
use std::sync::Arc;
#[tokio::test]
async fn test_config_empty_server() {
let mut config = ClientConfig::default();
config.server = "".to_string();
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::Empty, field: ConfigField::Server })));
}
#[tokio::test]
async fn test_config_whitespace_server() {
let mut config = ClientConfig::default();
config.server = " ".to_string();
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::Empty, field: ConfigField::Server })));
}
#[tokio::test]
async fn test_config_zero_correlation_length() {
let mut config = ClientConfig::default();
config.correlation_id_length = 0;
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::MustBeGreaterThanZero, field: ConfigField::CorrelationIdLength })));
}
#[tokio::test]
async fn test_config_zero_nonce_length() {
let mut config = ClientConfig::default();
config.nonce_length = 0;
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::MustBeGreaterThanZero, field: ConfigField::NonceLength })));
}
#[tokio::test]
async fn test_config_zero_poll_response_bytes() {
let mut config = ClientConfig::default();
config.max_poll_response_bytes = 0;
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::MustBeGreaterThanZero, field: ConfigField::MaxPollResponseBytes })));
}
#[tokio::test]
async fn test_config_empty_auth_header() {
let mut config = ClientConfig::default();
config.authorization_header = "".to_string();
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::Empty, field: ConfigField::AuthorizationHeader })));
}
#[tokio::test]
async fn test_config_empty_scheme() {
let mut config = ClientConfig::default();
config.default_scheme = "".to_string();
let res = config.validate();
assert!(matches!(res, Err(Error::InvalidConfig { problem: ConfigProblem::Empty, field: ConfigField::DefaultScheme })));
}
#[tokio::test]
async fn test_interaction_context_empty_label() {
let context = InteractionContext::new("");
assert_eq!(context.label, "");
}
#[tokio::test]
async fn test_config_server_null_byte() {
let mut config = ClientConfig::default();
config.server = "oast.pro\0".to_string();
assert!(config.validate().is_ok());
let res_client = InteractshClient::new(config).await;
assert!(res_client.is_err());
}
#[tokio::test]
async fn test_context_label_null_byte() {
let context = InteractionContext::new("null\0byte");
assert_eq!(context.label, "null\0byte");
}
#[tokio::test]
async fn test_context_attr_null_byte() {
let context = InteractionContext::new("test").with_attribute("key\0", "val\0");
assert_eq!(context.attributes.get("key\0").unwrap(), "val\0");
}
#[tokio::test]
async fn test_config_max_correlation_length() {
let mut config = ClientConfig::default();
config.correlation_id_length = usize::MAX;
let result = std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(InteractshClient::new(config)).unwrap_err();
}).join();
assert!(result.is_ok(), "Client creation with usize::MAX correlation_id_length should gracefully error");
}
#[tokio::test]
async fn test_config_max_nonce_length() {
let mut config = ClientConfig::default();
config.nonce_length = usize::MAX;
let result = std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(InteractshClient::new(config)).unwrap_err();
}).join();
assert!(result.is_ok(), "Client creation with usize::MAX nonce_length should gracefully error");
}
#[tokio::test]
async fn test_config_max_poll_response() {
let mut config = ClientConfig::default();
config.max_poll_response_bytes = usize::MAX;
assert!(config.validate().is_ok());
}
#[tokio::test]
async fn test_config_max_retries() {
let mut config = ClientConfig::default();
config.max_retries = usize::MAX;
assert!(config.validate().is_ok());
}
#[tokio::test]
async fn test_config_max_backoff() {
let mut config = ClientConfig::default();
config.retry_backoff_millis = u64::MAX;
assert!(config.validate().is_ok());
}
async fn mock_poll_server(response_body: String, chunked: bool) -> (u16, tokio::task::JoinHandle<()>) {
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let server_task = tokio::spawn(async move {
while let Ok((mut socket, _)) = listener.accept().await {
let mut buf = [0; 1024];
let _ = socket.read(&mut buf).await;
let req_str = String::from_utf8_lossy(&buf);
if req_str.contains("/register") {
let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}";
let _ = socket.write_all(response.as_bytes()).await;
} else if req_str.contains("/poll") {
let response = if chunked {
format!("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{:x}\r\n{}\r\n0\r\n\r\n", response_body.len(), response_body)
} else {
format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}", response_body.len(), response_body)
};
let _ = socket.write_all(response.as_bytes()).await;
}
}
});
(port, server_task)
}
#[tokio::test]
async fn test_massive_json_response_size_limit() {
let mut massive_json = String::from("{\"interactions\": [], \"data\": [");
for _i in 0..50000 {
massive_json.push_str("\"duplicate_data\",");
}
massive_json.push_str("\"end\"]}");
let (port, server_task) = mock_poll_server(massive_json, false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_poll_response_bytes = 1000;
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::OversizedResponse { .. })));
server_task.abort();
}
#[tokio::test]
async fn test_massive_json_response_size_limit_chunked() {
let mut massive_json = String::from("{\"interactions\": [], \"data\": [");
for _i in 0..50000 {
massive_json.push_str("\"duplicate_data\",");
}
massive_json.push_str("\"end\"]}");
let (port, server_task) = mock_poll_server(massive_json, true).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_poll_response_bytes = 1000;
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::OversizedResponse { .. })));
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
async fn test_concurrency_8_threads_hammer_cache() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = Arc::new(InteractshClient::new(config).await.unwrap());
let mut tasks = vec![];
for i in 0..8 {
let c = Arc::clone(&client);
tasks.push(tokio::spawn(async move {
for j in 0..1000 {
let url = c.generate_url(InteractionContext::new(format!("thread_{}_{}", i, j))).unwrap();
if j % 50 == 0 {
let _ = c.tracked_count().unwrap();
}
if j % 2 == 0 {
let forgotten = c.forget(&url.nonce).unwrap();
assert!(forgotten.is_some());
}
}
}));
}
for task in tasks {
task.await.unwrap();
}
assert_eq!(client.tracked_count().unwrap(), 4000);
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
async fn test_concurrency_tracked_count_under_load() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = Arc::new(InteractshClient::new(config).await.unwrap());
let mut tasks = vec![];
for _ in 0..8 {
let c = Arc::clone(&client);
tasks.push(tokio::spawn(async move {
for _ in 0..1000 {
let _ = c.tracked_count().unwrap();
}
}));
}
for task in tasks {
task.await.unwrap();
}
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
async fn test_concurrency_forget_nonexistent() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = Arc::new(InteractshClient::new(config).await.unwrap());
let mut tasks = vec![];
for i in 0..8 {
let c = Arc::clone(&client);
tasks.push(tokio::spawn(async move {
for j in 0..100 {
let forgotten = c.forget(&format!("fake_{}_{}", i, j)).unwrap();
assert!(forgotten.is_none());
}
}));
}
for task in tasks {
task.await.unwrap();
}
server_task.abort();
}
#[tokio::test]
async fn test_poll_response_truncated_json() {
let (port, server_task) = mock_poll_server("{\"interactions\": [], \"da".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::Parse { .. })));
server_task.abort();
}
#[tokio::test]
async fn test_poll_response_missing_aes_key() {
let body = "{\"interactions\": [], \"data\": [\"enc_data_here\"]}".to_string();
let (port, server_task) = mock_poll_server(body, false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::Crypto { .. })));
server_task.abort();
}
#[tokio::test]
async fn test_poll_response_invalid_aes_key() {
let body = "{\"interactions\": [], \"data\": [\"enc_data_here\"], \"aes_key\": \"not_base64!!!\"}".to_string();
let (port, server_task) = mock_poll_server(body, false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::Crypto { .. })));
server_task.abort();
}
#[tokio::test]
async fn test_poll_response_invalid_encrypted_data() {
let body = "{\"interactions\": [], \"data\": [\"not_base64!!!\"], \"aes_key\": \"AABBCCDD\"}".to_string();
let (port, server_task) = mock_poll_server(body, false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::Crypto { .. })));
server_task.abort();
}
#[tokio::test]
async fn test_poll_response_malformed_raw_interaction() {
let body = "{\"interactions\": [{\"full-id\": \"foo\"}]}".to_string(); let (port, server_task) = mock_poll_server(body, false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let poll_res = client.poll().await;
assert!(matches!(poll_res, Err(Error::Parse { .. })));
server_task.abort();
}
#[tokio::test]
async fn test_context_bom() {
let context = InteractionContext::new("\u{feff}label_with_bom");
assert_eq!(context.label, "\u{feff}label_with_bom");
}
#[tokio::test]
async fn test_context_fake_surrogate() {
let unprintable = String::from_utf8_lossy(&[0xed, 0xa0, 0x80]).to_string();
let context = InteractionContext::new("test").with_attribute("surrogate_replaced", unprintable);
let value = context.attributes.get("surrogate_replaced").unwrap();
assert_eq!(value.chars().count(), 3);
assert!(value.chars().all(|ch| ch == char::REPLACEMENT_CHARACTER));
}
#[tokio::test]
async fn test_context_emojis() {
let context = InteractionContext::new("test").with_attribute("weird", "🔥👩🚒🧯");
assert_eq!(context.attributes.get("weird").unwrap(), "🔥👩🚒🧯");
}
#[tokio::test]
async fn test_config_unicode_domain() {
let mut config = ClientConfig::default();
config.server = "http://öäü.com".to_string();
config.max_retries = 0;
let client_res = InteractshClient::new(config).await;
assert!(client_res.is_err());
}
#[tokio::test]
async fn test_context_duplicate_attribute_keys() {
let context = InteractionContext::new("test")
.with_attribute("dup", "val1")
.with_attribute("dup", "val2");
assert_eq!(context.attributes.get("dup").unwrap(), "val2");
}
#[tokio::test]
async fn test_context_duplicate_generation_nonces() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let url1 = client.generate_url(InteractionContext::new("1")).unwrap();
let url2 = client.generate_url(InteractionContext::new("2")).unwrap();
assert_ne!(url1.nonce, url2.nonce);
server_task.abort();
}
#[tokio::test]
async fn test_resource_100k_mappings() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
for i in 0..100_000 {
let context = InteractionContext::new(format!("item_{}", i));
client.generate_url(context).unwrap();
}
assert_eq!(client.tracked_count().unwrap(), 100_000);
server_task.abort();
}
#[tokio::test]
async fn test_resource_deep_attribute_value() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let context = InteractionContext::new("deep")
.with_attribute("payload", "A".repeat(10_000_000)); client.generate_url(context).unwrap();
assert_eq!(client.tracked_count().unwrap(), 1);
server_task.abort();
}
#[tokio::test]
async fn test_resource_many_attributes() {
let (port, server_task) = mock_poll_server("{}".to_string(), false).await;
let mut config = ClientConfig::default();
config.server = format!("http://127.0.0.1:{}", port);
config.max_retries = 0;
let client = InteractshClient::new(config).await.unwrap();
let mut context = InteractionContext::new("many");
for i in 0..10_000 {
context = context.with_attribute(format!("k_{}", i), format!("v_{}", i));
}
client.generate_url(context).unwrap();
assert_eq!(client.tracked_count().unwrap(), 1);
server_task.abort();
}