subx_cli/config/
test_service.rs

1#![allow(deprecated)]
2//! Test configuration service for isolated testing.
3//!
4//! This module provides a configuration service implementation specifically
5//! designed for testing environments, offering complete isolation and
6//! predictable configuration states.
7
8use crate::config::service::ConfigService;
9use crate::error::SubXError;
10use crate::{Result, config::Config};
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14/// Test configuration service implementation.
15///
16/// This service provides a fixed configuration for testing purposes,
17/// ensuring complete isolation between tests and predictable behavior.
18/// It does not load from external sources or cache.
19pub struct TestConfigService {
20    config: Mutex<Config>,
21}
22
23impl TestConfigService {
24    /// Create a new test configuration service with the provided configuration.
25    ///
26    /// # Arguments
27    ///
28    /// * `config` - The fixed configuration to use
29    pub fn new(config: Config) -> Self {
30        Self {
31            config: Mutex::new(config),
32        }
33    }
34
35    /// Create a test configuration service with default settings.
36    ///
37    /// This is useful for tests that don't need specific configuration values.
38    pub fn with_defaults() -> Self {
39        Self::new(Config::default())
40    }
41
42    /// Create a test configuration service with specific AI settings.
43    ///
44    /// # Arguments
45    ///
46    /// * `provider` - AI provider name
47    /// * `model` - AI model name
48    pub fn with_ai_settings(provider: &str, model: &str) -> Self {
49        let mut config = Config::default();
50        config.ai.provider = provider.to_string();
51        config.ai.model = model.to_string();
52        Self::new(config)
53    }
54
55    /// Create a test configuration service with specific AI settings including API key.
56    ///
57    /// # Arguments
58    ///
59    /// * `provider` - AI provider name
60    /// * `model` - AI model name
61    /// * `api_key` - API key for the provider
62    pub fn with_ai_settings_and_key(provider: &str, model: &str, api_key: &str) -> Self {
63        let mut config = Config::default();
64        config.ai.provider = provider.to_string();
65        config.ai.model = model.to_string();
66        config.ai.api_key = Some(api_key.to_string());
67        Self::new(config)
68    }
69
70    /// Create a test configuration service with specific sync settings.
71    ///
72    /// # Arguments
73    ///
74    /// * `correlation_threshold` - Correlation threshold for synchronization
75    /// * `max_offset` - Maximum time offset in seconds
76    pub fn with_sync_settings(correlation_threshold: f32, max_offset: f32) -> Self {
77        let mut config = Config::default();
78        config.sync.correlation_threshold = correlation_threshold;
79        config.sync.max_offset_seconds = max_offset;
80        Self::new(config)
81    }
82
83    /// Create a test configuration service with specific parallel processing settings.
84    ///
85    /// # Arguments
86    ///
87    /// * `max_workers` - Maximum number of parallel workers
88    /// * `queue_size` - Task queue size
89    pub fn with_parallel_settings(max_workers: usize, queue_size: usize) -> Self {
90        let mut config = Config::default();
91        config.general.max_concurrent_jobs = max_workers;
92        config.parallel.task_queue_size = queue_size;
93        Self::new(config)
94    }
95
96    /// Get the underlying configuration.
97    ///
98    /// This is useful for tests that need direct access to the configuration object.
99    pub fn config(&self) -> std::sync::MutexGuard<Config> {
100        self.config.lock().unwrap()
101    }
102
103    /// Get a mutable reference to the underlying configuration.
104    ///
105    /// This allows tests to modify the configuration after creation.
106    pub fn config_mut(&self) -> std::sync::MutexGuard<Config> {
107        self.config.lock().unwrap()
108    }
109}
110
111impl ConfigService for TestConfigService {
112    fn get_config(&self) -> Result<Config> {
113        Ok(self.config.lock().unwrap().clone())
114    }
115
116    fn reload(&self) -> Result<()> {
117        // Test configuration doesn't need reloading since it's fixed
118        Ok(())
119    }
120
121    fn save_config(&self) -> Result<()> {
122        // Test environment does not perform actual file I/O
123        Ok(())
124    }
125
126    fn save_config_to_file(&self, _path: &Path) -> Result<()> {
127        // Test environment does not perform actual file I/O
128        Ok(())
129    }
130
131    fn get_config_file_path(&self) -> Result<PathBuf> {
132        // Return a dummy path to avoid conflicts in test environment
133        Ok(PathBuf::from("/tmp/subx_test_config.toml"))
134    }
135
136    fn get_config_value(&self, key: &str) -> Result<String> {
137        // Delegate to current configuration
138        // Note: unwrap_or_default to handle Option fields
139        let config = self.config.lock().unwrap();
140        let parts: Vec<&str> = key.split('.').collect();
141        match parts.as_slice() {
142            ["ai", "provider"] => Ok(config.ai.provider.clone()),
143            ["ai", "model"] => Ok(config.ai.model.clone()),
144            ["ai", "api_key"] => Ok(config.ai.api_key.clone().unwrap_or_default()),
145            ["ai", "base_url"] => Ok(config.ai.base_url.clone()),
146            ["ai", "temperature"] => Ok(config.ai.temperature.to_string()),
147            ["ai", "max_sample_length"] => Ok(config.ai.max_sample_length.to_string()),
148            ["ai", "max_tokens"] => Ok(config.ai.max_tokens.to_string()),
149            ["ai", "retry_attempts"] => Ok(config.ai.retry_attempts.to_string()),
150            ["ai", "retry_delay_ms"] => Ok(config.ai.retry_delay_ms.to_string()),
151            ["ai", "request_timeout_seconds"] => Ok(config.ai.request_timeout_seconds.to_string()),
152            ["formats", "default_output"] => Ok(config.formats.default_output.clone()),
153            ["formats", "default_encoding"] => Ok(config.formats.default_encoding.clone()),
154            ["formats", "preserve_styling"] => Ok(config.formats.preserve_styling.to_string()),
155            ["formats", "encoding_detection_confidence"] => {
156                Ok(config.formats.encoding_detection_confidence.to_string())
157            }
158            ["sync", "max_offset_seconds"] => Ok(config.sync.max_offset_seconds.to_string()),
159            ["sync", "default_method"] => Ok(config.sync.default_method.clone()),
160            ["sync", "vad", "enabled"] => Ok(config.sync.vad.enabled.to_string()),
161            ["sync", "vad", "sensitivity"] => Ok(config.sync.vad.sensitivity.to_string()),
162            ["sync", "vad", "chunk_size"] => Ok(config.sync.vad.chunk_size.to_string()),
163            ["sync", "vad", "sample_rate"] => Ok(config.sync.vad.sample_rate.to_string()),
164            ["sync", "vad", "padding_chunks"] => Ok(config.sync.vad.padding_chunks.to_string()),
165            ["sync", "vad", "min_speech_duration_ms"] => {
166                Ok(config.sync.vad.min_speech_duration_ms.to_string())
167            }
168            ["sync", "vad", "speech_merge_gap_ms"] => {
169                Ok(config.sync.vad.speech_merge_gap_ms.to_string())
170            }
171            ["general", "backup_enabled"] => Ok(config.general.backup_enabled.to_string()),
172            ["general", "task_timeout_seconds"] => {
173                Ok(config.general.task_timeout_seconds.to_string())
174            }
175            ["general", "enable_progress_bar"] => {
176                Ok(config.general.enable_progress_bar.to_string())
177            }
178            ["general", "worker_idle_timeout_seconds"] => {
179                Ok(config.general.worker_idle_timeout_seconds.to_string())
180            }
181            ["general", "max_concurrent_jobs"] => {
182                Ok(config.general.max_concurrent_jobs.to_string())
183            }
184            ["parallel", "max_workers"] => Ok(config.parallel.max_workers.to_string()),
185            ["parallel", "task_queue_size"] => Ok(config.parallel.task_queue_size.to_string()),
186            ["parallel", "enable_task_priorities"] => {
187                Ok(config.parallel.enable_task_priorities.to_string())
188            }
189            ["parallel", "auto_balance_workers"] => {
190                Ok(config.parallel.auto_balance_workers.to_string())
191            }
192            ["parallel", "overflow_strategy"] => {
193                Ok(format!("{:?}", config.parallel.overflow_strategy))
194            }
195            _ => Err(SubXError::config(format!(
196                "Unknown configuration key: {}",
197                key
198            ))),
199        }
200    }
201
202    fn reset_to_defaults(&self) -> Result<()> {
203        // Reset the configuration to default values
204        *self.config.lock().unwrap() = Config::default();
205        Ok(())
206    }
207
208    fn set_config_value(&self, key: &str, value: &str) -> Result<()> {
209        // Load current configuration
210        let mut cfg = self.get_config()?;
211        // Validate and set the value using the same logic as ProductionConfigService
212        self.validate_and_set_value(&mut cfg, key, value)?;
213        // Validate the entire configuration
214        crate::config::validator::validate_config(&cfg)?;
215        // Update the internal configuration
216        *self.config.lock().unwrap() = cfg;
217        Ok(())
218    }
219}
220
221impl TestConfigService {
222    /// Validate and set a configuration value (same logic as ProductionConfigService).
223    fn validate_and_set_value(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
224        use crate::config::OverflowStrategy;
225        use crate::config::validation::*;
226        use crate::error::SubXError;
227
228        let parts: Vec<&str> = key.split('.').collect();
229        match parts.as_slice() {
230            ["ai", "provider"] => {
231                validate_enum(value, &["openai", "anthropic", "local"])?;
232                config.ai.provider = value.to_string();
233            }
234            ["ai", "api_key"] => {
235                if !value.is_empty() {
236                    validate_api_key(value)?;
237                    config.ai.api_key = Some(value.to_string());
238                } else {
239                    config.ai.api_key = None;
240                }
241            }
242            ["ai", "model"] => {
243                config.ai.model = value.to_string();
244            }
245            ["ai", "base_url"] => {
246                validate_url(value)?;
247                config.ai.base_url = value.to_string();
248            }
249            ["ai", "max_sample_length"] => {
250                let v = validate_usize_range(value, 100, 10000)?;
251                config.ai.max_sample_length = v;
252            }
253            ["ai", "temperature"] => {
254                let v = validate_float_range(value, 0.0, 1.0)?;
255                config.ai.temperature = v;
256            }
257            ["ai", "max_tokens"] => {
258                let v = validate_uint_range(value, 1, 100_000)?;
259                config.ai.max_tokens = v;
260            }
261            ["ai", "retry_attempts"] => {
262                let v = validate_uint_range(value, 1, 10)?;
263                config.ai.retry_attempts = v;
264            }
265            ["ai", "retry_delay_ms"] => {
266                let v = validate_u64_range(value, 100, 30000)?;
267                config.ai.retry_delay_ms = v;
268            }
269            ["ai", "request_timeout_seconds"] => {
270                let v = validate_u64_range(value, 10, 600)?;
271                config.ai.request_timeout_seconds = v;
272            }
273            ["formats", "default_output"] => {
274                validate_enum(value, &["srt", "ass", "vtt", "webvtt"])?;
275                config.formats.default_output = value.to_string();
276            }
277            ["formats", "preserve_styling"] => {
278                let v = parse_bool(value)?;
279                config.formats.preserve_styling = v;
280            }
281            ["formats", "default_encoding"] => {
282                validate_enum(value, &["utf-8", "gbk", "big5", "shift_jis"])?;
283                config.formats.default_encoding = value.to_string();
284            }
285            ["formats", "encoding_detection_confidence"] => {
286                let v = validate_float_range(value, 0.0, 1.0)?;
287                config.formats.encoding_detection_confidence = v;
288            }
289            ["sync", "max_offset_seconds"] => {
290                let v = validate_float_range(value, 0.0, 300.0)?;
291                config.sync.max_offset_seconds = v;
292            }
293            ["sync", "default_method"] => {
294                validate_enum(value, &["auto", "vad"])?;
295                config.sync.default_method = value.to_string();
296            }
297            ["sync", "vad", "enabled"] => {
298                let v = parse_bool(value)?;
299                config.sync.vad.enabled = v;
300            }
301            ["sync", "vad", "sensitivity"] => {
302                let v = validate_float_range(value, 0.0, 1.0)?;
303                config.sync.vad.sensitivity = v;
304            }
305            ["sync", "vad", "chunk_size"] => {
306                let v = validate_usize_range(value, 1, usize::MAX)?;
307                config.sync.vad.chunk_size = v;
308            }
309            ["sync", "vad", "sample_rate"] => {
310                validate_enum(value, &["8000", "16000", "22050", "44100", "48000"])?;
311                config.sync.vad.sample_rate = value.parse().unwrap();
312            }
313            ["sync", "vad", "padding_chunks"] => {
314                let v = validate_uint_range(value, 0, u32::MAX)?;
315                config.sync.vad.padding_chunks = v;
316            }
317            ["sync", "vad", "min_speech_duration_ms"] => {
318                let v = validate_uint_range(value, 0, u32::MAX)?;
319                config.sync.vad.min_speech_duration_ms = v;
320            }
321            ["sync", "vad", "speech_merge_gap_ms"] => {
322                let v = validate_uint_range(value, 0, u32::MAX)?;
323                config.sync.vad.speech_merge_gap_ms = v;
324            }
325            ["sync", "correlation_threshold"] => {
326                let v = validate_float_range(value, 0.0, 1.0)?;
327                config.sync.correlation_threshold = v;
328            }
329            ["sync", "dialogue_detection_threshold"] => {
330                let v = validate_float_range(value, 0.0, 1.0)?;
331                config.sync.dialogue_detection_threshold = v;
332            }
333            ["sync", "min_dialogue_duration_ms"] => {
334                let v = validate_uint_range(value, 100, 5000)?;
335                config.sync.min_dialogue_duration_ms = v;
336            }
337            ["sync", "dialogue_merge_gap_ms"] => {
338                let v = validate_uint_range(value, 50, 2000)?;
339                config.sync.dialogue_merge_gap_ms = v;
340            }
341            ["sync", "enable_dialogue_detection"] => {
342                let v = parse_bool(value)?;
343                config.sync.enable_dialogue_detection = v;
344            }
345            ["sync", "audio_sample_rate"] => {
346                let v = validate_uint_range(value, 8000, 192000)?;
347                config.sync.audio_sample_rate = v;
348            }
349            ["sync", "auto_detect_sample_rate"] => {
350                let v = parse_bool(value)?;
351                config.sync.auto_detect_sample_rate = v;
352            }
353            ["general", "backup_enabled"] => {
354                let v = parse_bool(value)?;
355                config.general.backup_enabled = v;
356            }
357            ["general", "max_concurrent_jobs"] => {
358                let v = validate_usize_range(value, 1, 64)?;
359                config.general.max_concurrent_jobs = v;
360            }
361            ["general", "task_timeout_seconds"] => {
362                let v = validate_u64_range(value, 30, 3600)?;
363                config.general.task_timeout_seconds = v;
364            }
365            ["general", "enable_progress_bar"] => {
366                let v = parse_bool(value)?;
367                config.general.enable_progress_bar = v;
368            }
369            ["general", "worker_idle_timeout_seconds"] => {
370                let v = validate_u64_range(value, 10, 3600)?;
371                config.general.worker_idle_timeout_seconds = v;
372            }
373            ["parallel", "max_workers"] => {
374                let v = validate_usize_range(value, 1, 64)?;
375                config.parallel.max_workers = v;
376            }
377            ["parallel", "task_queue_size"] => {
378                let v = validate_usize_range(value, 100, 10000)?;
379                config.parallel.task_queue_size = v;
380            }
381            ["parallel", "enable_task_priorities"] => {
382                let v = parse_bool(value)?;
383                config.parallel.enable_task_priorities = v;
384            }
385            ["parallel", "auto_balance_workers"] => {
386                let v = parse_bool(value)?;
387                config.parallel.auto_balance_workers = v;
388            }
389            ["parallel", "overflow_strategy"] => {
390                validate_enum(value, &["Block", "Drop", "Expand"])?;
391                config.parallel.overflow_strategy = match value {
392                    "Block" => OverflowStrategy::Block,
393                    "Drop" => OverflowStrategy::Drop,
394                    "Expand" => OverflowStrategy::Expand,
395                    _ => unreachable!(),
396                };
397            }
398            _ => {
399                return Err(SubXError::config(format!(
400                    "Unknown configuration key: {}",
401                    key
402                )));
403            }
404        }
405        Ok(())
406    }
407}
408
409impl Default for TestConfigService {
410    fn default() -> Self {
411        Self::with_defaults()
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_config_service_with_defaults() {
421        let service = TestConfigService::with_defaults();
422        let config = service.get_config().unwrap();
423
424        assert_eq!(config.ai.provider, "openai");
425        assert_eq!(config.ai.model, "gpt-4.1-mini");
426    }
427
428    #[test]
429    fn test_config_service_with_ai_settings() {
430        let service = TestConfigService::with_ai_settings("anthropic", "claude-3");
431        let config = service.get_config().unwrap();
432
433        assert_eq!(config.ai.provider, "anthropic");
434        assert_eq!(config.ai.model, "claude-3");
435    }
436
437    #[test]
438    fn test_config_service_with_ai_settings_and_key() {
439        let service =
440            TestConfigService::with_ai_settings_and_key("openai", "gpt-4.1", "test-api-key");
441        let config = service.get_config().unwrap();
442
443        assert_eq!(config.ai.provider, "openai");
444        assert_eq!(config.ai.model, "gpt-4.1");
445        assert_eq!(config.ai.api_key, Some("test-api-key".to_string()));
446    }
447
448    #[test]
449    fn test_config_service_with_sync_settings() {
450        let service = TestConfigService::with_sync_settings(0.8, 45.0);
451        let config = service.get_config().unwrap();
452
453        assert_eq!(config.sync.correlation_threshold, 0.8);
454        assert_eq!(config.sync.max_offset_seconds, 45.0);
455    }
456
457    #[test]
458    fn test_config_service_with_parallel_settings() {
459        let service = TestConfigService::with_parallel_settings(8, 200);
460        let config = service.get_config().unwrap();
461
462        assert_eq!(config.general.max_concurrent_jobs, 8);
463        assert_eq!(config.parallel.task_queue_size, 200);
464    }
465
466    #[test]
467    fn test_config_service_reload() {
468        let service = TestConfigService::with_defaults();
469
470        // Reload should always succeed for test service
471        assert!(service.reload().is_ok());
472    }
473
474    #[test]
475    fn test_config_service_direct_access() {
476        let service = TestConfigService::with_defaults();
477
478        // Test direct read access
479        assert_eq!(service.config().ai.provider, "openai");
480
481        // Test mutable access
482        service.config_mut().ai.provider = "modified".to_string();
483        assert_eq!(service.config().ai.provider, "modified");
484
485        let config = service.get_config().unwrap();
486        assert_eq!(config.ai.provider, "modified");
487    }
488}