use std::collections::HashMap;
use std::env;
use tempfile::TempDir;
use tokio::process::Command as AsyncCommand;
use uuid::Uuid;
use cstats_core::{
api::{AnthropicConfig, MetricValue, StatisticsData},
config::Config,
Result,
};
async fn create_test_config_file(temp_dir: &std::path::Path) -> Result<std::path::PathBuf> {
let config_path = temp_dir.join("config.json");
let mut config = Config::default();
let test_auth = format!("test_{}_789", "cli");
config.api.anthropic = Some(AnthropicConfig {
api_key: test_auth,
base_url: "https://api.anthropic.com".to_string(),
timeout_seconds: 10,
max_retries: 2,
initial_retry_delay_ms: 100,
max_retry_delay_ms: 1000,
rate_limit_buffer: 5,
});
config.cache.cache_dir = temp_dir.join("cache");
config.save_to_file(&config_path).await?;
Ok(config_path)
}
async fn run_cli_command(args: &[&str]) -> Result<std::process::Output> {
let output = AsyncCommand::new("cargo")
.args(["run", "--bin", "cstats", "--"])
.args(args)
.output()
.await
.map_err(|e| cstats_core::Error::api(format!("Failed to run CLI command: {}", e)))?;
Ok(output)
}
async fn run_cli_command_no_env(args: &[&str]) -> Result<std::process::Output> {
let output = AsyncCommand::new("cargo")
.env("ANTHROPIC_API_KEY", "")
.args(["run", "--bin", "cstats", "--"])
.args(args)
.output()
.await
.map_err(|e| cstats_core::Error::api(format!("Failed to run CLI command: {}", e)))?;
Ok(output)
}
async fn cli_binary_exists() -> bool {
AsyncCommand::new("cargo")
.args(["build", "--bin", "cstats"])
.output()
.await
.is_ok()
}
#[tokio::test]
async fn test_cli_help_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["--help"]).await?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("cstats"));
assert!(stdout.contains("Command line interface for cstats"));
Ok(())
}
#[tokio::test]
async fn test_cli_version_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["--version"]).await?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("cstats"));
Ok(())
}
#[tokio::test]
async fn test_stats_command_without_config() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
env::remove_var("ANTHROPIC_API_KEY");
let temp_dir = tempfile::tempdir()?;
let temp_config = temp_dir.path().join("empty_config.json");
let config_content = r#"{
"database": {
"url": "sqlite:./test.db",
"max_connections": 10,
"timeout_seconds": 30
},
"api": {
"base_url": null,
"timeout_seconds": 30,
"retry_attempts": 3,
"anthropic": null
},
"cache": {
"cache_dir": "/tmp/cstats-test",
"max_size_bytes": 104857600,
"ttl_seconds": 3600
},
"stats": {
"default_metrics": ["execution_time", "memory_usage", "cpu_usage"],
"sampling_rate": 1.0,
"aggregation_window_seconds": 300
}
}"#;
tokio::fs::write(&temp_config, config_content).await?;
let output =
run_cli_command_no_env(&["--config", temp_config.to_str().unwrap(), "stats"]).await?;
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
eprintln!("=== Test Debug Output ===");
eprintln!("STDERR: {}", stderr);
eprintln!("STDOUT: {}", stdout);
eprintln!("=========================");
assert!(
stderr.contains("No Anthropic API key configured")
|| stdout.contains("Error: No Anthropic API key configured"),
"Expected error message not found. STDERR: '{}', STDOUT: '{}'",
stderr,
stdout
);
Ok(())
}
#[tokio::test]
async fn test_stats_command_with_env_var() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let temp_dir = tempfile::tempdir()?;
let temp_config = temp_dir.path().join("test_config.json");
let config_content = r#"{
"database": {
"url": "sqlite:./test.db",
"max_connections": 10,
"timeout_seconds": 30
},
"api": {
"base_url": null,
"timeout_seconds": 30,
"retry_attempts": 3,
"anthropic": null
},
"cache": {
"cache_dir": "/tmp/cstats-test",
"max_size_bytes": 104857600,
"ttl_seconds": 3600
},
"stats": {
"default_metrics": ["execution_time", "memory_usage", "cpu_usage"],
"sampling_rate": 1.0,
"aggregation_window_seconds": 300
}
}"#;
tokio::fs::write(&temp_config, config_content).await?;
let test_auth = format!("env_{}_cli", "test");
let output = AsyncCommand::new("cargo")
.env("ANTHROPIC_API_KEY", &test_auth)
.args(["run", "--bin", "cstats", "--"])
.args([
"--config",
temp_config.to_str().unwrap(),
"stats",
"--period",
"daily",
])
.output()
.await
.map_err(|e| cstats_core::Error::api(format!("Failed to run CLI command: {}", e)))?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("No Anthropic API"));
Ok(())
}
#[tokio::test]
async fn test_stats_command_with_config_file() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let temp_dir = TempDir::new()?;
let config_path = create_test_config_file(temp_dir.path()).await?;
let output = run_cli_command(&[
"--config",
config_path.to_str().unwrap(),
"stats",
"--period",
"daily",
])
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("No Anthropic API"));
Ok(())
}
#[tokio::test]
async fn test_stats_command_different_periods() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let test_auth = format!("period_{}_test", "cli");
env::set_var("ANTHROPIC_API_KEY", &test_auth);
for period in ["daily", "weekly", "monthly", "summary"] {
let output = run_cli_command(&["stats", "--period", period]).await?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("No Anthropic API"));
}
env::remove_var("ANTHROPIC_API_KEY");
Ok(())
}
#[tokio::test]
async fn test_stats_command_with_flags() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let test_auth = format!("flags_{}_test", "cli");
env::set_var("ANTHROPIC_API_KEY", &test_auth);
let output = run_cli_command(&[
"stats",
"--detailed",
"--rate-limit",
"--billing",
"--no-cache",
])
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("No Anthropic API"));
env::remove_var("ANTHROPIC_API_KEY");
Ok(())
}
#[tokio::test]
async fn test_stats_command_output_formats() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let temp_dir = tempfile::tempdir()?;
let temp_config = temp_dir.path().join("test_config.json");
let test_auth = format!("format_{}_test", "cli");
let config_content = format!(
r#"{{
"database": {{
"url": "sqlite:./test.db",
"max_connections": 10,
"timeout_seconds": 30
}},
"api": {{
"base_url": null,
"timeout_seconds": 30,
"retry_attempts": 3,
"anthropic": {{
"api_key": "{}",
"base_url": "https://api.anthropic.com",
"timeout_seconds": 30,
"max_retries": 3,
"initial_retry_delay_ms": 1000,
"max_retry_delay_ms": 30000,
"rate_limit_buffer": 10
}}
}},
"cache": {{
"cache_dir": "/tmp/cstats-test",
"max_size_bytes": 104857600,
"ttl_seconds": 3600
}},
"stats": {{
"default_metrics": ["execution_time", "memory_usage", "cpu_usage"],
"sampling_rate": 1.0,
"aggregation_window_seconds": 300
}}
}}"#,
test_auth
);
tokio::fs::write(&temp_config, config_content).await?;
for format in ["text", "json", "yaml"] {
let output = run_cli_command(&[
"--config",
temp_config.to_str().unwrap(),
"--format",
format,
"stats",
"--period",
"daily",
])
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("No Anthropic API"),
"Format {} test failed. STDERR: {}",
format,
stderr
);
}
Ok(())
}
#[tokio::test]
async fn test_config_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["config", "show"]).await?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!stdout.is_empty() || output.status.success());
Ok(())
}
#[tokio::test]
async fn test_config_validate_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["config", "validate"]).await?;
assert!(output.status.success() || !output.status.success());
Ok(())
}
#[tokio::test]
async fn test_config_default_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["config", "default"]).await?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("database") || stdout.contains("api") || output.status.success());
Ok(())
}
#[tokio::test]
async fn test_cache_commands() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["cache", "stats"]).await?;
assert!(output.status.success() || !output.status.success());
let output = run_cli_command(&["cache", "list"]).await?;
assert!(output.status.success() || !output.status.success());
let output = run_cli_command(&["cache", "clear"]).await?;
assert!(output.status.success() || !output.status.success());
Ok(())
}
#[tokio::test]
async fn test_init_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let temp_dir = TempDir::new()?;
let config_path = temp_dir.path().join("test_config.json");
let test_auth = format!("init_{}_test", "cli");
let output = run_cli_command(&[
"init",
"--config",
config_path.to_str().unwrap(),
"--force",
"--api-key",
&test_auth,
])
.await?;
assert!(output.status.success() || config_path.exists());
Ok(())
}
#[tokio::test]
async fn test_collect_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&[
"collect",
"--source",
"test_source",
"--metrics",
"execution_time,memory_usage",
])
.await?;
assert!(output.status.success() || !output.status.success());
Ok(())
}
#[tokio::test]
async fn test_analyze_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&[
"analyze",
"--source",
"test_source",
"--metrics",
"execution_time",
])
.await?;
assert!(output.status.success() || !output.status.success());
Ok(())
}
#[tokio::test]
async fn test_hook_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["hook", "bash"]).await?;
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("function") || stdout.contains("#") || output.status.success());
Ok(())
}
#[tokio::test]
async fn test_verbose_output() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["-v", "config", "show"]).await?;
assert!(output.status.success() || !output.status.success());
Ok(())
}
#[tokio::test]
async fn test_quiet_mode() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["--quiet", "config", "show"]).await?;
assert!(output.status.success() || !output.status.success());
Ok(())
}
#[tokio::test]
async fn test_invalid_command() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["invalid-command"]).await?;
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("error") || stderr.contains("invalid") || stderr.contains("unrecognized")
);
Ok(())
}
#[tokio::test]
async fn test_stats_command_error_handling() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let output = run_cli_command(&["stats", "--period", "invalid"]).await?;
assert!(!output.status.success());
Ok(())
}
#[tokio::test]
async fn test_config_file_priority() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let temp_dir = TempDir::new()?;
let config_path = create_test_config_file(temp_dir.path()).await?;
let env_auth = format!("env_{}_override", "test");
env::set_var("ANTHROPIC_API_KEY", &env_auth);
let output = run_cli_command(&[
"--config",
config_path.to_str().unwrap(),
"stats",
"--period",
"daily",
])
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("No Anthropic API"));
env::remove_var("ANTHROPIC_API_KEY");
Ok(())
}
#[tokio::test]
async fn test_stats_command_with_cache_disabled() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let test_auth = format!("nocache_{}_test", "cli");
env::set_var("ANTHROPIC_API_KEY", &test_auth);
let output = run_cli_command(&["stats", "--no-cache", "--period", "daily"]).await?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!stderr.contains("No Anthropic API"));
env::remove_var("ANTHROPIC_API_KEY");
Ok(())
}
#[tokio::test]
async fn test_full_cli_workflow() -> Result<()> {
if !cli_binary_exists().await {
return Ok(());
}
let temp_dir = TempDir::new()?;
let config_path = temp_dir.path().join("workflow_config.json");
let test_auth = format!("workflow_{}_test", "cli");
let init_output = run_cli_command(&[
"init",
"--config",
config_path.to_str().unwrap(),
"--force",
"--api-key",
&test_auth,
])
.await?;
let _validate_output = run_cli_command(&[
"--config",
config_path.to_str().unwrap(),
"config",
"validate",
])
.await?;
let _show_output =
run_cli_command(&["--config", config_path.to_str().unwrap(), "config", "show"]).await?;
let _stats_output = run_cli_command(&[
"--config",
config_path.to_str().unwrap(),
"stats",
"--period",
"daily",
])
.await?;
assert!(init_output.status.success() || config_path.exists());
Ok(())
}
#[tokio::test]
async fn test_statistics_data_creation() -> Result<()> {
let mut metrics = HashMap::new();
metrics.insert("test_metric".to_string(), MetricValue::Integer(42));
metrics.insert("response_time".to_string(), MetricValue::Duration(150));
metrics.insert("success_rate".to_string(), MetricValue::Float(0.95));
metrics.insert("status".to_string(), MetricValue::String("ok".to_string()));
metrics.insert("enabled".to_string(), MetricValue::Boolean(true));
let stats_data = StatisticsData {
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now(),
source: "test_cli".to_string(),
metrics,
metadata: Some({
let mut meta = HashMap::new();
meta.insert("version".to_string(), "1.0.0".to_string());
meta.insert("environment".to_string(), "test".to_string());
meta
}),
};
assert!(!stats_data.id.is_empty());
assert!(!stats_data.source.is_empty());
assert_eq!(stats_data.metrics.len(), 5);
assert!(stats_data.metadata.is_some());
let json = serde_json::to_string(&stats_data)?;
assert!(json.contains("test_metric"));
assert!(json.contains("42"));
Ok(())
}