bacnet-cli 0.14.0

BACnet command-line tool for device discovery, reading, and writing
//! Consumer-level discovery and feature-disabled compatibility checks.
#[allow(dead_code)]
mod support;
// Only the feature-disabled checks below use `failure`.
#[cfg(any(not(feature = "sc-tls"), not(feature = "pcap")))]
use support::failure;
use support::run;

#[tokio::test]
async fn help_and_version_do_not_load_sc_files() {
    for option in ["--help", "--version"] {
        for args in [
            vec![option],
            vec![
                "--sc",
                "--sc-ca",
                "/nonexistent/site é.pem",
                "--sc-cert",
                "/nonexistent/client.pem",
                "--sc-key",
                "/nonexistent/client.key",
                option,
            ],
            vec!["--sc", "--sc-ca=", option],
        ] {
            let output = run(args).await;
            assert!(
                output.status.success(),
                "{}",
                String::from_utf8_lossy(&output.stderr)
            );
            assert!(output.stderr.is_empty());
            let text = String::from_utf8(output.stdout).unwrap();
            if option == "--help" {
                assert!(text.contains("--sc-ca <FILE>"));
            } else {
                assert_eq!(text.trim(), concat!("bacnet ", env!("CARGO_PKG_VERSION")));
            }
        }
    }
    let output = run(["read", "--sc-ca", "/nonexistent/é.pem", "--help"]).await;
    assert!(output.status.success());
    assert!(output.stderr.is_empty());
}

#[cfg(not(feature = "sc-tls"))]
#[tokio::test]
async fn disabled_sc_preserves_rebuild_advice_without_loading_files() {
    for ca in [None, Some(""), Some("/nonexistent/site é.pem")] {
        let mut args = vec![
            "--sc",
            "--json",
            "read",
            "02:00:00:00:00:01",
            "dev:123",
            "on",
        ];
        if let Some(ca) = ca {
            args.extend(["--sc-ca", ca]);
        }
        let output = run(args).await;
        failure(&output, "cargo install bacnet-cli --features sc-tls");
        assert_eq!(output.status.code(), Some(1));
    }
}

#[cfg(not(feature = "pcap"))]
#[tokio::test]
async fn capture_without_client_preserves_its_feature_advice() {
    let output = run(["--sc", "--sc-ca", "/nonexistent/é.pem", "capture"]).await;
    failure(&output, "cargo install bacnet-cli --features pcap");
}

#[tokio::test]
async fn bip_ignores_unused_sc_ca_and_credentials() {
    // Unicast only to an owned loopback socket, never network device discovery.
    let socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
    let target = socket.local_addr().unwrap().to_string();
    let output = run([
        "--interface",
        "127.0.0.1",
        "--port",
        "0",
        "--json",
        "--sc-ca",
        "/nonexistent/site é.pem",
        "--sc-cert",
        "/nonexistent/cert",
        "--sc-key",
        "/nonexistent/key",
        "discover",
        "--wait",
        "0",
        "--target",
        &target,
    ])
    .await;
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(value, serde_json::json!([]));
}

#[tokio::test]
async fn ipv6_does_not_require_sc_files() {
    use bacnet_transport::port::TransportPort;
    // A bare B/IPv6 node on loopback: it answers the address resolution of
    // its VMAC but sends no I-Am, so the directed discover finds nothing.
    let mut node =
        bacnet_transport::bip6::Bip6Transport::new(std::net::Ipv6Addr::LOCALHOST, 0, Some(1234));
    let _rx = node.start().await.unwrap();
    let target = format!("[::1]:{}", node.local_address().unwrap().port());
    let output = run([
        "--ipv6",
        "--ipv6-interface",
        "::1",
        "--port",
        "0",
        "--device-instance",
        "123",
        "--json",
        "--sc-ca",
        "/nonexistent/site.pem",
        "discover",
        "--wait",
        "0",
        "--target",
        &target,
    ])
    .await;
    node.stop().await.unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(value, serde_json::json!([]));
}