#![cfg(feature = "integration_tests")]
mod common;
use common::compose;
use common::run_with_timeout;
use kafka_client::{
Client, ConsumerConfig, ProducerConfig, ProducerRecord, SaslMechanismType, admin::NewTopic,
};
use std::time::Duration;
async fn setup() {
compose::ensure(&compose::clusters::SASL).await;
}
fn sasl_bootstrap_addrs() -> Vec<String> {
let bootstrap = std::env::var("KAFKA_BOOTSTRAP_SASL")
.or_else(|_| std::env::var("KAFKA_BOOTSTRAP"))
.unwrap_or_else(|_| "127.0.0.1:9094".to_string());
bootstrap.split(',').map(|s| s.trim().to_string()).collect()
}
fn sasl_config_from_env() -> Option<(SaslMechanismType, String, String)> {
let mechanism = match std::env::var("SASL_MECHANISM").as_deref() {
Ok("PLAIN") => SaslMechanismType::Plain,
Ok("SCRAM-SHA-256") => SaslMechanismType::ScramSha256,
Ok("SCRAM-SHA-512") => SaslMechanismType::ScramSha512,
_ => return None,
};
let username = std::env::var("SASL_USERNAME").unwrap_or_else(|_| "admin".to_string());
let password = std::env::var("SASL_PASSWORD").unwrap_or_else(|_| "admin-secret".to_string());
Some((mechanism, username, password))
}
#[tokio::test]
async fn test_sasl_authentication() {
run_with_timeout(async {
setup().await;
let Some((mechanism, username, password)) = sasl_config_from_env() else {
eprintln!("SKIP: SASL_MECHANISM not set, skipping SASL auth test");
return;
};
let addrs = sasl_bootstrap_addrs();
println!(
"=== SASL Auth Test: mechanism={}, user={}, bootstrap={:?} ===",
mechanism.as_str(),
username,
addrs
);
let client = Client::builder(addrs.clone())
.with_client_id("sasl-auth-test")
.with_sasl(mechanism, &username, &password)
.with_metadata_ttl(Duration::from_secs(10))
.build()
.await
.expect("Failed to build Client with SASL auth");
client
.refresh_metadata()
.await
.expect("Failed to refresh metadata after SASL auth");
let brokers = client.metadata().get_all_brokers().await;
println!(" Metadata OK: {} broker(s) in cluster", brokers.len());
assert!(!brokers.is_empty(), "Expected at least 1 broker");
let topic = "sasl-auth-test-topic";
let result = client
.admin()
.create_topic(&NewTopic::new(topic, 1, 1))
.await
.unwrap();
assert!(
result.is_success() || result.already_exists(),
"Create topic failed: {:?}",
result.error_message
);
println!(" Topic '{}' created (rf=1)", topic);
common::wait_for_topic_ready(&client, topic, 1).await;
let producer = client.producer(ProducerConfig::new()).await;
for i in 0..5 {
let record = ProducerRecord::new(topic, bytes::Bytes::from(format!("sasl-msg-{}", i)));
producer
.send(record)
.await
.expect("Failed to produce message via SASL auth");
}
producer.flush().await.expect("Failed to flush producer");
println!(" Produced 5 messages via SASL auth");
let mut consumer = client.consumer(
ConsumerConfig::new()
.with_group_id("sasl-auth-test")
.with_earliest(),
);
consumer.subscribe(vec![topic.to_string()]).await.unwrap();
for i in 0..10 {
let assignment = consumer.group().assignment().await;
let has_partitions: usize = assignment.values().map(|v| v.len()).sum();
if has_partitions > 0 {
println!(" Consumer joined group after ~{}s", i + 1);
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
let mut all = Vec::new();
let deadline = std::time::Instant::now() + Duration::from_secs(15);
while all.len() < 5 && std::time::Instant::now() < deadline {
match consumer.poll_timeout(Duration::from_millis(3000)).await {
Ok(records) => all.extend(records),
Err(e) => eprintln!(" WARNING: Poll error: {}", e),
}
}
println!(" Consumed {}/5 messages via SASL auth", all.len());
assert!(
all.len() >= 5,
"Expected at least 5 messages, got {}",
all.len()
);
for r in &all {
let value = String::from_utf8_lossy(&r.value);
println!(" {}", value);
}
if let Err(e) = client.close().await {
eprintln!(" Close warning: {}", e);
}
println!("=== SASL Auth Test PASSED ===");
})
.await;
}
#[tokio::test]
async fn test_sasl_invalid_credentials_rejected() {
setup().await;
let Some((mechanism, _username, _password)) = sasl_config_from_env() else {
eprintln!("SKIP: SASL_MECHANISM not set, skipping invalid credential test");
return;
};
let addrs = sasl_bootstrap_addrs();
println!(
"=== SASL Invalid Credentials Test: mechanism={}, bootstrap={:?} ===",
mechanism.as_str(),
addrs
);
let result = Client::builder(addrs.clone())
.with_client_id("sasl-invalid-test")
.with_sasl(mechanism, "wrong_user", "wrong_password")
.with_metadata_ttl(Duration::from_secs(5))
.build()
.await;
match result {
Ok(_) => {
eprintln!(" NOTE: Build succeeded with invalid credentials, testing metadata...");
let client = result.unwrap();
let meta_result = client.refresh_metadata().await;
assert!(
meta_result.is_err(),
"Expected metadata refresh to fail with invalid SASL credentials"
);
eprintln!(
" Metadata refresh correctly rejected: {:?}",
meta_result.err().unwrap()
);
let _ = client.close().await;
}
Err(e) => {
println!(" Build correctly rejected: {}", e);
}
}
println!("=== SASL Invalid Credentials Test PASSED ===");
}