tcproxy 0.1.1

A TCP proxy for PostgreSQL connections with SSH tunnel support and runtime target switching
Documentation
use serial_test::serial;
use std::io::Write;
use std::process::Command;
use tempfile::{NamedTempFile, TempDir};

/// Test configuration file validation
#[test]
#[serial]
fn test_validate_config_command() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"
  max_connections: 1000

targets:
  local:
    host: "localhost"
    port: 5432

connection_management:
  health_check_interval_seconds: 30
  health_check_timeout_seconds: 5
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "validate-config",
        ])
        .output()
        .expect("Failed to execute command");

    assert!(output.status.success(), "Config validation should succeed");
}

/// Test invalid configuration file validation
#[test]
#[serial]
fn test_validate_invalid_config() {
    let invalid_config = r#"
proxy:
  listen_port: 0  # Invalid port
  listen_host: "127.0.0.1"

targets: {}  # Empty targets
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(invalid_config.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "validate-config",
        ])
        .output()
        .expect("Failed to execute command");

    assert!(
        !output.status.success(),
        "Invalid config validation should fail"
    );
}

/// Test configuration initialization
#[test]
#[serial]
fn test_init_config_command() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("test_config.yaml");

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "init-config",
            "--output",
            config_path.to_str().unwrap(),
        ])
        .output()
        .expect("Failed to execute command");

    assert!(
        output.status.success(),
        "Config initialization should succeed"
    );
    assert!(config_path.exists(), "Config file should be created");

    // Verify the created config is valid
    let content = std::fs::read_to_string(&config_path).unwrap();
    assert!(content.contains("proxy:"));
    assert!(content.contains("targets:"));
    assert!(content.contains("local:"));
}

/// Test showing configuration
#[test]
#[serial]
fn test_show_config_command() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"

targets:
  test:
    host: "test.example.com"
    port: 5432
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "show-config",
        ])
        .output()
        .expect("Failed to execute command");

    assert!(output.status.success(), "Show config should succeed");

    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("test.example.com"));
    assert!(stdout.contains("5433"));
}

/// Test listing targets
#[test]
#[serial]
fn test_list_targets_command() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"

targets:
  local:
    host: "localhost"
    port: 5432
  production:
    host: "prod.example.com"
    port: 5432
  development:
    host: "dev.example.com"
    port: 5432
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "list-targets",
        ])
        .output()
        .expect("Failed to execute command");

    assert!(output.status.success(), "List targets should succeed");

    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("local"));
    assert!(stdout.contains("production"));
    assert!(stdout.contains("development"));
}

/// Test health check command
#[test]
#[serial]
fn test_health_check_command() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"

targets:
  local:
    host: "localhost"
    port: 5432
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "health-check",
            "--target",
            "local",
        ])
        .output()
        .expect("Failed to execute command");

    // Health check may fail if PostgreSQL is not running, but command should execute
    let stderr = String::from_utf8(output.stderr).unwrap();
    assert!(stderr.contains("local") || output.status.success());
}

/// Test CLI argument parsing
#[test]
#[serial]
fn test_cli_help() {
    let output = Command::new("cargo")
        .args(&["run", "--", "--help"])
        .output()
        .expect("Failed to execute command");

    assert!(output.status.success(), "Help command should succeed");

    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("tcproxy"));
    assert!(stdout.contains("start"));
    assert!(stdout.contains("init-config"));
    assert!(stdout.contains("validate-config"));
}

/// Test version command
#[test]
#[serial]
fn test_version_command() {
    let output = Command::new("cargo")
        .args(&["run", "--", "--version"])
        .output()
        .expect("Failed to execute command");

    assert!(output.status.success(), "Version command should succeed");

    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("0.1.0"));
}

/// Test error handling for missing target
#[test]
#[serial]
fn test_start_without_target() {
    let output = Command::new("cargo")
        .args(&["run", "--", "start"])
        .output()
        .expect("Failed to execute command");

    assert!(!output.status.success(), "Start without target should fail");
}

/// Test error handling for nonexistent config file
#[test]
#[serial]
fn test_nonexistent_config_file() {
    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            "/nonexistent/config.yaml",
            "validate-config",
        ])
        .output()
        .expect("Failed to execute command");

    // The command should fail or produce an error message
    if output.status.success() {
        let stderr = String::from_utf8(output.stderr).unwrap();
        let stdout = String::from_utf8(output.stdout).unwrap();
        // Check if there's an error message about the missing file
        assert!(
            stderr.contains("No such file")
                || stderr.contains("not found")
                || stdout.contains("No such file")
                || stdout.contains("not found"),
            "Should indicate file not found"
        );
    } else {
        // Command failed as expected
        assert!(!output.status.success(), "Nonexistent config should fail");
    }
}

/// Test JSON logging format
#[test]
#[serial]
fn test_json_logging() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"

targets:
  local:
    host: "localhost"
    port: 5432
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "--json-logs",
            "show-config",
        ])
        .output()
        .expect("Failed to execute command");

    assert!(output.status.success(), "JSON logging should work");
}

/// Test different log levels
#[test]
#[serial]
fn test_log_levels() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"

targets:
  local:
    host: "localhost"
    port: 5432
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let log_levels = ["trace", "debug", "info", "warn", "error"];

    for level in log_levels {
        let output = Command::new("cargo")
            .args(&[
                "run",
                "--",
                "--config",
                temp_file.path().to_str().unwrap(),
                "--log-level",
                level,
                "validate-config",
            ])
            .output()
            .expect("Failed to execute command");

        assert!(output.status.success(), "Log level {} should work", level);
    }
}

/// Test invalid log level
#[test]
#[serial]
fn test_invalid_log_level() {
    let config_content = r#"
proxy:
  listen_port: 5433
  listen_host: "127.0.0.1"

targets:
  local:
    host: "localhost"
    port: 5432
"#;

    let mut temp_file = NamedTempFile::new().unwrap();
    temp_file.write_all(config_content.as_bytes()).unwrap();

    let output = Command::new("cargo")
        .args(&[
            "run",
            "--",
            "--config",
            temp_file.path().to_str().unwrap(),
            "--log-level",
            "invalid",
            "validate-config",
        ])
        .output()
        .expect("Failed to execute command");

    assert!(!output.status.success(), "Invalid log level should fail");
}