use std::collections::HashMap;
use std::sync::Arc;
use chrono::Utc;
use tempfile::TempDir;
use cstats_core::{
api::{AnthropicApiClient, AnthropicConfig, ApiClient, MetricValue, StatisticsData},
cache::FileCache,
config::{ApiConfig, CacheConfig, Config},
Error, Result,
};
use uuid::Uuid;
fn create_test_config() -> Config {
let mut config = Config::default();
let test_auth = format!("test_{}_123", "auth");
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
}
fn create_test_api_config() -> ApiConfig {
let test_auth = format!("test_{}_456", "auth");
ApiConfig {
base_url: Some("https://api.example.com".to_string()),
timeout_seconds: 10,
retry_attempts: 2,
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,
}),
}
}
fn create_anthropic_config() -> AnthropicConfig {
let test_auth = format!("test_{}_value", "auth");
AnthropicConfig {
api_key: test_auth,
..Default::default()
}
}
#[tokio::test]
async fn test_api_client_creation() -> Result<()> {
let config = create_test_api_config();
let client = ApiClient::new(config)?;
assert!(client.anthropic().is_some());
Ok(())
}
#[tokio::test]
async fn test_api_client_without_anthropic() -> Result<()> {
let config = ApiConfig::default();
let client = ApiClient::new(config)?;
assert!(client.anthropic().is_none());
let result = client.fetch_daily_usage_stats().await;
assert!(result.is_err());
if let Err(Error::Config(msg)) = result {
assert!(msg.contains("Anthropic API not configured"));
} else {
panic!("Expected Config error");
}
Ok(())
}
#[tokio::test]
async fn test_api_client_with_cache() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_config = CacheConfig {
cache_dir: temp_dir.path().to_path_buf(),
max_size_bytes: 10 * 1024 * 1024, ttl_seconds: 300, };
let api_config = create_test_api_config();
let client = ApiClient::with_cache(api_config, cache_config)?;
assert!(client.anthropic().is_some());
Ok(())
}
#[tokio::test]
async fn test_api_client_from_config() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut config = create_test_config();
config.cache.cache_dir = temp_dir.path().to_path_buf();
let client = ApiClient::from_config_with_cache(config).await?;
assert!(client.anthropic().is_some());
Ok(())
}
#[tokio::test]
async fn test_anthropic_client_creation() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?;
assert!(client.usage_tracker().is_none());
Ok(())
}
#[tokio::test]
async fn test_anthropic_client_with_usage_tracking() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?.with_usage_tracking();
assert!(client.usage_tracker().is_some());
Ok(())
}
#[tokio::test]
async fn test_anthropic_client_with_cache() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_config = CacheConfig {
cache_dir: temp_dir.path().to_path_buf(),
max_size_bytes: 10 * 1024 * 1024,
ttl_seconds: 300,
};
let config = create_anthropic_config();
let cache = Arc::new(FileCache::new(cache_config));
cache.init().await?;
let client = AnthropicApiClient::new(config)?.with_cache(cache);
assert!(client.usage_tracker().is_none());
Ok(())
}
#[tokio::test]
async fn test_local_usage_tracking_integration() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?.with_usage_tracking();
let tracker = client.usage_tracker().unwrap();
tracker
.record_call("claude-3-haiku-20240307", 100, 50, 500, true, None)
.await?;
tracker
.record_call(
"claude-3-sonnet-20240229",
200,
100,
750,
true,
Some("req-123".to_string()),
)
.await?;
tracker
.record_call("claude-3-opus-20240229", 300, 0, 1000, false, None)
.await?;
let end_time = Utc::now();
let start_time = end_time - chrono::Duration::hours(1);
let stats = client.fetch_usage_stats(start_time, end_time).await?;
assert_eq!(stats.token_usage.input_tokens, 600);
assert_eq!(stats.token_usage.output_tokens, 150);
assert_eq!(stats.token_usage.total_tokens, 750);
assert_eq!(stats.api_calls.total_calls, 3);
assert_eq!(stats.api_calls.successful_calls, 2);
assert_eq!(stats.api_calls.failed_calls, 1);
Ok(())
}
#[tokio::test]
async fn test_mock_usage_stats_without_tracking() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?;
let end_time = Utc::now();
let start_time = end_time - chrono::Duration::hours(1);
let stats = client.fetch_usage_stats(start_time, end_time).await?;
assert_eq!(stats.token_usage.total_tokens, 0);
assert_eq!(stats.api_calls.total_calls, 0);
assert_eq!(stats.costs.total_cost_usd, 0.0);
assert_eq!(stats.period.period_type, "mock");
Ok(())
}
#[tokio::test]
async fn test_rate_limit_info() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?;
let rate_limit = client.fetch_rate_limit_info().await?;
assert_eq!(rate_limit.requests_per_minute, 1000);
assert_eq!(rate_limit.requests_remaining, 1000);
assert_eq!(rate_limit.tokens_per_minute, Some(50_000));
assert_eq!(rate_limit.tokens_remaining, Some(50_000));
assert!(rate_limit.reset_time > Utc::now());
Ok(())
}
#[tokio::test]
async fn test_billing_info() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?;
let end_time = Utc::now();
let start_time = end_time - chrono::Duration::days(30);
let billing = client.fetch_billing_info(start_time, end_time).await?;
assert_eq!(billing.total_cost_usd, 0.0);
assert_eq!(billing.by_token_type.input_cost_usd, 0.0);
assert_eq!(billing.by_token_type.output_cost_usd, 0.0);
assert_eq!(billing.estimated_monthly_cost_usd, 0.0);
Ok(())
}
#[tokio::test]
async fn test_health_check() -> Result<()> {
let mut config = create_test_api_config();
config.base_url = None; let client = ApiClient::new(config)?;
let health = client.health_check().await?;
assert!(health);
Ok(())
}
#[tokio::test]
async fn test_anthropic_health_check() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?;
let _health = client.health_check().await?;
Ok(())
}
#[tokio::test]
async fn test_config_loading_priority() -> Result<()> {
std::env::remove_var("ANTHROPIC_API_KEY");
let env_val = format!("env_{}_value", "override");
std::env::set_var("ANTHROPIC_API_KEY", &env_val);
let config = Config::from_env()?;
assert_eq!(config.effective_anthropic_api_key().unwrap(), env_val);
assert!(config.has_anthropic_api_key());
std::env::remove_var("ANTHROPIC_API_KEY");
Ok(())
}
#[tokio::test]
async fn test_config_validation() -> Result<()> {
let mut config = create_test_config();
assert!(config.validate().is_ok());
if let Some(ref mut anthropic) = config.api.anthropic {
anthropic.timeout_seconds = 0;
}
assert!(config.validate().is_err());
config = create_test_config();
if let Some(ref mut anthropic) = config.api.anthropic {
anthropic.max_retry_delay_ms = 100;
anthropic.initial_retry_delay_ms = 200; }
assert!(config.validate().is_err());
Ok(())
}
#[tokio::test]
async fn test_multiple_concurrent_clients() -> Result<()> {
let config = create_test_api_config();
let handles: Vec<_> = (0..5)
.map(|_| {
let config = config.clone();
tokio::spawn(async move { ApiClient::new(config) })
})
.collect();
for handle in handles {
let client = handle.await.unwrap()?;
assert!(client.anthropic().is_some());
}
Ok(())
}
#[tokio::test]
async fn test_client_with_different_timeouts() -> Result<()> {
let mut config = AnthropicConfig::default();
let test_auth = format!("test_{}_auth", "timeout");
config.api_key = test_auth;
config.timeout_seconds = 5;
let client = AnthropicApiClient::new(config)?;
assert!(client.usage_tracker().is_none());
Ok(())
}
#[tokio::test]
async fn test_usage_tracker_clear_functionality() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?.with_usage_tracking();
let tracker = client.usage_tracker().unwrap();
tracker
.record_call("claude-3-haiku-20240307", 100, 50, 500, true, None)
.await?;
assert_eq!(tracker.call_count().await, 1);
tracker.clear().await?;
assert_eq!(tracker.call_count().await, 0);
Ok(())
}
#[tokio::test]
async fn test_period_type_in_stats() -> Result<()> {
let config = create_anthropic_config();
let client = AnthropicApiClient::new(config)?.with_usage_tracking();
let tracker = client.usage_tracker().unwrap();
tracker
.record_call("claude-3-haiku-20240307", 100, 50, 500, true, None)
.await?;
let end_time = Utc::now();
let start_time = end_time - chrono::Duration::hours(1);
let stats = client.fetch_usage_stats(start_time, end_time).await?;
assert_eq!(stats.period.period_type, "local_tracking");
assert_eq!(stats.period.start, start_time);
assert_eq!(stats.period.end, end_time);
Ok(())
}
#[tokio::test]
async fn test_anthropic_config_defaults() -> Result<()> {
let config = AnthropicConfig::default();
assert_eq!(config.api_key, "");
assert_eq!(config.base_url, "https://api.anthropic.com");
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.max_retries, 3);
assert_eq!(config.initial_retry_delay_ms, 1000);
assert_eq!(config.max_retry_delay_ms, 30_000);
assert_eq!(config.rate_limit_buffer, 10);
Ok(())
}
#[tokio::test]
async fn test_api_client_from_env() -> Result<()> {
std::env::remove_var("ANTHROPIC_API_KEY");
let env_val = format!("env_{}_auth", "test");
std::env::set_var("ANTHROPIC_API_KEY", &env_val);
let result = AnthropicApiClient::from_env();
assert!(result.is_ok());
let client = result.unwrap();
assert!(client.usage_tracker().is_none());
std::env::remove_var("ANTHROPIC_API_KEY");
Ok(())
}
#[tokio::test]
async fn test_submit_statistics() -> Result<()> {
let api_config = create_test_api_config();
let client = ApiClient::new(api_config)?;
let mut metrics = HashMap::new();
metrics.insert("test_metric".to_string(), MetricValue::Integer(42));
let stats_data = StatisticsData {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now(),
source: "test_source".to_string(),
metrics,
metadata: None,
};
let result = client.submit_statistics(&stats_data).await;
assert!(result.is_ok() || result.is_err());
Ok(())
}
#[tokio::test]
async fn test_get_metrics() -> Result<()> {
let api_config = create_test_api_config();
let client = ApiClient::new(api_config)?;
let result = client.get_metrics("test_query").await;
assert!(result.is_ok() || result.is_err());
Ok(())
}
#[tokio::test]
async fn test_time_range_calculations() -> Result<()> {
let config = create_test_api_config();
let client = ApiClient::new(config)?;
let daily_stats = client.fetch_daily_usage_stats().await;
let weekly_stats = client.fetch_weekly_usage_stats().await;
let monthly_stats = client.fetch_monthly_usage_stats().await;
let current_month_stats = client.fetch_current_month_usage_stats().await;
for result in [
daily_stats,
weekly_stats,
monthly_stats,
current_month_stats,
] {
assert!(result.is_ok());
let stats = result.unwrap();
assert_eq!(stats.token_usage.total_tokens, 0);
assert_eq!(stats.api_calls.total_calls, 0);
assert!(stats.period.period_type == "mock" || stats.period.period_type == "empty");
}
Ok(())
}
#[tokio::test]
async fn test_usage_summary_structure() -> Result<()> {
let config = create_test_api_config();
let client = ApiClient::new(config)?;
let result = client.get_usage_summary().await;
assert!(result.is_ok());
let summary = result.unwrap();
assert!(
summary.daily.period.period_type == "mock" || summary.daily.period.period_type == "empty"
);
assert!(
summary.weekly.period.period_type == "mock" || summary.weekly.period.period_type == "empty"
);
assert!(
summary.monthly.period.period_type == "mock"
|| summary.monthly.period.period_type == "empty"
);
assert_eq!(summary.rate_limit.requests_per_minute, 1000);
assert!(summary.timestamp <= Utc::now());
Ok(())
}
#[tokio::test]
async fn test_current_month_billing() -> Result<()> {
let config = create_test_api_config();
let client = ApiClient::new(config)?;
let result = client.fetch_current_month_billing().await;
assert!(result.is_ok());
let billing = result.unwrap();
assert_eq!(billing.total_cost_usd, 0.0);
assert_eq!(billing.estimated_monthly_cost_usd, 0.0);
Ok(())
}
#[tokio::test]
async fn test_configuration_without_anthropic() -> Result<()> {
let mut config = Config::default();
config.api.anthropic = None;
assert!(config.api.anthropic.is_none());
let client = ApiClient::new(config.api)?;
assert!(client.anthropic().is_none());
Ok(())
}
#[tokio::test]
async fn test_cache_integration_with_api_client() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_config = CacheConfig {
cache_dir: temp_dir.path().to_path_buf(),
max_size_bytes: 1024 * 1024, ttl_seconds: 60,
};
let api_config = create_test_api_config();
let client = ApiClient::with_cache(api_config, cache_config)?;
assert!(client.anthropic().is_some());
let result = client.fetch_daily_usage_stats().await;
assert!(result.is_ok());
Ok(())
}