提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
#[cfg(test)]
mod config_env_tests {
use crate::models::Config;
use std::collections::HashMap;
use std::env;
use std::sync::{Mutex, OnceLock};
use tempfile::TempDir;
// Safe environment variable testing using static state
static TEST_ENV_LOCK: OnceLock<Mutex<HashMap<String, Option<String>>>> = OnceLock::new();
fn get_test_env_lock() -> &'static Mutex<HashMap<String, Option<String>>> {
TEST_ENV_LOCK.get_or_init(|| Mutex::new(HashMap::new()))
}
// Safe helper to set environment variable for testing
fn safe_set_env_var(key: &str, value: &str) {
let lock = get_test_env_lock();
let mut env_state = lock.lock().unwrap();
// Store the original value if this is the first time setting this var
if !env_state.contains_key(key) {
env_state.insert(key.to_string(), env::var(key).ok());
}
// This is still technically unsafe, but we'll wrap it in unsafe block
// and document that tests should run serially to avoid race conditions
unsafe {
env::set_var(key, value);
}
}
// Safe helper to remove environment variable for testing
fn safe_remove_env_var(key: &str) {
let lock = get_test_env_lock();
let mut env_state = lock.lock().unwrap();
// Store the original value if this is the first time touching this var
if !env_state.contains_key(key) {
env_state.insert(key.to_string(), env::var(key).ok());
}
unsafe {
env::remove_var(key);
}
}
// Helper function to clear all voice-cli environment variables safely
fn clear_voice_cli_env_vars() {
let env_vars = [
"VOICE_CLI_HOST",
"VOICE_CLI_PORT",
"VOICE_CLI_LOG_LEVEL",
"VOICE_CLI_LOG_DIR",
"VOICE_CLI_LOG_MAX_FILES",
"VOICE_CLI_DEFAULT_MODEL",
"VOICE_CLI_MODELS_DIR",
"VOICE_CLI_AUTO_DOWNLOAD",
"VOICE_CLI_TRANSCRIPTION_WORKERS",
"VOICE_CLI_WORK_DIR",
"VOICE_CLI_PID_FILE",
"VOICE_CLI_MAX_FILE_SIZE",
"VOICE_CLI_CORS_ENABLED",
];
for var in &env_vars {
safe_remove_env_var(var);
}
// Add a small delay to ensure environment changes propagate
std::thread::sleep(std::time::Duration::from_millis(10));
}
// Cleanup function to restore original environment state
fn restore_original_env_vars() {
let lock = get_test_env_lock();
let env_state = lock.lock().unwrap();
for (key, original_value) in env_state.iter() {
match original_value {
Some(value) => unsafe {
env::set_var(key, value);
},
None => unsafe {
env::remove_var(key);
},
}
}
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_http_port_environment_override() {
clear_voice_cli_env_vars();
// Set environment variable for HTTP port
safe_set_env_var("VOICE_CLI_PORT", "9090");
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
let config = Config::load_with_env_overrides(&config_path).unwrap();
// Verify HTTP port was overridden
assert_eq!(config.server.port, 9090);
// Clean up
safe_remove_env_var("VOICE_CLI_PORT");
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_invalid_port_environment_variable() {
clear_voice_cli_env_vars();
// Set invalid environment variable
safe_set_env_var("VOICE_CLI_PORT", "invalid_port");
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
let result = Config::load_with_env_overrides(&config_path);
// Should fail with proper error message
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Invalid VOICE_CLI_PORT value 'invalid_port'"));
// Clean up
safe_remove_env_var("VOICE_CLI_PORT");
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_log_level_environment_override() {
clear_voice_cli_env_vars();
// Set environment variable for log level
safe_set_env_var("VOICE_CLI_LOG_LEVEL", "DEBUG");
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
let config = Config::load_with_env_overrides(&config_path).unwrap();
// Verify log level was overridden and normalized to lowercase
assert_eq!(config.logging.level, "debug");
// Clean up
safe_remove_env_var("VOICE_CLI_LOG_LEVEL");
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_invalid_log_level_environment_variable() {
clear_voice_cli_env_vars();
// Set invalid environment variable
safe_set_env_var("VOICE_CLI_LOG_LEVEL", "invalid_level");
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
let result = Config::load_with_env_overrides(&config_path);
// Should fail with proper error message
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Invalid VOICE_CLI_LOG_LEVEL"));
assert!(error_msg.contains("invalid_level"));
// Clean up
safe_remove_env_var("VOICE_CLI_LOG_LEVEL");
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_comprehensive_validation() {
clear_voice_cli_env_vars();
// Set multiple environment variables
// 注意使用有效的模型名称large-v3 而不是 large
safe_set_env_var("VOICE_CLI_PORT", "8081");
safe_set_env_var("VOICE_CLI_LOG_LEVEL", "warn");
safe_set_env_var("VOICE_CLI_DEFAULT_MODEL", "large-v3");
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
let config = Config::load_with_env_overrides(&config_path).unwrap();
// Verify all overrides were applied
assert_eq!(config.server.port, 8081);
assert_eq!(config.logging.level, "warn");
assert_eq!(config.whisper.default_model, "large-v3");
// Verify validation passes
assert!(config.validate().is_ok());
// Clean up
safe_remove_env_var("VOICE_CLI_PORT");
safe_remove_env_var("VOICE_CLI_LOG_LEVEL");
safe_remove_env_var("VOICE_CLI_DEFAULT_MODEL");
}
#[test]
fn test_empty_environment_variable_validation() {
clear_voice_cli_env_vars();
// Set empty environment variable
safe_set_env_var("VOICE_CLI_HOST", " "); // Use spaces instead of empty string
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
let result = Config::load_with_env_overrides(&config_path);
// Should fail with proper error message
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("VOICE_CLI_HOST environment variable cannot be empty"));
// Clean up
safe_remove_env_var("VOICE_CLI_HOST");
}
}

View File

@@ -0,0 +1,336 @@
#[cfg(test)]
mod config_validation_tests {
use crate::models::{
AudioProcessingConfig, Config, DaemonConfig, LoggingConfig, ServerConfig,
TaskManagementConfig, TtsConfig, WhisperConfig, WorkersConfig,
};
use std::path::PathBuf;
use tempfile::TempDir;
/// Helper function to create a valid base configuration
fn create_valid_config() -> Config {
Config {
server: ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
max_file_size: 25 * 1024 * 1024, // 25MB
cors_enabled: true,
},
whisper: WhisperConfig {
default_model: "base".to_string(),
models_dir: "./models".to_string(),
auto_download: true,
supported_models: vec!["base".to_string()],
audio_processing: AudioProcessingConfig::default(),
workers: WorkersConfig {
transcription_workers: 2,
channel_buffer_size: 100,
worker_timeout: 3600,
},
},
logging: LoggingConfig {
level: "info".to_string(),
log_dir: "./logs".to_string(),
max_file_size: "10MB".to_string(),
max_files: 10,
},
daemon: DaemonConfig {
pid_file: "./voice_cli.pid".to_string(),
log_file: "./logs/daemon.log".to_string(),
work_dir: "./work".to_string(),
},
task_management: TaskManagementConfig::default(),
tts: TtsConfig::default(),
}
}
#[test]
fn test_valid_config_validation() {
let config = create_valid_config();
assert!(config.validate().is_ok());
}
#[test]
fn test_empty_host_validation() {
let mut config = create_valid_config();
config.server.host = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Server host cannot be empty"));
}
#[test]
fn test_invalid_port_validation() {
let mut config = create_valid_config();
config.server.port = 0;
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error
.to_string()
.contains("Server port must be between 1 and 65535")
);
}
#[test]
fn test_port_too_high_validation() {
// Note: We can't test with 70000 as it doesn't fit in u16
// This test verifies the validation logic exists, but the type system
// already prevents invalid port values at compile time
let mut config = create_valid_config();
config.server.port = 65535; // Maximum valid port
let result = config.validate();
assert!(result.is_ok()); // Should be valid
}
#[test]
fn test_zero_max_file_size_validation() {
let mut config = create_valid_config();
config.server.max_file_size = 0;
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error
.to_string()
.contains("Max file size must be greater than 0")
);
}
#[test]
fn test_empty_default_model_validation() {
let mut config = create_valid_config();
config.whisper.default_model = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Default model cannot be empty"));
}
#[test]
fn test_empty_models_dir_validation() {
let mut config = create_valid_config();
config.whisper.models_dir = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error
.to_string()
.contains("Models directory cannot be empty")
);
}
#[test]
fn test_zero_transcription_workers_validation() {
let mut config = create_valid_config();
config.whisper.workers.transcription_workers = 0;
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error
.to_string()
.contains("Transcription workers must be greater than 0")
);
}
#[test]
fn test_invalid_log_level_validation() {
let mut config = create_valid_config();
config.logging.level = "invalid".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid log level"));
}
#[test]
fn test_valid_log_levels() {
let valid_levels = ["trace", "debug", "info", "warn", "error"];
for level in &valid_levels {
let mut config = create_valid_config();
config.logging.level = level.to_string();
let result = config.validate();
assert!(result.is_ok(), "Log level '{}' should be valid", level);
}
}
#[test]
fn test_case_insensitive_log_levels() {
let levels = ["TRACE", "Debug", "INFO", "Warn", "ERROR"];
for level in &levels {
let mut config = create_valid_config();
config.logging.level = level.to_string();
let result = config.validate();
assert!(
result.is_ok(),
"Log level '{}' should be valid (case insensitive)",
level
);
}
}
#[test]
fn test_empty_log_dir_validation() {
let mut config = create_valid_config();
config.logging.log_dir = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Log directory cannot be empty"));
}
#[test]
fn test_zero_max_files_validation() {
let mut config = create_valid_config();
config.logging.max_files = 0;
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error
.to_string()
.contains("Max log files must be greater than 0")
);
}
#[test]
fn test_empty_work_dir_validation() {
let mut config = create_valid_config();
config.daemon.work_dir = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Work directory cannot be empty"));
}
#[test]
fn test_empty_pid_file_validation() {
let mut config = create_valid_config();
config.daemon.pid_file = "".to_string();
let result = config.validate();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("PID file path cannot be empty"));
}
#[test]
fn test_multiple_validation_errors() {
let mut config = create_valid_config();
config.server.host = "".to_string();
config.server.port = 0;
config.whisper.default_model = "".to_string();
config.logging.level = "invalid".to_string();
let result = config.validate();
assert!(result.is_err());
// Should report the first error encountered
let error = result.unwrap_err();
assert!(error.to_string().contains("Server host cannot be empty"));
}
#[test]
fn test_config_file_loading_and_validation() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.yml");
// Write invalid config
let invalid_config_yaml = r#"
server:
host: ""
port: 0
whisper:
default_model: ""
"#;
std::fs::write(&config_path, invalid_config_yaml).unwrap();
// Loading should fail due to validation
let result = Config::load_or_create(&config_path);
assert!(result.is_err());
}
#[test]
fn test_config_path_helpers() {
let config = create_valid_config();
// Test path helper methods
let models_path = config.models_dir_path();
assert_eq!(models_path, PathBuf::from("./models"));
let log_path = config.log_dir_path();
assert_eq!(log_path, PathBuf::from("./logs"));
}
#[test]
fn test_edge_case_port_values() {
let mut config = create_valid_config();
// Test minimum valid port
config.server.port = 1;
assert!(config.validate().is_ok());
// Test maximum valid port
config.server.port = 65535;
assert!(config.validate().is_ok());
// Note: Can't test 65536 as it doesn't fit in u16
// The type system prevents invalid port values at compile time
}
#[test]
fn test_transcription_workers_boundary() {
let mut config = create_valid_config();
// Test minimum valid workers
config.whisper.workers.transcription_workers = 1;
assert!(config.validate().is_ok());
// Test high number of workers
config.whisper.workers.transcription_workers = 100;
assert!(config.validate().is_ok());
}
#[test]
fn test_max_files_boundary() {
let mut config = create_valid_config();
// Test minimum valid max files
config.logging.max_files = 1;
assert!(config.validate().is_ok());
// Test high number of files
config.logging.max_files = 1000;
assert!(config.validate().is_ok());
}
}

View File

@@ -0,0 +1,59 @@
#[cfg(test)]
mod graceful_shutdown_tests {
use crate::models::Config;
use crate::server;
use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::info;
#[tokio::test]
async fn test_graceful_shutdown_mechanism() {
// Test that the broadcast channel shutdown mechanism works
let (shutdown_tx, _) = broadcast::channel(1);
let mut shutdown_rx = shutdown_tx.subscribe();
// Test sending and receiving shutdown signals
let send_handle = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
info!("Sending shutdown signal...");
shutdown_tx
.send(())
.expect("Failed to send shutdown signal");
});
let receive_handle = tokio::spawn(async move {
info!("Waiting for shutdown signal...");
let result = shutdown_rx.recv().await;
assert!(result.is_ok(), "Should receive shutdown signal");
info!("Received shutdown signal successfully");
});
// Wait for both tasks to complete
let (send_result, receive_result) = tokio::join!(send_handle, receive_handle);
assert!(
send_result.is_ok(),
"Send task should complete successfully"
);
assert!(
receive_result.is_ok(),
"Receive task should complete successfully"
);
}
#[tokio::test]
async fn test_app_state_shutdown() {
// Test that AppState can be created and shut down gracefully
let config = Config::default();
let config_arc = Arc::new(config);
let app_state = server::handlers::AppState::new(config_arc.clone())
.await
.expect("Failed to create app state");
// Test that shutdown works
app_state.shutdown().await;
info!("AppState shutdown completed successfully");
}
}

View File

@@ -0,0 +1,11 @@
#[cfg(test)]
pub mod config_env_tests;
#[cfg(test)]
pub mod config_validation_tests;
#[cfg(test)]
pub mod task_management_integration_tests;
#[cfg(test)]
pub mod graceful_shutdown_test;

View File

@@ -0,0 +1,55 @@
#[cfg(test)]
mod task_management_integration_tests {
use crate::models::{AsyncTranscriptionTask, Config, TaskManagementConfig, TaskStatus};
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
async fn create_test_config() -> (Arc<Config>, TempDir) {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test_tasks.db");
let mut config = Config::default();
config.task_management = TaskManagementConfig {
max_concurrent_tasks: 2,
retry_attempts: 2,
task_timeout_seconds: 30,
catch_panic: true,
sqlite_db_path: db_path.to_string_lossy().to_string(),
task_retention_minutes: 1440, // 24 hours in minutes
sled_db_path: "./data/sled".to_string(),
};
(Arc::new(config), temp_dir)
}
// #[tokio::test]
// async fn test_task_store_crud_operations() {
// let (config, _temp_dir) = create_test_config().await;
// let task_store = Arc::new(TaskStore::from_config(&config.task_management).await.unwrap());
// // Create test task
// let task = AsyncTranscriptionTask::new(
// "test-crud-task".to_string(),
// PathBuf::from("test.mp3"),
// "test.mp3".to_string(),
// Some("base".to_string()),
// Some("json".to_string()),
// );
// // Test save task
// task_store.save_task("test-crud-task", &task).await.unwrap();
// // Test get task
// let retrieved_task = task_store.get_task("test-crud-task").await.unwrap();
// assert!(retrieved_task.is_some());
// let retrieved_task = retrieved_task.unwrap();
// assert_eq!(retrieved_task.task_id, "test-crud-task");
// assert_eq!(retrieved_task.original_filename, "test.mp3");
// // Test get status
// let status = task_store.get_status("test-crud-task").await.unwrap();
// assert!(status.is_some());
// assert!(matches!(status.unwrap(), TaskStatus::Pending { .. }));
// }
}