use ccxt_core::serde::Deserialize;
use std::env;
#[derive(Debug, Clone, Deserialize)]
#[allow(clippy::unsafe_derive_deserialize)]
pub struct TestConfig {
#[serde(default)]
pub enable_private_tests: bool,
#[serde(default)]
pub enable_integration_tests: bool,
#[serde(default = "default_timeout")]
pub test_timeout_seconds: u64,
#[serde(default)]
pub binance: ExchangeConfig,
#[serde(default)]
pub okx: ExchangeConfig,
#[serde(default)]
pub bybit: ExchangeConfig,
#[serde(default)]
pub bitget: ExchangeConfig,
#[serde(default)]
pub test: TestDataConfig,
#[serde(default = "default_log_level")]
pub rust_log: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ExchangeConfig {
pub api_key: Option<String>,
pub api_secret: Option<String>,
#[serde(default)]
pub use_testnet: bool,
pub api_endpoint: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TestDataConfig {
#[serde(default = "default_test_symbol")]
pub symbol: String,
#[serde(default)]
pub use_mock_data: bool,
#[serde(default = "default_test_amount")]
pub amount: f64,
#[serde(default = "default_price_offset")]
pub price_offset_percent: f64,
}
impl Default for TestDataConfig {
fn default() -> Self {
Self {
symbol: default_test_symbol(),
use_mock_data: false,
amount: default_test_amount(),
price_offset_percent: default_price_offset(),
}
}
}
fn default_timeout() -> u64 {
30
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_test_symbol() -> String {
"BTC/USDT".to_string()
}
fn default_test_amount() -> f64 {
0.001
}
fn default_price_offset() -> f64 {
5.0
}
impl TestConfig {
pub fn from_env() -> Result<Self, envy::Error> {
dotenvy::dotenv().ok();
envy::from_env::<Self>()
}
pub fn from_dotenv(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
dotenvy::from_filename(path)?;
Ok(Self::from_env()?)
}
#[must_use]
pub fn should_skip_private_tests(&self) -> bool {
!self.enable_private_tests
}
#[must_use]
pub fn should_skip_integration_tests(&self) -> bool {
!self.enable_integration_tests
}
#[must_use]
pub fn binance_credentials(&self) -> Option<(&str, &str)> {
match (&self.binance.api_key, &self.binance.api_secret) {
(Some(key), Some(secret)) => Some((key.as_str(), secret.as_str())),
_ => None,
}
}
#[must_use]
pub fn has_binance_credentials(&self) -> bool {
self.binance_credentials().is_some()
}
pub fn init_logging(&self) {
if env::var("RUST_LOG").is_err() {
unsafe {
env::set_var("RUST_LOG", &self.rust_log);
}
}
tracing_subscriber::fmt::init();
}
}
#[macro_export]
macro_rules! skip_if {
($condition:expr, $reason:expr) => {
if $condition {
println!("⚠️ Skipping test: {}", $reason);
return;
}
};
}
#[macro_export]
macro_rules! require_credentials {
($config:expr, $exchange:ident) => {
if !$config.paste::paste! { [<has_ $exchange _credentials>] }() {
println!(
"⚠️ Skipping test: Missing {} API credentials",
stringify!($exchange).to_uppercase()
);
return;
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = TestConfig {
enable_private_tests: false,
enable_integration_tests: false,
test_timeout_seconds: default_timeout(),
binance: ExchangeConfig::default(),
okx: ExchangeConfig::default(),
bybit: ExchangeConfig::default(),
bitget: ExchangeConfig::default(),
test: TestDataConfig::default(),
rust_log: default_log_level(),
};
assert!(!config.enable_private_tests);
assert!(!config.enable_integration_tests);
assert_eq!(config.test_timeout_seconds, 30);
assert_eq!(config.test.symbol, "BTC/USDT");
assert_eq!(config.rust_log, "info");
}
#[test]
fn test_exchange_config_default() {
let config = ExchangeConfig::default();
assert!(config.api_key.is_none());
assert!(config.api_secret.is_none());
assert!(!config.use_testnet);
}
#[test]
fn test_has_credentials() {
let mut config = TestConfig {
enable_private_tests: true,
enable_integration_tests: false,
test_timeout_seconds: 30,
binance: ExchangeConfig {
api_key: Some("test_key".to_string()),
api_secret: Some("test_secret".to_string()),
use_testnet: true,
api_endpoint: None,
},
okx: ExchangeConfig::default(),
bybit: ExchangeConfig::default(),
bitget: ExchangeConfig::default(),
test: TestDataConfig::default(),
rust_log: "debug".to_string(),
};
assert!(config.has_binance_credentials());
assert_eq!(
config.binance_credentials(),
Some(("test_key", "test_secret"))
);
config.binance.api_key = None;
assert!(!config.has_binance_credentials());
}
#[test]
fn test_should_skip_tests() {
let config = TestConfig {
enable_private_tests: false,
enable_integration_tests: true,
test_timeout_seconds: 30,
binance: ExchangeConfig::default(),
okx: ExchangeConfig::default(),
bybit: ExchangeConfig::default(),
bitget: ExchangeConfig::default(),
test: TestDataConfig::default(),
rust_log: "info".to_string(),
};
assert!(config.should_skip_private_tests());
assert!(!config.should_skip_integration_tests());
}
}