use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use confers::Config;
#[cfg(feature = "db")]
use super::app::DatabaseConfig;
use super::app::{
AuditConfig, AuthConfig, ConfigError, EmbeddingConfig, MemoryPoolConfig, ModelConfig,
MonitoringConfig, RateLimitConfig, ServerConfig, apply_priority_defaults,
apply_security_env_overrides,
};
use crate::pipeline::PipelineConfig;
#[derive(Config, Debug, Clone, Serialize, Deserialize)]
#[config(env_prefix = "VECBOOST_")]
#[serde(default)]
pub struct AppConfig {
pub server: ServerConfig,
pub model: ModelConfig,
pub embedding: EmbeddingConfig,
pub monitoring: MonitoringConfig,
pub auth: AuthConfig,
pub rate_limit: RateLimitConfig,
pub audit: AuditConfig,
pub memory_pool: MemoryPoolConfig,
pub pipeline: PipelineConfig,
#[cfg(feature = "db")]
pub database: DatabaseConfig,
}
impl AppConfig {
pub fn load_via_confers() -> Result<Self, ConfigError> {
Self::load_via_confers_with_path("config.toml")
}
pub fn load_via_confers_with_path<P: Into<PathBuf>>(path: P) -> Result<Self, ConfigError> {
let mut config = confers::ConfigBuilder::<Self>::new()
.allow_absolute_paths()
.file_optional(path)
.env_prefix("VECBOOST_")
.build()?;
apply_security_env_overrides(&mut config)?;
apply_priority_defaults(&mut config.pipeline.priority);
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::AppConfig;
use crate::config::app::test_support::ENV_LOCK;
#[test]
fn test_confers_app_config_compiles() {
let _ = AppConfig::default();
}
#[test]
fn test_app_config_default_values() {
let config = AppConfig::default();
assert!(!config.server.grpc_enabled);
assert!(config.model.batch_size > 0);
assert!(config.embedding.max_batch_size > 0);
}
#[test]
fn test_load_via_confers_with_nonexistent_path() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
assert!(result.is_ok(), "should fall back to defaults");
let config = result.unwrap();
assert!(!config.server.grpc_enabled);
}
#[test]
fn test_load_via_confers_with_valid_toml() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("test_config.toml");
std::fs::write(
&config_path,
r#"
[server]
host = "0.0.0.0"
port = 8080
grpc_enabled = true
[model]
model_repo = "test/repo"
batch_size = 64
[embedding]
max_batch_size = 128
"#,
)
.expect("Failed to write config");
let result = AppConfig::load_via_confers_with_path(&config_path);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.server.port, 8080);
assert_eq!(config.server.host, "0.0.0.0");
assert!(config.server.grpc_enabled);
assert_eq!(config.model.batch_size, 64);
assert_eq!(config.embedding.max_batch_size, 128);
}
#[test]
fn test_load_via_confers_with_empty_toml() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("empty.toml");
std::fs::write(&config_path, "").expect("Failed to write empty config");
let result = AppConfig::load_via_confers_with_path(&config_path);
assert!(result.is_ok());
let config = result.unwrap();
assert!(!config.server.grpc_enabled);
}
#[test]
fn test_load_via_confers_jwt_secret_env_override() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var(
"VECBOOST_JWT_SECRET",
"this-is-a-valid-jwt-secret-32chars!!",
);
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
let config = result.expect("load should succeed with valid JWT secret");
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
assert_eq!(
config.auth.jwt_secret.as_deref(),
Some("this-is-a-valid-jwt-secret-32chars!!")
);
}
#[test]
fn test_load_via_confers_empty_jwt_secret_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_JWT_SECRET", "");
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
assert!(
result.is_err(),
"empty JWT secret must be rejected by apply_security_env_overrides"
);
}
#[test]
fn test_load_via_confers_short_jwt_secret_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_JWT_SECRET", "tooshort");
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
assert!(
result.is_err(),
"JWT secret shorter than 32 chars must be rejected"
);
}
#[test]
fn test_load_via_confers_admin_password_env_override() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_ADMIN_PASSWORD", "SuperSecurePass123!");
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
let config = result.expect("load should succeed with valid admin password");
unsafe {
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
assert_eq!(
config.auth.default_admin_password.as_deref(),
Some("SuperSecurePass123!")
);
}
#[test]
fn test_load_via_confers_empty_admin_password_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_ADMIN_PASSWORD", "");
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
assert!(
result.is_err(),
"empty admin password must be rejected by apply_security_env_overrides"
);
}
#[test]
fn test_load_via_confers_short_admin_password_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_ADMIN_PASSWORD", "short");
}
let result = AppConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
assert!(
result.is_err(),
"admin password shorter than 12 chars must be rejected"
);
}
#[test]
fn test_load_via_confers_with_partial_toml() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("partial.toml");
std::fs::write(
&config_path,
r#"
[server]
port = 9999
"#,
)
.expect("Failed to write partial config");
let result = AppConfig::load_via_confers_with_path(&config_path);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.server.port, 9999);
}
#[test]
fn test_app_config_clone() {
let config = AppConfig::default();
let cloned = config.clone();
assert_eq!(config.server.port, cloned.server.port);
}
#[test]
fn test_app_config_debug_format() {
let config = AppConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("AppConfig"));
}
}