use async_trait::async_trait;
use flowsdk::mqtt_client::client::ConnectionResult;
use flowsdk::mqtt_client::opts::MqttClientOptions;
use flowsdk::mqtt_client::tokio_async_client::{
TokioAsyncClientConfig, TokioAsyncMqttClient, TokioMqttEventHandler,
};
use flowsdk::mqtt_client::MqttClientError;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Clone)]
struct TestEventHandler {
connected_count: Arc<Mutex<u32>>,
}
impl TestEventHandler {
fn new() -> Self {
Self {
connected_count: Arc::new(Mutex::new(0)),
}
}
fn get_connected_count(&self) -> u32 {
*self.connected_count.lock().unwrap()
}
}
#[async_trait]
impl TokioMqttEventHandler for TestEventHandler {
async fn on_connected(&mut self, _result: &ConnectionResult) {
let mut count = self.connected_count.lock().unwrap();
*count += 1;
}
}
#[tokio::test]
async fn test_connect_sync_with_default_timeout() {
let config = TokioAsyncClientConfig::builder()
.connect_timeout_ms(100) .build();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-connect-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
let start = std::time::Instant::now();
let result = client.connect_sync().await;
let duration = start.elapsed();
match result {
Ok(_) => {
assert!(
duration < Duration::from_millis(150),
"Connect should complete within timeout + margin"
);
}
Err(MqttClientError::OperationTimeout {
operation,
timeout_ms,
}) => {
assert_eq!(operation, "connect");
assert_eq!(timeout_ms, 100);
assert!(
duration >= Duration::from_millis(95)
&& duration <= Duration::from_millis(150),
"Timeout should occur around the configured duration, got {:?}",
duration
);
}
Err(e) => {
println!("Connect failed with non-timeout error: {}", e);
}
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_connect_sync_with_custom_timeout() {
let config = TokioAsyncClientConfig::default();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-connect-custom-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
let start = std::time::Instant::now();
let result = client.connect_sync_with_timeout(50).await;
let duration = start.elapsed();
match result {
Ok(_) => {
assert!(
duration < Duration::from_millis(100),
"Connect should complete within custom timeout + margin"
);
}
Err(MqttClientError::OperationTimeout {
operation,
timeout_ms,
}) => {
assert_eq!(operation, "connect");
assert_eq!(timeout_ms, 50);
assert!(
duration >= Duration::from_millis(45)
&& duration <= Duration::from_millis(100),
"Custom timeout should occur around 50ms, got {:?}",
duration
);
}
Err(e) => {
println!("Connect failed with non-timeout error: {}", e);
}
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_successful_operation_within_timeout() {
let config = TokioAsyncClientConfig::builder()
.connect_timeout_ms(5000) .build();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-success-within-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler.clone()), config).await {
Ok(client) => {
let start = std::time::Instant::now();
match client.connect_sync().await {
Ok(result) => {
let duration = start.elapsed();
assert!(
duration < Duration::from_secs(5),
"Successful connect should not take full timeout duration"
);
println!("✅ Connected successfully in {:?}", duration);
println!(" Reason code: {}", result.reason_code);
println!(" Session present: {}", result.session_present);
assert_eq!(handler.get_connected_count(), 1);
let _ = client.disconnect().await;
}
Err(e) => {
println!("Connect failed (broker may not be running): {}", e);
}
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_timeout_error_conversion_to_io_error() {
let config = TokioAsyncClientConfig::builder()
.connect_timeout_ms(100) .build();
let options = MqttClientOptions::builder()
.peer("192.0.2.1:1883") .client_id("test-timeout-conversion")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
let result: Result<_, std::io::Error> = client.connect_sync().await.map_err(Into::into);
match result {
Ok(_) => {
println!("Unexpectedly connected");
}
Err(io_err) => {
println!("Got io::Error: {:?} - {}", io_err.kind(), io_err);
if io_err.to_string().contains("timed out") {
assert_eq!(io_err.kind(), std::io::ErrorKind::TimedOut);
}
}
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_publish_sync_with_timeout() {
let config = TokioAsyncClientConfig::default();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-publish-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
if client.connect_sync().await.is_ok() {
let result = client
.publish_sync_with_timeout("test/topic", b"test payload", 1, false, 5000)
.await;
match result {
Ok(pub_result) => {
println!("✅ Published successfully");
println!(" Packet ID: {:?}", pub_result.packet_id);
println!(" Reason code: {:?}", pub_result.reason_code);
}
Err(MqttClientError::OperationTimeout {
operation,
timeout_ms,
}) => {
assert_eq!(operation, "publish");
assert_eq!(timeout_ms, 5000);
println!("⏱️ Publish timed out as expected");
}
Err(e) => {
println!("Publish failed: {}", e);
}
}
let _ = client.disconnect().await;
} else {
println!("Could not connect (broker may not be running)");
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_subscribe_sync_with_timeout() {
let config = TokioAsyncClientConfig::default();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-subscribe-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
if client.connect_sync().await.is_ok() {
let result = client
.subscribe_sync_with_timeout("test/topic/#", 1, 3000)
.await;
match result {
Ok(sub_result) => {
println!("✅ Subscribed successfully");
println!(" Packet ID: {:?}", sub_result.packet_id);
println!(" Reason codes: {:?}", sub_result.reason_codes);
}
Err(MqttClientError::OperationTimeout {
operation,
timeout_ms,
}) => {
assert_eq!(operation, "subscribe");
assert_eq!(timeout_ms, 3000);
println!("⏱️ Subscribe timed out as expected");
}
Err(e) => {
println!("Subscribe failed: {}", e);
}
}
let _ = client.disconnect().await;
} else {
println!("Could not connect (broker may not be running)");
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_ping_sync_with_timeout() {
let config = TokioAsyncClientConfig::default();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-ping-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
if client.connect_sync().await.is_ok() {
let start = std::time::Instant::now();
let result = client.ping_sync_with_timeout(2000).await;
let duration = start.elapsed();
match result {
Ok(_) => {
println!("✅ Ping succeeded in {:?}", duration);
assert!(duration < Duration::from_secs(2));
}
Err(MqttClientError::OperationTimeout {
operation,
timeout_ms,
}) => {
assert_eq!(operation, "ping");
assert_eq!(timeout_ms, 2000);
println!("⏱️ Ping timed out as expected");
}
Err(e) => {
println!("Ping failed: {}", e);
}
}
let _ = client.disconnect().await;
} else {
println!("Could not connect (broker may not be running)");
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}
#[tokio::test]
async fn test_none_timeout_means_no_timeout() {
let config = TokioAsyncClientConfig::builder()
.no_connect_timeout() .build();
let options = MqttClientOptions::builder()
.peer("127.0.0.1:1883")
.client_id("test-no-timeout")
.clean_start(true)
.build();
let handler = TestEventHandler::new();
match TokioAsyncMqttClient::new(options, Box::new(handler), config).await {
Ok(client) => {
let result = tokio::time::timeout(Duration::from_secs(10), client.connect_sync()).await;
match result {
Ok(Ok(_)) => {
println!("✅ Connected successfully with no timeout configured");
let _ = client.disconnect().await;
}
Ok(Err(e)) => {
match e {
MqttClientError::OperationTimeout { .. } => {
panic!("Should not get OperationTimeout when timeout is None");
}
_ => {
println!("Connect failed with non-timeout error: {}", e);
}
}
}
Err(_) => {
println!("Test timeout expired (expected with None timeout config)");
}
}
}
Err(e) => {
println!("Failed to create client: {}", e);
}
}
}