1#![allow(deprecated)]
2use crate::config::{EnvironmentProvider, SystemEnvironmentProvider};
9use crate::{Result, config::Config, error::SubXError};
10use config::{Config as ConfigCrate, ConfigBuilder, Environment, File, builder::DefaultState};
11use log::debug;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, RwLock};
14
15pub trait ConfigService: Send + Sync {
20 fn get_config(&self) -> Result<Config>;
40
41 fn reload(&self) -> Result<()>;
50
51 fn save_config(&self) -> Result<()>;
60
61 fn save_config_to_file(&self, path: &Path) -> Result<()>;
74
75 fn get_config_file_path(&self) -> Result<PathBuf>;
82
83 fn get_config_value(&self, key: &str) -> Result<String>;
93
94 fn reset_to_defaults(&self) -> Result<()>;
103
104 fn set_config_value(&self, key: &str, value: &str) -> Result<()>;
118}
119
120pub struct ProductionConfigService {
129 config_builder: ConfigBuilder<DefaultState>,
130 cached_config: Arc<RwLock<Option<Config>>>,
131 env_provider: Arc<dyn EnvironmentProvider>,
132}
133
134impl ProductionConfigService {
135 pub fn new() -> Result<Self> {
142 Self::with_env_provider(Arc::new(SystemEnvironmentProvider::new()))
143 }
144
145 pub fn with_env_provider(env_provider: Arc<dyn EnvironmentProvider>) -> Result<Self> {
150 let config_file_path = if let Some(custom_path) = env_provider.get_var("SUBX_CONFIG_PATH") {
152 PathBuf::from(custom_path)
153 } else {
154 Self::user_config_path()
155 };
156
157 let config_builder = ConfigCrate::builder()
158 .add_source(File::with_name("config/default").required(false))
159 .add_source(File::from(config_file_path).required(false))
160 .add_source(Environment::with_prefix("SUBX").separator("_"));
161
162 Ok(Self {
163 config_builder,
164 cached_config: Arc::new(RwLock::new(None)),
165 env_provider,
166 })
167 }
168
169 pub fn with_custom_file(mut self, file_path: PathBuf) -> Result<Self> {
181 self.config_builder = self.config_builder.add_source(File::from(file_path));
182 Ok(self)
183 }
184
185 fn user_config_path() -> PathBuf {
190 dirs::config_dir()
191 .unwrap_or_else(|| PathBuf::from("."))
192 .join("subx")
193 .join("config.toml")
194 }
195
196 fn load_and_validate(&self) -> Result<Config> {
202 debug!("ProductionConfigService: Loading configuration from sources");
203
204 let config_crate = self.config_builder.build_cloned().map_err(|e| {
206 debug!("ProductionConfigService: Config build failed: {}", e);
207 SubXError::config(format!("Failed to build configuration: {}", e))
208 })?;
209
210 let mut app_config = Config::default();
212
213 if let Ok(config) = config_crate.clone().try_deserialize::<Config>() {
215 app_config = config;
216 debug!("ProductionConfigService: Full configuration loaded successfully");
217 } else {
218 debug!("ProductionConfigService: Full deserialization failed, attempting partial load");
219
220 if let Ok(raw_map) = config_crate
222 .try_deserialize::<std::collections::HashMap<String, serde_json::Value>>()
223 {
224 if let Some(ai_section) = raw_map.get("ai") {
226 if let Some(ai_obj) = ai_section.as_object() {
227 if let Some(api_key) = ai_obj.get("apikey").and_then(|v| v.as_str()) {
229 app_config.ai.api_key = Some(api_key.to_string());
230 debug!(
231 "ProductionConfigService: AI API key loaded from SUBX_AI_APIKEY"
232 );
233 }
234 if let Some(provider) = ai_obj.get("provider").and_then(|v| v.as_str()) {
235 app_config.ai.provider = provider.to_string();
236 debug!(
237 "ProductionConfigService: AI provider loaded from SUBX_AI_PROVIDER"
238 );
239 }
240 if let Some(model) = ai_obj.get("model").and_then(|v| v.as_str()) {
241 app_config.ai.model = model.to_string();
242 debug!("ProductionConfigService: AI model loaded from SUBX_AI_MODEL");
243 }
244 if let Some(base_url) = ai_obj.get("base_url").and_then(|v| v.as_str()) {
245 app_config.ai.base_url = base_url.to_string();
246 debug!(
247 "ProductionConfigService: AI base URL loaded from SUBX_AI_BASE_URL"
248 );
249 }
250 }
251 }
252 }
253 }
254
255 if app_config.ai.api_key.is_none() {
258 if let Some(api_key) = self.env_provider.get_var("OPENAI_API_KEY") {
259 debug!("ProductionConfigService: Found OPENAI_API_KEY environment variable");
260 app_config.ai.api_key = Some(api_key);
261 }
262 }
263
264 if let Some(base_url) = self.env_provider.get_var("OPENAI_BASE_URL") {
266 debug!("ProductionConfigService: Found OPENAI_BASE_URL environment variable");
267 app_config.ai.base_url = base_url;
268 }
269
270 crate::config::validator::validate_config(&app_config).map_err(|e| {
272 debug!("ProductionConfigService: Config validation failed: {}", e);
273 SubXError::config(format!("Configuration validation failed: {}", e))
274 })?;
275
276 debug!("ProductionConfigService: Configuration loaded and validated successfully");
277 Ok(app_config)
278 }
279
280 fn validate_and_set_value(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
282 use crate::config::OverflowStrategy;
283 use crate::config::validation::*;
284 use crate::error::SubXError;
285
286 let parts: Vec<&str> = key.split('.').collect();
287 match parts.as_slice() {
288 ["ai", "provider"] => {
289 validate_enum(value, &["openai", "anthropic", "local"])?;
290 config.ai.provider = value.to_string();
291 }
292 ["ai", "api_key"] => {
293 if !value.is_empty() {
294 validate_api_key(value)?;
295 config.ai.api_key = Some(value.to_string());
296 } else {
297 config.ai.api_key = None;
298 }
299 }
300 ["ai", "model"] => {
301 config.ai.model = value.to_string();
302 }
303 ["ai", "base_url"] => {
304 validate_url(value)?;
305 config.ai.base_url = value.to_string();
306 }
307 ["ai", "max_sample_length"] => {
308 let v = validate_usize_range(value, 100, 10000)?;
309 config.ai.max_sample_length = v;
310 }
311 ["ai", "temperature"] => {
312 let v = validate_float_range(value, 0.0, 1.0)?;
313 config.ai.temperature = v;
314 }
315 ["ai", "max_tokens"] => {
316 let v = validate_uint_range(value, 1, 100000)?;
317 config.ai.max_tokens = v;
318 }
319 ["ai", "retry_attempts"] => {
320 let v = validate_uint_range(value, 1, 10)?;
321 config.ai.retry_attempts = v;
322 }
323 ["ai", "retry_delay_ms"] => {
324 let v = validate_u64_range(value, 100, 30000)?;
325 config.ai.retry_delay_ms = v;
326 }
327 ["ai", "request_timeout_seconds"] => {
328 let v = validate_u64_range(value, 10, 600)?;
329 config.ai.request_timeout_seconds = v;
330 }
331 ["formats", "default_output"] => {
332 validate_enum(value, &["srt", "ass", "vtt", "webvtt"])?;
333 config.formats.default_output = value.to_string();
334 }
335 ["formats", "preserve_styling"] => {
336 let v = parse_bool(value)?;
337 config.formats.preserve_styling = v;
338 }
339 ["formats", "default_encoding"] => {
340 validate_enum(value, &["utf-8", "gbk", "big5", "shift_jis"])?;
341 config.formats.default_encoding = value.to_string();
342 }
343 ["formats", "encoding_detection_confidence"] => {
344 let v = validate_float_range(value, 0.0, 1.0)?;
345 config.formats.encoding_detection_confidence = v;
346 }
347 ["sync", "max_offset_seconds"] => {
348 let v = validate_float_range(value, 0.1, 3600.0)?; config.sync.max_offset_seconds = v;
350 }
351 ["sync", "default_method"] => {
352 validate_enum(value, &["auto", "vad"])?;
353 config.sync.default_method = value.to_string();
354 }
355 ["sync", "vad", "enabled"] => {
356 let v = parse_bool(value)?;
357 config.sync.vad.enabled = v;
358 }
359 ["sync", "vad", "sensitivity"] => {
360 let v = validate_float_range(value, 0.0, 1.0)?;
361 config.sync.vad.sensitivity = v;
362 }
363 ["sync", "vad", "chunk_size"] => {
364 let v = validate_usize_range(value, 1, usize::MAX)?;
365 config.sync.vad.chunk_size = v;
366 }
367 ["sync", "vad", "sample_rate"] => {
368 validate_enum(value, &["8000", "16000", "22050", "44100", "48000"])?;
369 config.sync.vad.sample_rate = value.parse().unwrap();
370 }
371 ["sync", "vad", "padding_chunks"] => {
372 let v = validate_uint_range(value, 0, u32::MAX)?;
373 config.sync.vad.padding_chunks = v;
374 }
375 ["sync", "vad", "min_speech_duration_ms"] => {
376 let v = validate_uint_range(value, 0, u32::MAX)?;
377 config.sync.vad.min_speech_duration_ms = v;
378 }
379 ["sync", "vad", "speech_merge_gap_ms"] => {
380 let v = validate_uint_range(value, 0, u32::MAX)?;
381 config.sync.vad.speech_merge_gap_ms = v;
382 }
383 ["general", "backup_enabled"] => {
384 let v = parse_bool(value)?;
385 config.general.backup_enabled = v;
386 }
387 ["general", "max_concurrent_jobs"] => {
388 let v = validate_usize_range(value, 1, 64)?;
389 config.general.max_concurrent_jobs = v;
390 }
391 ["general", "task_timeout_seconds"] => {
392 let v = validate_u64_range(value, 30, 3600)?;
393 config.general.task_timeout_seconds = v;
394 }
395 ["general", "enable_progress_bar"] => {
396 let v = parse_bool(value)?;
397 config.general.enable_progress_bar = v;
398 }
399 ["general", "worker_idle_timeout_seconds"] => {
400 let v = validate_u64_range(value, 10, 3600)?;
401 config.general.worker_idle_timeout_seconds = v;
402 }
403 ["parallel", "max_workers"] => {
404 let v = validate_usize_range(value, 1, 64)?;
405 config.parallel.max_workers = v;
406 }
407 ["parallel", "task_queue_size"] => {
408 let v = validate_usize_range(value, 100, 10000)?;
409 config.parallel.task_queue_size = v;
410 }
411 ["parallel", "enable_task_priorities"] => {
412 let v = parse_bool(value)?;
413 config.parallel.enable_task_priorities = v;
414 }
415 ["parallel", "auto_balance_workers"] => {
416 let v = parse_bool(value)?;
417 config.parallel.auto_balance_workers = v;
418 }
419 ["parallel", "overflow_strategy"] => {
420 validate_enum(value, &["Block", "Drop", "Expand"])?;
421 config.parallel.overflow_strategy = match value {
422 "Block" => OverflowStrategy::Block,
423 "Drop" => OverflowStrategy::Drop,
424 "Expand" => OverflowStrategy::Expand,
425 _ => unreachable!(),
426 };
427 }
428 _ => {
429 return Err(SubXError::config(format!(
430 "Unknown configuration key: {}",
431 key
432 )));
433 }
434 }
435 Ok(())
436 }
437
438 fn save_config_to_file_with_config(
440 &self,
441 path: &std::path::Path,
442 config: &Config,
443 ) -> Result<()> {
444 let toml_content = toml::to_string_pretty(config)
445 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
446 if let Some(parent) = path.parent() {
447 std::fs::create_dir_all(parent).map_err(|e| {
448 SubXError::config(format!("Failed to create config directory: {}", e))
449 })?;
450 }
451 std::fs::write(path, toml_content)
452 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
453 Ok(())
454 }
455}
456
457impl ConfigService for ProductionConfigService {
458 fn get_config(&self) -> Result<Config> {
459 {
461 let cache = self.cached_config.read().unwrap();
462 if let Some(config) = cache.as_ref() {
463 debug!("ProductionConfigService: Returning cached configuration");
464 return Ok(config.clone());
465 }
466 }
467
468 let app_config = self.load_and_validate()?;
470
471 {
473 let mut cache = self.cached_config.write().unwrap();
474 *cache = Some(app_config.clone());
475 }
476
477 Ok(app_config)
478 }
479
480 fn reload(&self) -> Result<()> {
481 debug!("ProductionConfigService: Reloading configuration");
482
483 {
485 let mut cache = self.cached_config.write().unwrap();
486 *cache = None;
487 }
488
489 self.get_config()?;
491
492 debug!("ProductionConfigService: Configuration reloaded successfully");
493 Ok(())
494 }
495
496 fn save_config(&self) -> Result<()> {
497 let _config = self.get_config()?;
498 let path = self.get_config_file_path()?;
499 self.save_config_to_file(&path)
500 }
501
502 fn save_config_to_file(&self, path: &Path) -> Result<()> {
503 let config = self.get_config()?;
504 let toml_content = toml::to_string_pretty(&config)
505 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
506
507 if let Some(parent) = path.parent() {
508 std::fs::create_dir_all(parent).map_err(|e| {
509 SubXError::config(format!("Failed to create config directory: {}", e))
510 })?;
511 }
512
513 std::fs::write(path, toml_content)
514 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
515
516 Ok(())
517 }
518
519 fn get_config_file_path(&self) -> Result<PathBuf> {
520 if let Some(custom) = self.env_provider.get_var("SUBX_CONFIG_PATH") {
522 return Ok(PathBuf::from(custom));
523 }
524
525 let config_dir = dirs::config_dir()
526 .ok_or_else(|| SubXError::config("Unable to determine config directory"))?;
527 Ok(config_dir.join("subx").join("config.toml"))
528 }
529
530 fn get_config_value(&self, key: &str) -> Result<String> {
531 let config = self.get_config()?;
532 let parts: Vec<&str> = key.split('.').collect();
533 match parts.as_slice() {
534 ["ai", "provider"] => Ok(config.ai.provider.clone()),
535 ["ai", "model"] => Ok(config.ai.model.clone()),
536 ["ai", "api_key"] => Ok(config.ai.api_key.clone().unwrap_or_default()),
537 ["ai", "base_url"] => Ok(config.ai.base_url.clone()),
538 ["ai", "max_sample_length"] => Ok(config.ai.max_sample_length.to_string()),
539 ["ai", "temperature"] => Ok(config.ai.temperature.to_string()),
540 ["ai", "max_tokens"] => Ok(config.ai.max_tokens.to_string()),
541 ["ai", "retry_attempts"] => Ok(config.ai.retry_attempts.to_string()),
542 ["ai", "retry_delay_ms"] => Ok(config.ai.retry_delay_ms.to_string()),
543 ["ai", "request_timeout_seconds"] => Ok(config.ai.request_timeout_seconds.to_string()),
544
545 ["formats", "default_output"] => Ok(config.formats.default_output.clone()),
546 ["formats", "default_encoding"] => Ok(config.formats.default_encoding.clone()),
547 ["formats", "preserve_styling"] => Ok(config.formats.preserve_styling.to_string()),
548 ["formats", "encoding_detection_confidence"] => {
549 Ok(config.formats.encoding_detection_confidence.to_string())
550 }
551
552 ["sync", "default_method"] => Ok(config.sync.default_method.clone()),
553 ["sync", "max_offset_seconds"] => Ok(config.sync.max_offset_seconds.to_string()),
554 ["sync", "vad", "enabled"] => Ok(config.sync.vad.enabled.to_string()),
555 ["sync", "vad", "sensitivity"] => Ok(config.sync.vad.sensitivity.to_string()),
556 ["sync", "vad", "chunk_size"] => Ok(config.sync.vad.chunk_size.to_string()),
557 ["sync", "vad", "sample_rate"] => Ok(config.sync.vad.sample_rate.to_string()),
558 ["sync", "vad", "padding_chunks"] => Ok(config.sync.vad.padding_chunks.to_string()),
559 ["sync", "vad", "min_speech_duration_ms"] => {
560 Ok(config.sync.vad.min_speech_duration_ms.to_string())
561 }
562 ["sync", "vad", "speech_merge_gap_ms"] => {
563 Ok(config.sync.vad.speech_merge_gap_ms.to_string())
564 }
565
566 ["general", "backup_enabled"] => Ok(config.general.backup_enabled.to_string()),
567 ["general", "max_concurrent_jobs"] => {
568 Ok(config.general.max_concurrent_jobs.to_string())
569 }
570 ["general", "task_timeout_seconds"] => {
571 Ok(config.general.task_timeout_seconds.to_string())
572 }
573 ["general", "enable_progress_bar"] => {
574 Ok(config.general.enable_progress_bar.to_string())
575 }
576 ["general", "worker_idle_timeout_seconds"] => {
577 Ok(config.general.worker_idle_timeout_seconds.to_string())
578 }
579
580 ["parallel", "max_workers"] => Ok(config.parallel.max_workers.to_string()),
581 ["parallel", "task_queue_size"] => Ok(config.parallel.task_queue_size.to_string()),
582 ["parallel", "enable_task_priorities"] => {
583 Ok(config.parallel.enable_task_priorities.to_string())
584 }
585 ["parallel", "auto_balance_workers"] => {
586 Ok(config.parallel.auto_balance_workers.to_string())
587 }
588 ["parallel", "overflow_strategy"] => {
589 Ok(format!("{:?}", config.parallel.overflow_strategy))
590 }
591
592 _ => Err(SubXError::config(format!(
593 "Unknown configuration key: {}",
594 key
595 ))),
596 }
597 }
598
599 fn set_config_value(&self, key: &str, value: &str) -> Result<()> {
600 let mut config = self.get_config()?;
602
603 self.validate_and_set_value(&mut config, key, value)?;
605
606 crate::config::validator::validate_config(&config)?;
608
609 let path = self.get_config_file_path()?;
611 self.save_config_to_file_with_config(&path, &config)?;
612
613 {
615 let mut cache = self.cached_config.write().unwrap();
616 *cache = Some(config);
617 }
618
619 Ok(())
620 }
621
622 fn reset_to_defaults(&self) -> Result<()> {
623 let default_config = Config::default();
624 let path = self.get_config_file_path()?;
625
626 let toml_content = toml::to_string_pretty(&default_config)
627 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
628
629 if let Some(parent) = path.parent() {
630 std::fs::create_dir_all(parent).map_err(|e| {
631 SubXError::config(format!("Failed to create config directory: {}", e))
632 })?;
633 }
634
635 std::fs::write(&path, toml_content)
636 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
637
638 self.reload()
639 }
640}
641
642impl Default for ProductionConfigService {
643 fn default() -> Self {
644 Self::new().expect("Failed to create default ProductionConfigService")
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::config::TestConfigService;
652 use crate::config::TestEnvironmentProvider;
653 use std::sync::Arc;
654
655 #[test]
656 fn test_production_config_service_creation() {
657 let service = ProductionConfigService::new();
658 assert!(service.is_ok());
659 }
660
661 #[test]
662 fn test_production_config_service_with_custom_file() {
663 let service = ProductionConfigService::new()
664 .unwrap()
665 .with_custom_file(PathBuf::from("test.toml"));
666 assert!(service.is_ok());
667 }
668
669 #[test]
670 fn test_production_service_implements_config_service_trait() {
671 let service = ProductionConfigService::new().unwrap();
672
673 let config1 = service.get_config();
675 assert!(config1.is_ok());
676
677 let reload_result = service.reload();
678 assert!(reload_result.is_ok());
679
680 let config2 = service.get_config();
681 assert!(config2.is_ok());
682 }
683
684 #[test]
685 fn test_config_service_with_openai_api_key() {
686 let test_service = TestConfigService::with_ai_settings_and_key(
688 "openai",
689 "gpt-4.1-mini",
690 "sk-test-openai-key-123",
691 );
692
693 let config = test_service.get_config().unwrap();
694 assert_eq!(
695 config.ai.api_key,
696 Some("sk-test-openai-key-123".to_string())
697 );
698 assert_eq!(config.ai.provider, "openai");
699 assert_eq!(config.ai.model, "gpt-4.1-mini");
700 }
701
702 #[test]
703 fn test_config_service_with_custom_base_url() {
704 let mut config = Config::default();
706 config.ai.base_url = "https://custom.openai.endpoint".to_string();
707
708 let test_service = TestConfigService::new(config);
709 let loaded_config = test_service.get_config().unwrap();
710
711 assert_eq!(loaded_config.ai.base_url, "https://custom.openai.endpoint");
712 }
713
714 #[test]
715 fn test_config_service_with_both_openai_settings() {
716 let mut config = Config::default();
718 config.ai.api_key = Some("sk-test-api-key-combined".to_string());
719 config.ai.base_url = "https://api.custom-openai.com".to_string();
720
721 let test_service = TestConfigService::new(config);
722 let loaded_config = test_service.get_config().unwrap();
723
724 assert_eq!(
725 loaded_config.ai.api_key,
726 Some("sk-test-api-key-combined".to_string())
727 );
728 assert_eq!(loaded_config.ai.base_url, "https://api.custom-openai.com");
729 }
730
731 #[test]
732 fn test_config_service_provider_precedence() {
733 let test_service =
735 TestConfigService::with_ai_settings_and_key("openai", "gpt-4.1", "sk-explicit-key");
736
737 let config = test_service.get_config().unwrap();
738 assert_eq!(config.ai.api_key, Some("sk-explicit-key".to_string()));
739 assert_eq!(config.ai.provider, "openai");
740 assert_eq!(config.ai.model, "gpt-4.1");
741 }
742
743 #[test]
744 fn test_config_service_fallback_behavior() {
745 let test_service = TestConfigService::with_defaults();
747 let config = test_service.get_config().unwrap();
748
749 assert_eq!(config.ai.provider, "openai");
751 assert_eq!(config.ai.model, "gpt-4.1-mini");
752 assert_eq!(config.ai.base_url, "https://api.openai.com/v1");
753 assert_eq!(config.ai.api_key, None); }
755
756 #[test]
757 fn test_config_service_reload_functionality() {
758 let test_service = TestConfigService::with_defaults();
760
761 let config1 = test_service.get_config().unwrap();
763 assert_eq!(config1.ai.provider, "openai");
764
765 let reload_result = test_service.reload();
767 assert!(reload_result.is_ok());
768
769 let config2 = test_service.get_config().unwrap();
771 assert_eq!(config2.ai.provider, "openai");
772 }
773
774 #[test]
775 fn test_config_service_custom_base_url_override() {
776 let mut config = Config::default();
778 config.ai.base_url = "https://my-proxy.openai.com/v1".to_string();
779
780 let test_service = TestConfigService::new(config);
781 let loaded_config = test_service.get_config().unwrap();
782
783 assert_eq!(loaded_config.ai.base_url, "https://my-proxy.openai.com/v1");
784 }
785
786 #[test]
787 fn test_config_service_sync_settings() {
788 let test_service = TestConfigService::with_sync_settings(0.8, 45.0);
790 let config = test_service.get_config().unwrap();
791
792 assert_eq!(config.sync.correlation_threshold, 0.8);
793 assert_eq!(config.sync.max_offset_seconds, 45.0);
794 }
795
796 #[test]
797 fn test_config_service_parallel_settings() {
798 let test_service = TestConfigService::with_parallel_settings(8, 200);
800 let config = test_service.get_config().unwrap();
801
802 assert_eq!(config.general.max_concurrent_jobs, 8);
803 assert_eq!(config.parallel.task_queue_size, 200);
804 }
805
806 #[test]
807 fn test_config_service_direct_access() {
808 let test_service = TestConfigService::with_defaults();
810
811 assert_eq!(test_service.config().ai.provider, "openai");
813
814 test_service.config_mut().ai.provider = "modified".to_string();
816 assert_eq!(test_service.config().ai.provider, "modified");
817
818 let config = test_service.get_config().unwrap();
820 assert_eq!(config.ai.provider, "modified");
821 }
822
823 #[test]
824 fn test_production_config_service_openai_api_key_loading() {
825 let mut env_provider = TestEnvironmentProvider::new();
827 env_provider.set_var("OPENAI_API_KEY", "sk-test-openai-key-env");
828
829 env_provider.set_var(
831 "SUBX_CONFIG_PATH",
832 "/tmp/test_config_that_does_not_exist.toml",
833 );
834
835 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
836 .expect("Failed to create config service");
837
838 let config = service.get_config().expect("Failed to get config");
839
840 assert_eq!(
841 config.ai.api_key,
842 Some("sk-test-openai-key-env".to_string())
843 );
844 }
845
846 #[test]
847 fn test_production_config_service_openai_base_url_loading() {
848 let mut env_provider = TestEnvironmentProvider::new();
850 env_provider.set_var("OPENAI_BASE_URL", "https://test.openai.com/v1");
851
852 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
853 .expect("Failed to create config service");
854
855 let config = service.get_config().expect("Failed to get config");
856
857 assert_eq!(config.ai.base_url, "https://test.openai.com/v1");
858 }
859
860 #[test]
861 fn test_production_config_service_both_openai_env_vars() {
862 let mut env_provider = TestEnvironmentProvider::new();
864 env_provider.set_var("OPENAI_API_KEY", "sk-test-key-both");
865 env_provider.set_var("OPENAI_BASE_URL", "https://both.openai.com/v1");
866
867 env_provider.set_var(
869 "SUBX_CONFIG_PATH",
870 "/tmp/test_config_both_that_does_not_exist.toml",
871 );
872
873 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
874 .expect("Failed to create config service");
875
876 let config = service.get_config().expect("Failed to get config");
877
878 assert_eq!(config.ai.api_key, Some("sk-test-key-both".to_string()));
879 assert_eq!(config.ai.base_url, "https://both.openai.com/v1");
880 }
881
882 #[test]
883 fn test_production_config_service_no_openai_env_vars() {
884 let mut env_provider = TestEnvironmentProvider::new(); env_provider.set_var(
889 "SUBX_CONFIG_PATH",
890 "/tmp/test_config_no_openai_that_does_not_exist.toml",
891 );
892
893 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
894 .expect("Failed to create config service");
895
896 let config = service.get_config().expect("Failed to get config");
897
898 assert_eq!(config.ai.api_key, None);
900 assert_eq!(config.ai.base_url, "https://api.openai.com/v1"); }
902
903 #[test]
904 fn test_production_config_service_api_key_priority() {
905 let mut env_provider = TestEnvironmentProvider::new();
907 env_provider.set_var("OPENAI_API_KEY", "sk-env-key");
908 env_provider.set_var("SUBX_AI_APIKEY", "sk-config-key");
910
911 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
912 .expect("Failed to create config service");
913
914 let config = service.get_config().expect("Failed to get config");
915
916 assert!(config.ai.api_key.is_some());
919 }
920}