use std::net::TcpStream;
use std::process::{Child, Command};
use std::time::{Duration, Instant};
fn cli_bin() -> &'static str {
env!("CARGO_BIN_EXE_powdb-cli")
}
fn server_bin() -> Option<std::path::PathBuf> {
let cli = std::path::Path::new(cli_bin());
let dir = cli.parent()?;
let ext = if cfg!(windows) { ".exe" } else { "" };
let candidate = dir.join(format!("powdb-server{ext}"));
candidate.exists().then_some(candidate)
}
fn tmp(tag: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!(
"powdb_tlscli_{tag}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = std::fs::remove_dir_all(&p);
p
}
fn cli(args: &[&str]) -> std::process::Output {
Command::new(cli_bin())
.args(args)
.env_remove("POWDB_TLS")
.env_remove("POWDB_TLS_CA")
.env_remove("POWDB_TLS_SERVER_NAME")
.env_remove("POWDB_PASSWORD")
.output()
.expect("failed to run powdb-cli")
}
fn free_port() -> u16 {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral");
l.local_addr().unwrap().port()
}
fn wait_for_port(port: u16) {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("server did not start listening on port {port}");
}
struct ServerGuard(Child);
impl Drop for ServerGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn write_localhost_certs(dir: &std::path::Path) -> (std::path::PathBuf, std::path::PathBuf) {
let rcgen::CertifiedKey { cert, signing_key } =
rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
std::fs::create_dir_all(dir).unwrap();
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
std::fs::write(&cert_path, cert.pem()).unwrap();
std::fs::write(&key_path, signing_key.serialize_pem()).unwrap();
(cert_path, key_path)
}
fn stderr_str(o: &std::process::Output) -> String {
String::from_utf8_lossy(&o.stderr).to_string()
}
#[test]
fn cli_tls_against_tls_required_server() {
let Some(server) = server_bin() else {
eprintln!("skipping: powdb-server binary not found next to powdb-cli");
return;
};
let data = tmp("data");
let data_s = data.to_str().unwrap().to_string();
let cert_dir = tmp("certs");
let (cert_path, key_path) = write_localhost_certs(&cert_dir);
let cert_s = cert_path.to_str().unwrap().to_string();
let port = free_port();
let child = Command::new(&server)
.args(["--data-dir", &data_s, "--port", &port.to_string()])
.env("POWDB_TLS_CERT", &cert_s)
.env("POWDB_TLS_KEY", key_path.to_str().unwrap())
.env_remove("POWDB_PASSWORD")
.env_remove("POWDB_ADMIN_USER")
.env_remove("POWDB_ADMIN_PASSWORD")
.spawn()
.expect("failed to spawn powdb-server");
let _guard = ServerGuard(child);
wait_for_port(port);
let addr = format!("localhost:{port}");
let mut ok = None;
for _ in 0..5 {
let out = cli(&[
"--remote",
&addr,
"--tls",
"--tls-ca",
&cert_s,
"-c",
r#"type TlsT { required id: int }; insert TlsT { id := 7 }; count(TlsT)"#,
]);
if out.status.success() {
ok = Some(out);
break;
}
std::thread::sleep(Duration::from_millis(100));
}
let out = ok.expect("TLS CLI round trip never succeeded");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains('1'),
"expected count(TlsT) == 1 in output: {stdout}"
);
let ip_addr = format!("127.0.0.1:{port}");
let out = cli(&[
"--remote",
&ip_addr,
"--tls",
"--tls-ca",
&cert_s,
"--tls-server-name",
"localhost",
"-c",
"count(TlsT)",
]);
assert!(
out.status.success(),
"--tls-server-name override failed: {}",
stderr_str(&out)
);
let out = cli(&[
"--remote",
&ip_addr,
"--tls",
"--tls-ca",
&cert_s,
"-c",
"count(TlsT)",
]);
assert!(!out.status.success(), "hostname mismatch must fail");
assert!(
stderr_str(&out).contains("TLS handshake"),
"expected a TLS handshake error, got: {}",
stderr_str(&out)
);
let out = cli(&["--remote", &addr, "--tls", "-c", "count(TlsT)"]);
assert!(!out.status.success(), "untrusted cert must fail");
assert!(
stderr_str(&out).contains("TLS handshake"),
"expected a TLS handshake error, got: {}",
stderr_str(&out)
);
let out = cli(&[
"--remote",
&addr,
"--tls",
"--tls-ca",
"/nonexistent/ca.pem",
"-c",
"count(TlsT)",
]);
assert!(!out.status.success(), "bad CA path must fail");
assert!(
stderr_str(&out).contains("failed to open TLS CA file"),
"expected a CA file error, got: {}",
stderr_str(&out)
);
let out = cli(&["--remote", &addr, "-c", "count(TlsT)"]);
assert!(
!out.status.success(),
"plaintext CLI against a TLS server must fail"
);
assert!(
stderr_str(&out).contains("Error:"),
"expected a clean error message, got: {}",
stderr_str(&out)
);
let _ = std::fs::remove_dir_all(&data);
let _ = std::fs::remove_dir_all(&cert_dir);
}