use serial_test::serial;
use std::io::Write;
use std::net::TcpStream;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use tempfile::NamedTempFile;
fn get_postgresql_port() -> u16 {
if TcpStream::connect_timeout(&"127.0.0.1:5434".parse().unwrap(), Duration::from_secs(2))
.is_ok()
{
5434
} else {
5432
}
}
fn is_postgresql_available() -> bool {
let port = get_postgresql_port();
TcpStream::connect_timeout(
&format!("127.0.0.1:{}", port).parse().unwrap(),
Duration::from_secs(2),
)
.is_ok()
}
#[test]
#[serial]
fn test_proxy_startup_local_target() {
if !is_postgresql_available() {
println!("Skipping test: PostgreSQL not available on localhost:5432");
return;
}
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 15433
listen_host: "127.0.0.1"
max_connections: 100
targets:
local:
host: "localhost"
port: {}
connection_management:
health_check_interval_seconds: 30
health_check_timeout_seconds: 5
"#,
postgresql_port
);
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_content.as_bytes()).unwrap();
let mut child = Command::new("cargo")
.args(&[
"run",
"--",
"--config",
temp_file.path().to_str().unwrap(),
"start",
"--target",
"local",
"--port",
"15433",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start proxy");
thread::sleep(Duration::from_secs(2));
let proxy_available =
TcpStream::connect_timeout(&"127.0.0.1:15433".parse().unwrap(), Duration::from_secs(2))
.is_ok();
let _ = child.kill();
let _ = child.wait();
assert!(proxy_available, "Proxy should be listening on port 15433");
}
#[test]
#[serial]
fn test_proxy_invalid_target() {
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 15434
listen_host: "127.0.0.1"
targets:
local:
host: "localhost"
port: {}
"#,
postgresql_port
);
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(),
"start",
"--target",
"nonexistent",
])
.output()
.expect("Failed to execute command");
assert!(!output.status.success(), "Should fail with invalid target");
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("not found") || stderr.contains("nonexistent"));
}
#[test]
#[serial]
fn test_proxy_port_override() {
if !is_postgresql_available() {
println!("Skipping test: PostgreSQL not available on localhost:5432");
return;
}
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 5433
listen_host: "127.0.0.1"
targets:
local:
host: "localhost"
port: {}
"#,
postgresql_port
);
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_content.as_bytes()).unwrap();
let mut child = Command::new("cargo")
.args(&[
"run",
"--",
"--config",
temp_file.path().to_str().unwrap(),
"start",
"--target",
"local",
"--port",
"15435",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start proxy");
thread::sleep(Duration::from_secs(2));
let proxy_available =
TcpStream::connect_timeout(&"127.0.0.1:15435".parse().unwrap(), Duration::from_secs(2))
.is_ok();
let _ = child.kill();
let _ = child.wait();
assert!(
proxy_available,
"Proxy should be listening on overridden port 15435"
);
}
#[test]
#[serial]
fn test_proxy_host_override() {
if !is_postgresql_available() {
println!("Skipping test: PostgreSQL not available on localhost:5432");
return;
}
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 15436
listen_host: "127.0.0.1"
targets:
local:
host: "localhost"
port: {}
"#,
postgresql_port
);
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_content.as_bytes()).unwrap();
let mut child = Command::new("cargo")
.args(&[
"run",
"--",
"--config",
temp_file.path().to_str().unwrap(),
"start",
"--target",
"local",
"--host",
"127.0.0.1",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start proxy");
thread::sleep(Duration::from_secs(2));
let proxy_available =
TcpStream::connect_timeout(&"127.0.0.1:15436".parse().unwrap(), Duration::from_secs(2))
.is_ok();
let _ = child.kill();
let _ = child.wait();
assert!(
proxy_available,
"Proxy should be listening with host override"
);
}
#[test]
#[serial]
fn test_health_check_postgresql() {
if !is_postgresql_available() {
println!("Skipping test: PostgreSQL not available on localhost:5432");
return;
}
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 5433
listen_host: "127.0.0.1"
targets:
local:
host: "localhost"
port: {}
connection_management:
health_check_timeout_seconds: 10
"#,
postgresql_port
);
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");
assert!(
output.status.success(),
"Health check should succeed with available PostgreSQL"
);
}
#[test]
#[serial]
fn test_health_check_unavailable_target() {
let config_content = r#"
proxy:
listen_port: 5433
listen_host: "127.0.0.1"
targets:
unavailable:
host: "192.0.2.1" # RFC5737 test address - should be unreachable
port: 5432
connection_management:
health_check_timeout_seconds: 2
"#;
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",
"unavailable",
])
.output()
.expect("Failed to execute command");
if output.status.success() {
let stderr = String::from_utf8(output.stderr).unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stderr.contains("failed")
|| stderr.contains("timeout")
|| stderr.contains("unreachable")
|| stderr.contains("Connection refused")
|| stderr.contains("No route to host")
|| stderr.contains("unhealthy")
|| stdout.contains("failed")
|| stdout.contains("timeout")
|| stdout.contains("unreachable")
|| stdout.contains("Connection refused")
|| stdout.contains("No route to host")
|| stdout.contains("unhealthy")
|| stdout.contains("connectivity not tested"),
"Should indicate connection failure or that connectivity wasn't tested. stdout: '{}', stderr: '{}'",
stdout,
stderr
);
} else {
assert!(
!output.status.success(),
"Health check should fail for unavailable target"
);
}
}
#[test]
#[serial]
fn test_ssh_config_validation() {
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 5433
listen_host: "127.0.0.1"
targets:
ssh_target:
host: "localhost"
port: {}
ssh:
enabled: true
host: "bastion.example.com"
user: "testuser"
key_file: "/tmp/nonexistent.pem"
port: 22
timeout_seconds: 30
auto_reconnect: true
max_reconnect_attempts: 3
connection_management:
health_check_timeout_seconds: 5
"#,
postgresql_port
);
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(),
"SSH config validation should succeed"
);
}
#[test]
#[serial]
fn test_multiple_targets_config() {
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 5433
listen_host: "127.0.0.1"
targets:
local:
host: "localhost"
port: {}
production:
host: "prod.example.com"
port: {}
ssh:
enabled: true
host: "bastion.example.com"
user: "produser"
key_file: "/path/to/prod.pem"
development:
host: "dev.example.com"
port: {}
ssh:
enabled: false
host: "dev-bastion.example.com"
user: "devuser"
key_file: "/path/to/dev.pem"
connection_management:
health_check_interval_seconds: 60
health_check_timeout_seconds: 10
"#,
postgresql_port, postgresql_port, postgresql_port
);
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]
#[serial]
fn test_proxy_graceful_shutdown() {
if !is_postgresql_available() {
println!("Skipping test: PostgreSQL not available");
return;
}
let postgresql_port = get_postgresql_port();
let config_content = format!(
r#"
proxy:
listen_port: 15437
listen_host: "127.0.0.1"
targets:
local:
host: "localhost"
port: {}
"#,
postgresql_port
);
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_content.as_bytes()).unwrap();
let mut child = Command::new("cargo")
.args(&[
"run",
"--",
"--config",
temp_file.path().to_str().unwrap(),
"start",
"--target",
"local",
"--port",
"15437",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start proxy");
thread::sleep(Duration::from_secs(2));
let proxy_available =
TcpStream::connect_timeout(&"127.0.0.1:15437".parse().unwrap(), Duration::from_secs(2))
.is_ok();
assert!(proxy_available, "Proxy should be running");
let _ = child.kill();
let exit_status = child.wait().expect("Failed to wait for child process");
println!("Process exited with status: {:?}", exit_status);
}