knust 0.1.2

Asynchronous KNX/IP library for Rust
Documentation
//! Integration tests for complete Knx workflows
//!
//! These tests verify end-to-end functionality including direct group
//! communication, multi-target coordination, and secure communication
//! workflows. There's no built-in device abstraction layer — see
//! `examples/custom_devices.rs` for a pattern to build one on top of
//! `send_telegram`/`read_group_value`.

use knust::protocol::telegram::{Direction, Priority, Telegram, TelegramType};
use knust::protocol::{Address, GroupAddress, IndividualAddress};
#[cfg(feature = "secure")]
use knust::security::SessionConfig;
#[cfg(feature = "secure")]
use knust::transport::SecurityConfig;
use knust::transport::{BackoffConfig, TcpConfig};
use knust::{ConnectionConfig, ConnectionType, Knx, KnxError};
use std::time::Duration;
use tokio::time::timeout;

fn write_telegram(
    source: IndividualAddress,
    destination: GroupAddress,
    payload: Vec<u8>,
) -> Telegram {
    Telegram {
        source,
        destination: Address::Group(destination),
        payload,
        priority: Priority::Normal,
        direction: Direction::Outgoing,
        telegram_type: TelegramType::GroupValueWrite,
        gateway_id: None,
        timestamp: std::time::SystemTime::now(),
    }
}

/// Test end-to-end group communication scenarios
#[tokio::test]
async fn test_end_to_end_group_communication() -> Result<(), KnxError> {
    let config = ConnectionConfig {
        connection_type: ConnectionType::Tunneling,
        gateway_ip: Some("127.0.0.1".parse().unwrap()),
        gateway_port: Some(3671),
        local_ip: Some("127.0.0.1".parse().unwrap()),
        individual_address: IndividualAddress::new(1, 1, 1),
        security: None,
        timeout_ms: 5000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
    };

    let knx = Knx::new(config).await?;
    let source_addr = IndividualAddress::new(1, 1, 240);

    // 1. Switch on the living room light's group address
    let result = timeout(
        Duration::from_secs(5),
        knx.send_telegram(&write_telegram(
            source_addr,
            GroupAddress::from_parts(1, 2, 1)?,
            vec![0x01],
        )),
    )
    .await;
    match result {
        Ok(Ok(())) => println!("✓ Living room light switch write sent successfully"),
        Ok(Err(e)) => println!("⚠ Living room light switch write failed (expected in test): {e}"),
        Err(_) => println!("⚠ Living room light switch write timed out (expected in test)"),
    }

    // 2. Set brightness
    let result = timeout(
        Duration::from_secs(5),
        knx.send_telegram(&write_telegram(
            source_addr,
            GroupAddress::from_parts(1, 2, 2)?,
            vec![128],
        )),
    )
    .await;
    match result {
        Ok(Ok(())) => println!("✓ Living room light brightness write sent successfully"),
        Ok(Err(e)) => {
            println!("⚠ Living room light brightness write failed (expected in test): {e}");
        }
        Err(_) => println!("⚠ Living room light brightness write timed out (expected in test)"),
    }

    // 3. Switch on the kitchen light's group address
    let result = timeout(
        Duration::from_secs(5),
        knx.send_telegram(&write_telegram(
            source_addr,
            GroupAddress::from_parts(1, 3, 1)?,
            vec![0x01],
        )),
    )
    .await;
    match result {
        Ok(Ok(())) => println!("✓ Kitchen light switch write sent successfully"),
        Ok(Err(e)) => println!("⚠ Kitchen light switch write failed (expected in test): {e}"),
        Err(_) => println!("⚠ Kitchen light switch write timed out (expected in test)"),
    }

    // 4. Read a sensor value
    let result = knx
        .read_group_value(GroupAddress::from_parts(2, 1, 1)?, Duration::from_secs(5))
        .await;
    match result {
        Ok(_) => println!("✓ Temperature sensor read successfully"),
        Err(e) => println!("⚠ Temperature sensor read failed (expected in test): {e}"),
    }

    println!("✓ End-to-end group communication test completed");
    Ok(())
}

/// Test multi-target coordination scenarios
#[tokio::test]
async fn test_multi_target_coordination() -> Result<(), KnxError> {
    let config = ConnectionConfig {
        connection_type: ConnectionType::Routing,
        timeout_ms: 5000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
        ..Default::default()
    };

    let knx = Knx::new(config).await?;
    let source_addr = IndividualAddress::new(1, 1, 240);

    let addresses = vec![
        GroupAddress::from_parts(1, 1, 1)?,
        GroupAddress::from_parts(1, 1, 2)?,
        GroupAddress::from_parts(1, 1, 3)?,
    ];

    // Coordinated control - switch all three group addresses on simultaneously
    let mut tasks = Vec::new();
    for addr in &addresses {
        let knx_clone = knx.clone();
        let addr = *addr;
        tasks.push(tokio::spawn(async move {
            timeout(
                Duration::from_secs(5),
                knx_clone.send_telegram(&write_telegram(source_addr, addr, vec![0x01])),
            )
            .await
        }));
    }

    for (i, task) in tasks.into_iter().enumerate() {
        match task.await {
            Ok(Ok(Ok(()))) => println!("✓ Group address {} switched on successfully", i + 1),
            Ok(Ok(Err(e))) => println!("⚠ Group address {} failed (expected): {}", i + 1, e),
            Ok(Err(_)) => println!("⚠ Group address {} timed out (expected)", i + 1),
            Err(e) => println!("⚠ Group address {} task failed: {}", i + 1, e),
        }
    }

    // Coordinated brightness control (different value per target)
    let brightness_levels = [64u8, 128, 192];
    let mut brightness_tasks = Vec::new();
    for (addr, &brightness) in addresses.iter().zip(brightness_levels.iter()) {
        let knx_clone = knx.clone();
        let addr = *addr;
        brightness_tasks.push(tokio::spawn(async move {
            timeout(
                Duration::from_secs(5),
                knx_clone.send_telegram(&write_telegram(source_addr, addr, vec![brightness])),
            )
            .await
        }));
    }

    for (i, task) in brightness_tasks.into_iter().enumerate() {
        match task.await {
            Ok(Ok(Ok(()))) => println!(
                "✓ Group address {} brightness set to {}",
                i + 1,
                brightness_levels[i]
            ),
            Ok(Ok(Err(e))) => println!(
                "⚠ Group address {} brightness failed (expected): {}",
                i + 1,
                e
            ),
            Ok(Err(_)) => println!("⚠ Group address {} brightness timed out (expected)", i + 1),
            Err(e) => println!("⚠ Group address {} brightness task failed: {}", i + 1, e),
        }
    }

    assert_eq!(addresses.len(), 3);
    println!("✓ Multi-target coordination test completed");
    Ok(())
}

/// Test secure communication workflows
#[cfg(feature = "secure")]
#[tokio::test]
async fn test_secure_communication_workflow() -> Result<(), KnxError> {
    // Create security configuration
    let _session_config = SessionConfig {
        user_id: 1,
        user_password: "test_password".to_string(),
        device_auth_password: Some("device_auth".to_string()),
        keepalive_interval: 60,
    };

    let security_config = SecurityConfig {
        device_auth_password: "device_auth".to_string(),
        user_password: Some("test_password".to_string()),
        keyring_path: None,
        session_timeout: 300,
    };

    let config = ConnectionConfig {
        connection_type: ConnectionType::SecureTunneling,
        gateway_ip: Some("127.0.0.1".parse().unwrap()),
        security: Some(security_config),
        timeout_ms: 10000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
        ..Default::default()
    };

    // Create Knx instance with security
    let knx = Knx::new(config).await?;
    let source_addr = IndividualAddress::new(1, 1, 240);
    let secure_addr = GroupAddress::from_parts(1, 5, 1)?;

    // Test secure operations
    let result = timeout(Duration::from_secs(10), async {
        knx.send_telegram(&write_telegram(source_addr, secure_addr, vec![0x01]))
            .await?;
        tokio::time::sleep(Duration::from_millis(100)).await;
        knx.send_telegram(&write_telegram(source_addr, secure_addr, vec![0x00]))
            .await?;
        Ok::<(), KnxError>(())
    })
    .await;

    match result {
        Ok(Ok(())) => println!("✓ Secure communication workflow completed successfully"),
        Ok(Err(e)) => println!("⚠ Secure communication failed (expected in test): {e}"),
        Err(_) => println!("⚠ Secure communication timed out (expected in test)"),
    }

    println!("✓ Secure communication workflow test completed");
    Ok(())
}

/// Test error handling and recovery scenarios
#[tokio::test]
async fn test_error_handling_and_recovery() -> Result<(), KnxError> {
    let config = ConnectionConfig {
        connection_type: ConnectionType::Tunneling,
        gateway_ip: Some("192.168.255.255".parse().unwrap()), // Invalid IP
        timeout_ms: 5000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
        ..Default::default()
    };

    // This should fail to connect
    let result = timeout(Duration::from_secs(5), Knx::new(config)).await;

    match result {
        Ok(Ok(_)) => println!("⚠ Unexpected success with invalid gateway"),
        Ok(Err(e)) => {
            println!("✓ Expected error with invalid gateway: {e}");

            // Verify error contains useful information
            let error_string = format!("{e}");
            assert!(
                error_string.contains("192.168.255.255")
                    || error_string.contains("connection")
                    || error_string.contains("transport")
            );
        }
        Err(_) => println!("✓ Connection attempt timed out as expected"),
    }

    // Test a telegram send without a connection
    let config = ConnectionConfig {
        connection_type: ConnectionType::Routing,
        timeout_ms: 2000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
        ..Default::default()
    };
    let knx = Knx::new(config).await?;
    let source_addr = IndividualAddress::new(1, 1, 240);
    let addr = GroupAddress::from_parts(1, 1, 1)?;

    let result = timeout(
        Duration::from_secs(2),
        knx.send_telegram(&write_telegram(source_addr, addr, vec![0x01])),
    )
    .await;
    match result {
        Ok(Ok(())) => println!("⚠ Unexpected success without connection"),
        Ok(Err(e)) => println!("✓ Expected error without connection: {e}"),
        Err(_) => println!("✓ Operation timed out as expected without connection"),
    }

    println!("✓ Error handling and recovery test completed");
    Ok(())
}

/// Test configuration validation
#[tokio::test]
async fn test_configuration_validation() {
    // Test invalid group address
    let result = GroupAddress::from_parts(32, 8, 255); // Invalid values (max values)
    assert!(result.is_err(), "Should reject invalid group address");

    // Test valid group address
    let result = GroupAddress::from_parts(1, 2, 3);
    assert!(result.is_ok(), "Should accept valid group address");

    // Test connection config validation
    let config = ConnectionConfig {
        connection_type: ConnectionType::Tunneling,
        gateway_ip: None, // Missing required gateway IP for tunneling
        timeout_ms: 2000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
        ..Default::default()
    };

    // This should be handled gracefully
    let result = timeout(Duration::from_secs(2), Knx::new(config)).await;
    match result {
        Ok(Ok(_)) => println!("⚠ Unexpected success with incomplete config"),
        Ok(Err(e)) => println!("✓ Expected error with incomplete config: {e}"),
        Err(_) => println!("✓ Configuration validation timed out as expected"),
    }

    println!("✓ Configuration validation test completed");
}

/// Test resource cleanup and memory management
#[tokio::test]
async fn test_resource_cleanup() -> Result<(), KnxError> {
    let config = ConnectionConfig {
        connection_type: ConnectionType::Routing,
        timeout_ms: 1000,
        auto_reconnect: false,
        reconnect_backoff: BackoffConfig::default(),
        tcp_config: TcpConfig::default(),
        ..Default::default()
    };

    // Create and destroy multiple Knx instances
    for i in 0u8..5 {
        let knx = Knx::new(config.clone()).await?;
        let source_addr = IndividualAddress::new(1, 1, 240);
        let addr = GroupAddress::from_parts(1, 1, i + 1)?;

        // Simulate some operations
        let _ = timeout(
            Duration::from_millis(100),
            knx.send_telegram(&write_telegram(source_addr, addr, vec![0x01])),
        )
        .await;

        // Knx should be dropped here and resources cleaned up
        drop(knx);

        println!("✓ Knx instance {} created and cleaned up", i + 1);
    }

    println!("✓ Resource cleanup test completed");
    Ok(())
}