use std::fs;
use std::io::Write;
use std::process::Command;
use tempfile::TempDir;
fn get_cli_binary() -> std::path::PathBuf {
let mut path = std::env::current_exe()
.expect("Failed to get current exe path")
.parent()
.expect("Failed to get parent dir")
.parent()
.expect("Failed to get target dir")
.to_path_buf();
path.push("graph_d");
#[cfg(windows)]
path.set_extension("exe");
path
}
fn cli_binary_available() -> bool {
get_cli_binary().exists()
}
#[test]
fn test_cli_version_flag() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg("--version")
.output()
.expect("Failed to execute CLI");
assert!(output.status.success(), "Version flag should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("graph_d") || stdout.contains("0.1.0"),
"Version output should contain program name or version: {stdout}"
);
}
#[test]
fn test_cli_help_flag() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg("--help")
.output()
.expect("Failed to execute CLI");
assert!(output.status.success(), "Help flag should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("DATABASE") || stdout.contains("database") || stdout.contains(":memory:"),
"Help should mention database option: {stdout}"
);
assert!(
stdout.contains("--cmd") || stdout.contains("-c"),
"Help should mention command option: {stdout}"
);
}
#[test]
fn test_cli_single_command_mode() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Single command mode should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_match_query_on_empty_database() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("MATCH (n:Person) RETURN n.name")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"MATCH query on empty database should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_invalid_query() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("INVALID SYNTAX")
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{stdout}{stderr}").to_lowercase();
assert!(
combined.contains("error")
|| combined.contains("invalid")
|| combined.contains("unexpected"),
"Invalid query should produce error message. stdout: {stdout}, stderr: {stderr}"
);
}
#[test]
fn test_cli_quiet_mode() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-q")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Quiet mode should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.len() < 10000,
"Quiet mode should not produce excessive output"
);
}
#[test]
fn test_cli_script_file_mode() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let script_path = temp_dir.path().join("test_script.gql");
let mut file = fs::File::create(&script_path).expect("Failed to create script file");
writeln!(file, "-- This is a comment").unwrap();
writeln!(file, "MATCH (n) RETURN n;").unwrap();
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-f")
.arg(&script_path)
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Script file mode should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_script_with_comments() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let script_path = temp_dir.path().join("comments.gql");
let mut file = fs::File::create(&script_path).expect("Failed to create script file");
writeln!(file, "-- SQL-style comment").unwrap();
writeln!(file, "// C-style comment").unwrap();
writeln!(file, "MATCH (n) RETURN n;").unwrap();
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-f")
.arg(&script_path)
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Script with comments should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_script_multiple_queries() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let script_path = temp_dir.path().join("multi.gql");
let mut file = fs::File::create(&script_path).expect("Failed to create script file");
writeln!(file, "MATCH (n) RETURN n;").unwrap();
writeln!(file, "MATCH (n:Person) RETURN n.name;").unwrap();
writeln!(file, "MATCH (n) RETURN count(n);").unwrap();
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-f")
.arg(&script_path)
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Multiple MATCH queries in script should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_file_database_creation() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("test.db");
let output = Command::new(get_cli_binary())
.arg(&db_path)
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"File database creation should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
db_path.exists(),
"Database file should be created at {db_path:?}"
);
}
#[test]
fn test_cli_file_database_reopening() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let db_path = temp_dir.path().join("persist.db");
let output1 = Command::new(get_cli_binary())
.arg(&db_path)
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output1.status.success(),
"First session should succeed. stderr: {}",
String::from_utf8_lossy(&output1.stderr)
);
let output2 = Command::new(get_cli_binary())
.arg(&db_path)
.arg("-c")
.arg("MATCH (n) RETURN count(n)")
.output()
.expect("Failed to execute CLI");
assert!(
output2.status.success(),
"Second session should succeed (database reopening). stderr: {}",
String::from_utf8_lossy(&output2.stderr)
);
}
#[test]
fn test_cli_json_output_format() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-o")
.arg("json")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"JSON format should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_csv_output_format() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-o")
.arg("csv")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"CSV format should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_table_output_format() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-o")
.arg("table")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Table format should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_rejects_database_path_traversal() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg("../../../etc/passwd")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{stdout}{stderr}").to_lowercase();
assert!(
!output.status.success()
|| combined.contains("traversal")
|| combined.contains("error")
|| combined.contains("invalid"),
"Path traversal should be rejected. status: {:?}, combined: {}",
output.status,
combined
);
}
#[test]
fn test_cli_rejects_script_path_traversal() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-f")
.arg("../../../etc/passwd")
.output()
.expect("Failed to execute CLI");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{stdout}{stderr}").to_lowercase();
assert!(
!output.status.success()
|| combined.contains("traversal")
|| combined.contains("error")
|| combined.contains("invalid"),
"Script path traversal should be rejected. status: {:?}, combined: {}",
output.status,
combined
);
}
#[test]
fn test_cli_error_no_sensitive_data() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("MATCH (n) WHERE n.secret_password = 'hunter2' RETURN n")
.output()
.expect("Failed to execute CLI");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
!combined.contains("hunter2"),
"Error messages should not leak property values. output: {combined}"
);
}
#[test]
fn test_cli_verbose_mode() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-v")
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Verbose mode should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Created") || stderr.contains("database") || !stderr.is_empty(),
"Verbose mode should show status messages. stderr: {stderr}"
);
}
#[test]
fn test_cli_empty_script_file() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let script_path = temp_dir.path().join("empty.gql");
fs::File::create(&script_path).expect("Failed to create script file");
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-f")
.arg(&script_path)
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Empty script file should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_comment_only_script() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let script_path = temp_dir.path().join("comments_only.gql");
let mut file = fs::File::create(&script_path).expect("Failed to create script file");
writeln!(file, "-- This is a comment").unwrap();
writeln!(file, "// Another comment").unwrap();
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-f")
.arg(&script_path)
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Comment-only script should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_default_memory_database() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg("-c")
.arg("MATCH (n) RETURN n")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"Default memory database should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_create_node() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("CREATE (n:Person {name: 'Alice'}) RETURN n")
.output()
.expect("Failed to execute CLI");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"CREATE should succeed. stdout: {stdout}, stderr: {stderr}"
);
assert!(
stderr.contains("1 row returned"),
"CREATE should return 1 row. stderr: {stderr}"
);
}
#[test]
fn test_cli_match_with_where_clause() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("MATCH (n:Person) WHERE n.age > 21 RETURN n.name")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"MATCH with WHERE clause should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_match_with_label() {
if !cli_binary_available() {
eprintln!("Skipping: CLI binary not found (build with --features cli)");
return;
}
let output = Command::new(get_cli_binary())
.arg(":memory:")
.arg("-c")
.arg("MATCH (p:Person) RETURN p")
.output()
.expect("Failed to execute CLI");
assert!(
output.status.success(),
"MATCH with label should succeed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}