use std::io::Write;
use super::app::test_support::ENV_LOCK;
use super::app_config::AppConfig;
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 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 = AppConfig::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().expect("env lock poisoned");
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 =
AppConfig::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 =
AppConfig::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");
}
}
#[tokio::test]
async fn test_confers_inmemorybus_subscribe_publish() {
use confers::ConfigBus;
use confers::bus::{ConfigChangeEvent, InMemoryBus};
use futures::StreamExt;
let bus = InMemoryBus::new();
let mut stream = bus
.subscribe()
.await
.expect("subscribe should return a stream");
let event = ConfigChangeEvent::new(
"test-instance",
"file://config.toml",
vec!["server.port".to_string(), "auth.jwt_secret".to_string()],
"checksum-abc123",
);
bus.publish(event.clone())
.await
.expect("publish should succeed");
let received = tokio::time::timeout(std::time::Duration::from_secs(2), stream.next())
.await
.expect("should receive event within timeout")
.expect("stream should not end");
assert_eq!(received.instance_id, "test-instance");
assert_eq!(received.source, "file://config.toml");
assert_eq!(
received.changed_keys,
vec!["server.port".to_string(), "auth.jwt_secret".to_string()]
);
assert_eq!(received.checksum, "checksum-abc123");
}
#[test]
fn test_confers_defaults_when_no_file() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
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 = AppConfig::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, 3000);
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 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 = AppConfig::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().expect("env lock poisoned");
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 = AppConfig::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().expect("env lock poisoned");
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 = AppConfig::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"
);
}