Skip to main content

subx_cli/config/
test_service.rs

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