netspeed-cli 0.10.1

Command-line interface for testing internet bandwidth using speedtest.net
Documentation
//! Integration tests for TLS CLI options.
//!
//! Tests cover:
//! - `--ca-cert` path validation and file format handling
//! - `--pin-certs` flag acceptance
//! - `--tls-version` parsing (1.2, 1.3)
//! - Combination of TLS options with other CLI flags
//! - Error cases and warnings

use std::fs;
use std::process::Command;

/// Helper to create a unique temp certificate file path.
fn temp_cert_path() -> std::path::PathBuf {
    let pid = std::process::id();
    std::env::temp_dir().join(format!("netspeed_test_cert_{}.pem", pid))
}

/// Returns a path that should not exist (cross-platform).
fn nonexistent_path() -> std::path::PathBuf {
    let temp = std::env::temp_dir();
    temp.join(format!("netspeed_nonexistent_{}.pem", std::process::id()))
}

/// Returns a path to an existing directory (cross-platform).
fn existing_directory_path() -> std::path::PathBuf {
    std::env::temp_dir()
}

/// Helper to create a test certificate file.
fn create_test_cert(path: &std::path::Path) {
    let cert_content = "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKJ8h5L7V3R2MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp\nc2NvMRUwEwYDVQQKDAxUZXN0IERlbW8gQ0EwHhcNMjMwMTAxMDAwMDAwWhcNMjQw\nMTAxMDAwMDAwWjBFMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEW\nMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMGA1UECgwMVGVzdCBEZW1vIENBMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw1s3xfn8Z8c3R1hL+8jK2w0F\nkZmJkZnJmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZm\nZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZm\nZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZm\nAgMBAAGjUzBRMB0GA1UdDgQWBBQYT9W7dF2R2P7L5D3K9Z2Y5Q3Z8DAfBgNVHSME\nGDAWgBQYT9W7dF2R2P7L5D3K9Z2Y5Q3Z8DAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCQ4e1H8gZ+8f3F5N3F6hK7L5J2N4L9K8Q0L1M2N3O4P5Q6\nR7S8T9U0V1W2X3Y4Z5A6B7C8D9E0F1G2H3I4J5K6L7M8N9O0P1Q2R3S4T5U6V7W8\n-----END CERTIFICATE-----".as_bytes();
    fs::write(path, cert_content).expect("Failed to write test cert");
}

// ── Help Documentation Tests ─────────────────────────────────────────

#[test]
fn test_ca_cert_in_help() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--help"])
        .output()
        .expect("Failed to execute command");
    let combined = String::from_utf8_lossy(&output.stdout).to_string()
        + &String::from_utf8_lossy(&output.stderr);
    assert!(
        combined.contains("--ca-cert"),
        "--ca-cert should be documented in help"
    );
}

#[test]
fn test_pin_certs_in_help() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--help"])
        .output()
        .expect("Failed to execute command");
    let combined = String::from_utf8_lossy(&output.stdout).to_string()
        + &String::from_utf8_lossy(&output.stderr);
    assert!(
        combined.contains("--pin-certs"),
        "--pin-certs should be documented in help"
    );
}

#[test]
fn test_tls_version_in_help() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--help"])
        .output()
        .expect("Failed to execute command");
    let combined = String::from_utf8_lossy(&output.stdout).to_string()
        + &String::from_utf8_lossy(&output.stderr);
    assert!(
        combined.contains("--tls-version"),
        "--tls-version should be documented in help"
    );
}

// ── Path Validation Tests ────────────────────────────────────────────

#[test]
fn test_ca_cert_accepts_valid_pem_file() {
    let cert_path = temp_cert_path();
    create_test_cert(&cert_path);
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            cert_path.to_str().unwrap(),
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("does not exist") && !stderr.contains("is a directory"),
        "Valid cert path should be accepted. stderr: {stderr}"
    );
    fs::remove_file(&cert_path).ok();
}

#[test]
fn test_ca_cert_rejects_nonexistent_path() {
    let cert_path = nonexistent_path();
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            cert_path.to_str().unwrap(),
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !output.status.success(),
        "Non-existent CA cert path should be rejected"
    );
    assert!(
        stderr.contains("not found")
            || stderr.contains("does not exist")
            || stderr.contains("No such file"),
        "Error should mention file not found. stderr: {stderr}"
    );
}

#[test]
fn test_ca_cert_rejects_directory() {
    let dir_path = existing_directory_path();
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            dir_path.to_str().unwrap(),
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !output.status.success(),
        "Directory path for --ca-cert should be rejected"
    );
    assert!(
        stderr.contains("not a file") || stderr.contains("is a directory"),
        "Error should mention it's a directory. stderr: {stderr}"
    );
}

// ── TLS Version Tests ────────────────────────────────────────────────

#[test]
fn test_tls_version_rejects_invalid() {
    for version in ["2.0", "1.1", "3.0", "TLSv1.2"] {
        let output = Command::new("cargo")
            .args(["run", "--quiet", "--", "--tls-version", version])
            .output()
            .expect("Failed to execute command");
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(
            !output.status.success(),
            "Invalid TLS version '{}' should be rejected",
            version
        );
        assert!(
            stderr.contains("1.2") && stderr.contains("1.3"),
            "Error should mention valid TLS versions. stderr: {stderr}"
        );
    }
}

#[test]
fn test_tls_version_accepts_1_2() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--tls-version", "1.2"])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: invalid value"),
        "Valid TLS version 1.2 should be accepted. stderr: {stderr}"
    );
}

#[test]
fn test_tls_version_accepts_1_3() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--tls-version", "1.3"])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: invalid value"),
        "Valid TLS version 1.3 should be accepted. stderr: {stderr}"
    );
}

// ── Pin Certs Tests ──────────────────────────────────────────────────

#[test]
fn test_pin_certs_flag_accepted() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--pin-certs"])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: unexpected argument"),
        "--pin-certs should be accepted. stderr: {stderr}"
    );
}

#[test]
fn test_pin_certs_combined_with_json() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--pin-certs", "--json"])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: unexpected argument"),
        "--pin-certs with --json should parse successfully. stderr: {stderr}"
    );
}

#[test]
fn test_pin_certs_with_format_dashboard() {
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--pin-certs",
            "--format",
            "dashboard",
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: unexpected argument"),
        "--pin-certs with --format dashboard should parse successfully. stderr: {stderr}"
    );
}

// ── Combination Tests ────────────────────────────────────────────────

#[test]
fn test_ca_cert_combined_with_tls_version() {
    let cert_path = temp_cert_path();
    create_test_cert(&cert_path);
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            cert_path.to_str().unwrap(),
            "--tls-version",
            "1.3",
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: unexpected argument"),
        "--ca-cert combined with --tls-version should parse successfully. stderr: {stderr}"
    );
    fs::remove_file(&cert_path).ok();
}

#[test]
fn test_ca_cert_and_pin_certs_mutually_exclusive() {
    // Using both --ca-cert and --pin-certs together should fail
    // as they are mutually exclusive options
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            "/some/path.pem",
            "--pin-certs",
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    // The CLI should reject this combination - either with error about conflict
    // or with guidance about mutual exclusivity
    assert!(
        !output.status.success(),
        "Using both --ca-cert and --pin-certs should fail"
    );
    // Check that the error mentions one of these options
    assert!(
        stderr.contains("--ca-cert") || stderr.contains("--pin-certs"),
        "Error should mention the conflicting options. stderr: {stderr}"
    );
}

#[test]
fn test_all_tls_options_together() {
    let cert_path = temp_cert_path();
    create_test_cert(&cert_path);
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            cert_path.to_str().unwrap(),
            "--tls-version",
            "1.2",
            "--pin-certs",
            "--json",
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: unexpected argument"),
        "All TLS options combined should parse successfully. stderr: {stderr}"
    );
    fs::remove_file(&cert_path).ok();
}

#[test]
fn test_tls_options_with_other_flags() {
    let cert_path = temp_cert_path();
    create_test_cert(&cert_path);
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            cert_path.to_str().unwrap(),
            "--no-download",
            "--json",
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("error: unexpected argument"),
        "TLS options with --no-download should parse successfully. stderr: {stderr}"
    );
    fs::remove_file(&cert_path).ok();
}

// ── Error Message Tests ──────────────────────────────────────────────

#[test]
fn test_ca_cert_error_message_format() {
    let cert_path = nonexistent_path();
    let output = Command::new("cargo")
        .args([
            "run",
            "--quiet",
            "--",
            "--ca-cert",
            cert_path.to_str().unwrap(),
        ])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Error") || stderr.contains("error") || stderr.contains("invalid"),
        "Expected user-friendly error message, got: {stderr}"
    );
}

#[test]
fn test_tls_version_error_lists_valid_options() {
    let output = Command::new("cargo")
        .args(["run", "--quiet", "--", "--tls-version", "2.0"])
        .output()
        .expect("Failed to execute command");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("1.2") && stderr.contains("1.3"),
        "Error should list valid TLS versions. stderr: {stderr}"
    );
}