use std::io::Write;
use super::app::test_support::ENV_LOCK;
use super::app_config::VecboostConfig;
fn write_temp_toml(content: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let path = dir.path().join("test_config.toml");
let mut file = std::fs::File::create(&path).expect("failed to create temp config file");
file.write_all(content.as_bytes())
.expect("failed to write temp config");
(dir, path)
}
#[test]
fn test_confers_load_from_minimal_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 toml_content = r#"
[server]
host = "127.0.0.1"
port = 9002
timeout = 30
[model]
model_repo = "BAAI/bge-m3"
use_gpu = false
batch_size = 8
expected_dimension = 1024
[embedding]
default_aggregation = "mean"
similarity_metric = "cosine"
cache_enabled = false
cache_size = 0
max_batch_size = 32
[monitoring]
memory_limit_mb = 512
metrics_enabled = false
log_level = "info"
[rate_limit]
enabled = false
[pipeline]
enabled = false
[audit]
enabled = false
"#;
let (_dir, path) = write_temp_toml(toml_content);
let config =
VecboostConfig::load_via_confers_with_path(&path).expect("confers load should succeed");
assert_eq!(config.server.host, "127.0.0.1");
assert_eq!(config.server.port, 9002);
assert_eq!(config.server.timeout, Some(30));
assert_eq!(config.model.model_repo, "BAAI/bge-m3");
assert!(!config.model.use_gpu);
assert_eq!(config.model.batch_size, 8);
assert_eq!(config.model.expected_dimension, Some(1024));
assert_eq!(config.embedding.default_aggregation, "mean");
assert!(!config.embedding.cache_enabled);
assert_eq!(config.embedding.max_batch_size, 32);
assert!(!config.rate_limit.enabled);
assert!(!config.audit.enabled);
}
#[test]
fn test_confers_env_var_jwt_secret_override() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
let toml_content = r#"
[server]
host = "0.0.0.0"
port = 8080
[model]
model_repo = "test/model"
use_gpu = false
batch_size = 1
expected_dimension = 128
[embedding]
default_aggregation = "mean"
similarity_metric = "cosine"
cache_enabled = false
cache_size = 0
max_batch_size = 1
[monitoring]
metrics_enabled = false
[rate_limit]
enabled = false
[pipeline]
enabled = false
[audit]
enabled = false
"#;
let (_dir, path) = write_temp_toml(toml_content);
let config_no_env =
VecboostConfig::load_via_confers_with_path(&path).expect("confers load should succeed");
assert!(
config_no_env.auth.jwt_secret.is_none(),
"jwt_secret should be None when VECBOOST_JWT_SECRET is not set"
);
let test_secret = "test-jwt-secret-from-env-var-32+chars";
assert!(
test_secret.len() >= 32,
"test secret should be at least 32 chars"
);
unsafe {
std::env::set_var("VECBOOST_JWT_SECRET", test_secret);
}
let config_with_env =
VecboostConfig::load_via_confers_with_path(&path).expect("confers load should succeed");
assert_eq!(
config_with_env.auth.jwt_secret,
Some(test_secret.to_string()),
"VECBOOST_JWT_SECRET should override TOML default"
);
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
}
#[test]
fn test_confers_defaults_when_no_file() {
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 non_existent = std::path::PathBuf::from("/tmp/vecboost_nonexistent_config_9999.toml");
let config = VecboostConfig::load_via_confers_with_path(&non_existent)
.expect("should load with defaults when file is missing");
assert_eq!(config.server.host, "0.0.0.0");
assert_eq!(config.server.port, 9002);
assert_eq!(config.model.model_repo, "BAAI/bge-m3");
assert_eq!(config.model.batch_size, 32);
assert_eq!(config.embedding.default_aggregation, "mean");
assert!(config.embedding.cache_enabled);
assert!(!config.auth.enabled);
assert!(config.auth.jwt_secret.is_none());
}
#[test]
fn test_confers_toml_overrides_defaults() {
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 toml_content = r#"
[server]
host = "10.0.0.1"
port = 7777
grpc_enabled = true
[model]
model_repo = "custom/repo"
use_gpu = true
batch_size = 64
expected_dimension = 768
[embedding]
default_aggregation = "max"
similarity_metric = "dot"
cache_enabled = true
cache_size = 4096
max_batch_size = 128
[monitoring]
metrics_enabled = true
log_level = "debug"
[rate_limit]
enabled = true
[pipeline]
enabled = true
[audit]
enabled = true
"#;
let (_dir, path) = write_temp_toml(toml_content);
let config =
VecboostConfig::load_via_confers_with_path(&path).expect("confers load should succeed");
assert_eq!(config.server.host, "10.0.0.1");
assert_eq!(config.server.port, 7777);
assert!(config.server.grpc_enabled);
assert_eq!(config.model.model_repo, "custom/repo");
assert!(config.model.use_gpu);
assert_eq!(config.model.batch_size, 64);
assert_eq!(config.model.expected_dimension, Some(768));
assert_eq!(config.embedding.default_aggregation, "max");
assert_eq!(config.embedding.similarity_metric, "dot");
assert_eq!(config.embedding.cache_size, 4096);
assert_eq!(config.embedding.max_batch_size, 128);
assert!(config.rate_limit.enabled);
assert!(config.audit.enabled);
}
#[test]
fn test_trusted_proxies_and_max_text_length_defaults_when_omitted() {
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 toml_content = r#"
[server]
host = "127.0.0.1"
port = 9000
[model]
model_repo = "test/model"
use_gpu = false
batch_size = 8
expected_dimension = 128
[embedding]
default_aggregation = "mean"
similarity_metric = "cosine"
cache_enabled = false
cache_size = 0
max_batch_size = 32
# max_text_length 故意省略,应 fallback 到 default 8192
[monitoring]
metrics_enabled = false
[rate_limit]
enabled = false
[pipeline]
enabled = false
[audit]
enabled = false
[auth]
enabled = false
# trusted_proxies 故意省略,应 fallback 到 default 空 Vec
"#;
let (_dir, path) = write_temp_toml(toml_content);
let config =
VecboostConfig::load_via_confers_with_path(&path).expect("confers load should succeed");
assert!(
config.auth.trusted_proxies.is_empty(),
"trusted_proxies should default to empty Vec when omitted"
);
assert_eq!(
config.embedding.max_text_length, 8192,
"max_text_length should default to 8192 when omitted"
);
}
#[test]
fn test_trusted_proxies_and_max_text_length_loaded_when_present() {
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 toml_content = r#"
[server]
host = "127.0.0.1"
port = 9001
[model]
model_repo = "test/model"
use_gpu = false
batch_size = 8
expected_dimension = 128
[embedding]
default_aggregation = "mean"
similarity_metric = "cosine"
cache_enabled = false
cache_size = 0
max_batch_size = 32
max_text_length = 4096
[monitoring]
metrics_enabled = false
[rate_limit]
enabled = false
[pipeline]
enabled = false
[audit]
enabled = false
[auth]
enabled = false
trusted_proxies = ["10.0.0.0/8", "192.168.0.0/16"]
"#;
let (_dir, path) = write_temp_toml(toml_content);
let config =
VecboostConfig::load_via_confers_with_path(&path).expect("confers load should succeed");
assert_eq!(
config.auth.trusted_proxies,
vec!["10.0.0.0/8".to_string(), "192.168.0.0/16".to_string()],
"trusted_proxies should match TOML values"
);
assert_eq!(
config.embedding.max_text_length, 4096,
"max_text_length should match TOML value"
);
}
#[test]
fn test_validation_port_zero_rejected() {
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 toml_content = r#"
[server]
port = 0
[model]
model_repo = "test/model"
batch_size = 8
[embedding]
max_batch_size = 32
max_text_length = 8192
[audit]
enabled = false
[pipeline]
enabled = false
"#;
let (_dir, path) = write_temp_toml(toml_content);
let result = VecboostConfig::load_via_confers_with_path(&path);
assert!(result.is_err(), "port=0 must be rejected by validation");
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("port"),
"error should mention 'port': {err_msg}"
);
}
#[test]
fn test_validation_empty_model_repo_rejected() {
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 toml_content = r#"
[server]
port = 3000
[model]
model_repo = ""
batch_size = 8
[embedding]
max_batch_size = 32
max_text_length = 8192
[audit]
enabled = false
[pipeline]
enabled = false
"#;
let (_dir, path) = write_temp_toml(toml_content);
let result = VecboostConfig::load_via_confers_with_path(&path);
assert!(
result.is_err(),
"empty model_repo must be rejected by validation"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("model_repo"),
"error should mention 'model_repo': {err_msg}"
);
}
#[test]
fn test_validation_batch_size_zero_rejected() {
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 toml_content = r#"
[server]
port = 3000
[model]
model_repo = "test/model"
batch_size = 0
[embedding]
max_batch_size = 32
max_text_length = 8192
[audit]
enabled = false
[pipeline]
enabled = false
"#;
let (_dir, path) = write_temp_toml(toml_content);
let result = VecboostConfig::load_via_confers_with_path(&path);
assert!(
result.is_err(),
"batch_size=0 must be rejected by validation"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("batch_size"),
"error should mention 'batch_size': {err_msg}"
);
}
#[test]
fn test_validation_default_config_passes() {
let config = VecboostConfig::default();
let result = config.validate();
assert!(
result.is_ok(),
"default config should pass validation: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_config_watch_detects_file_change() {
let toml_content = r#"
[server]
host = "127.0.0.1"
port = 9002
timeout = 30
[model]
model_repo = "BAAI/bge-m3"
use_gpu = false
batch_size = 8
[embedding]
max_batch_size = 32
max_text_length = 8192
[audit]
enabled = false
[pipeline]
enabled = false
"#;
let (_dir, path) = write_temp_toml(toml_content);
let mut watcher = confers::watcher::FsWatcher::new(&path, 100)
.await
.expect("FsWatcher should be created");
assert!(watcher.is_running(), "watcher should be running");
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let watch_task = tokio::spawn(async move {
if let Some(changed_path) = watcher.recv().await {
let _ = tx.send(changed_path).await;
}
});
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
{
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.expect("should open file for append");
writeln!(file, "\n# hot reload test change").expect("should write");
}
let result = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv()).await;
assert!(result.is_ok(), "should receive change event within 5s");
let changed_path = result.unwrap().expect("channel should have a value");
assert!(
changed_path.to_string_lossy().contains("test_config"),
"changed path should reference the config file: {:?}",
changed_path
);
watch_task.abort();
}
#[tokio::test]
async fn test_watcher_guard_lifecycle() {
let guard = confers::watcher::WatcherGuard::new();
assert!(!guard.is_running(), "new guard should not be running");
guard.start();
assert!(guard.is_running(), "guard should be running after start");
let result = guard.shutdown(std::time::Duration::from_secs(2)).await;
assert!(result.is_ok(), "shutdown should succeed");
assert!(result.unwrap(), "shutdown with no task should return true");
assert!(
!guard.is_running(),
"guard should not be running after shutdown"
);
}
#[test]
fn test_encryption_roundtrip_via_serde() {
use super::app::AuthConfig;
use crate::config::app::test_support::ENV_LOCK;
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let enc_key = "vecboost-test-encryption-key-32b"; assert_eq!(enc_key.len(), 32);
unsafe {
std::env::set_var("VECBOOST_ENCRYPTION_KEY", enc_key);
}
let config = AuthConfig {
jwt_secret: Some("my-super-secret-jwt-token-value".to_string()),
default_admin_password: Some("AdminP@ssw0rd!2026".to_string()),
..Default::default()
};
let serialized = toml::to_string(&config).expect("serialize should succeed");
assert!(
!serialized.contains("my-super-secret-jwt-token-value"),
"serialized TOML should not contain plaintext jwt_secret"
);
assert!(
!serialized.contains("AdminP@ssw0rd!2026"),
"serialized TOML should not contain plaintext admin password"
);
let deserialized: AuthConfig = toml::from_str(&serialized).expect("deserialize should succeed");
assert_eq!(
deserialized.jwt_secret,
Some("my-super-secret-jwt-token-value".to_string()),
"jwt_secret should survive encrypt→decrypt roundtrip"
);
assert_eq!(
deserialized.default_admin_password,
Some("AdminP@ssw0rd!2026".to_string()),
"admin password should survive encrypt→decrypt roundtrip"
);
unsafe {
std::env::remove_var("VECBOOST_ENCRYPTION_KEY");
}
}
#[test]
fn test_encryption_fallback_to_plaintext_without_key() {
use super::app::AuthConfig;
use crate::config::app::test_support::ENV_LOCK;
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_ENCRYPTION_KEY");
}
let config = AuthConfig {
jwt_secret: Some("plaintext-jwt-secret".to_string()),
..Default::default()
};
let serialized = toml::to_string(&config).expect("serialize should succeed");
assert!(
serialized.contains("plaintext-jwt-secret"),
"without encryption key, values should be plaintext"
);
let deserialized: AuthConfig = toml::from_str(&serialized).expect("deserialize should succeed");
assert_eq!(
deserialized.jwt_secret,
Some("plaintext-jwt-secret".to_string())
);
}
#[test]
fn test_encryption_none_values_pass_through() {
use super::app::AuthConfig;
use crate::config::app::test_support::ENV_LOCK;
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_ENCRYPTION_KEY");
}
let config = AuthConfig::default();
assert!(config.jwt_secret.is_none());
assert!(config.default_admin_password.is_none());
let serialized = toml::to_string(&config).expect("serialize should succeed");
let deserialized: AuthConfig = toml::from_str(&serialized).expect("deserialize should succeed");
assert!(deserialized.jwt_secret.is_none());
assert!(deserialized.default_admin_password.is_none());
}
#[test]
fn test_schema_generation_produces_typescript() {
let schema = VecboostConfig::generate_schema().expect("schema generation should succeed");
assert!(!schema.is_empty(), "generated schema should not be empty");
assert!(
schema.contains("interface") || schema.contains("type"),
"TypeScript output should contain interface or type definitions: {}",
&schema[..schema.len().min(200)]
);
assert!(
schema.contains("server") || schema.contains("Server"),
"schema should contain server config references"
);
}