unifi-cli 0.2.2

CLI for UniFi Network controller
Documentation
use std::io::IsTerminal;
use unifi_cli::output::{OutputConfig, OutputFormat};

/// Parse the schema output from the binary and validate it against the clispec v0.2 JSON Schema.
#[test]
fn schema_validates_against_clispec_v02() {
    // Load the vendored clispec v0.2 JSON Schema
    let schema_path =
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/clispec-v0.2.json");
    let schema_str =
        std::fs::read_to_string(&schema_path).expect("failed to read clispec-v0.2.json fixture");
    let schema_value: serde_json::Value =
        serde_json::from_str(&schema_str).expect("failed to parse clispec schema");

    // Generate the unifi schema by calling print_schema via the binary
    let binary = env!("CARGO_BIN_EXE_unifi");
    let output = std::process::Command::new(binary)
        .arg("schema")
        .output()
        .expect("failed to run unifi schema");
    assert!(
        output.status.success(),
        "schema command failed: {:?}",
        output.status
    );

    let schema_output: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema output is not valid JSON");

    // Validate using jsonschema crate
    let validator = jsonschema::validator_for(&schema_value).expect("invalid clispec schema");
    let errors: Vec<String> = validator
        .iter_errors(&schema_output)
        .map(|e| format!("{e}"))
        .collect();
    if !errors.is_empty() {
        panic!("Schema validation failed:\n{}", errors.join("\n"));
    }
}

#[test]
fn explicit_text_format_is_not_json() {
    let out = OutputConfig::new(OutputFormat::Text, false);
    assert!(
        !out.is_json(),
        "OutputFormat::Text must never be JSON even when piped"
    );
}

#[test]
fn explicit_json_format_is_json() {
    let out = OutputConfig::new(OutputFormat::Json, false);
    assert!(out.is_json(), "OutputFormat::Json must always be JSON");
}

#[test]
fn auto_format_is_json_when_not_tty() {
    let out = OutputConfig::new(OutputFormat::Auto, false);
    let expected = !std::io::stdout().is_terminal();
    assert_eq!(out.is_json(), expected);
}

#[test]
fn error_envelope_last_line_is_json() {
    let kind = "auth_error";
    let message = "Authentication error: bad key";
    let expected = serde_json::json!({
        "error": {
            "kind": kind,
            "message": message,
        }
    });
    assert!(expected["error"]["kind"].as_str().is_some());
    assert_eq!(expected["error"]["kind"].as_str().unwrap(), kind);
    assert_eq!(expected["error"]["message"].as_str().unwrap(), message);
}

#[test]
fn confirmation_required_error_without_yes_and_no_tty() {
    let binary = env!("CARGO_BIN_EXE_unifi");
    let output = std::process::Command::new(binary)
        .args([
            "--host",
            "https://192.0.2.1",
            "--api-key",
            "fake-key",
            "clients",
            "block",
            "aa:bb:cc:dd:ee:ff",
        ])
        .stdin(std::fs::File::open("/dev/null").unwrap())
        .output()
        .expect("failed to run binary");

    assert_eq!(
        output.status.code(),
        Some(2),
        "expected exit code 2, got {:?}\nstderr: {}",
        output.status.code(),
        String::from_utf8_lossy(&output.stderr)
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    let last_line = stderr.trim_end().lines().last().unwrap_or("");
    let envelope: serde_json::Value =
        serde_json::from_str(last_line).expect("last stderr line must be valid JSON");
    assert_eq!(
        envelope["error"]["kind"].as_str(),
        Some("confirmation_required")
    );
}

#[test]
fn pagination_envelope_shape() {
    let items: Vec<serde_json::Value> = vec![];
    let envelope = serde_json::json!({
        "items": items,
        "total": 0usize,
        "limit": 100usize,
        "offset": 0usize,
    });
    assert!(envelope["items"].is_array());
    assert!(envelope["total"].is_number());
    assert!(envelope["limit"].is_number());
    assert!(envelope["offset"].is_number());
}